Mirror of https://github.com/roostorg/coop github.com/roostorg/coop
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 168 lines 6.2 kB view raw
1import { 2 Cache, 3 MemoryStore, 4 wrapProducer, 5 type AnyParams, 6 type AnyValidators, 7 type ConsumerDirectives, 8 type Entry, 9 type ProducerDirectives, 10} from '../lib/cache/index.js'; 11import { type ReadonlyDeep } from 'type-fest'; 12 13import { jsonParse, jsonStringify } from './encoding.js'; 14import { type JSON } from './json-schema-types.js'; 15 16type Opts<KeyType, CachedContentType, CacheKeyType extends string = string> = { 17 /** 18 * This is the function responsible for returning the content that will be 19 * cached for the provided key. 20 */ 21 producer(cacheKey: KeyType): Promise<CachedContentType>; 22 /** 23 * These are the "producer directives" (or a function that returns the 24 * producer directives given the producer's return value). These instruct the 25 * cache on how long the value is likely to still be correct (or at least 26 * worth serving from cache). Consumers can specify their own freshness 27 * requirements when reading from the cache, which may force the cache to get 28 * a new value even if the producer said the cached value is still probably 29 * ok (or, alternately, may allow a cached value to be used longer than the 30 * producer thinks is generally safe). 31 */ 32 directives: 33 | ProducerDirectives 34 | ((it: ReadonlyDeep<CachedContentType>) => ProducerDirectives); 35 collapseOverlappingRequestsTime?: number; 36 numItemsLimit?: number; 37 onItemEviction?(value: ReadonlyDeep<CachedContentType>, key: KeyType): void; 38 keyGeneration?: { 39 toString(it: KeyType): CacheKeyType; 40 fromString(str: CacheKeyType): KeyType; 41 }; 42}; 43 44type CachedFn<KeyType, CachedContentType> = { 45 ( 46 key: KeyType, 47 directives?: ConsumerDirectives, 48 ): Promise<ReadonlyDeep<CachedContentType>>; 49 close(): Promise<void>; 50 /** Invalidates the cached value for the given key. Used when the source data has been replaced (e.g. key rotation). */ 51 invalidate?(key: KeyType): Promise<void>; 52}; 53 54/** 55 * Basically, this is like {@link wrapProducer}, except it takes care of a bunch 56 * of other small details that our generic caching library doesn't and shouldn't 57 * know about, like that we're using an in-memory store; that we want to cache 58 * all produced values using the same directives (as given in the `directive` 59 * option); that we want to mark the cache result as readonly (because the 60 * in-memory store means it'll be reused between callers); etc. 61 */ 62export function cached< 63 KeyType extends ReadonlyDeep<JSON>, 64 CachedContentType, 65 CacheKeyType extends string = string, 66>( 67 opts: Opts<KeyType, CachedContentType, CacheKeyType>, 68): CachedFn<KeyType, CachedContentType>; 69export function cached< 70 KeyType, 71 CachedContentType, 72 CacheKeyType extends string = string, 73>( 74 // NB: if the KeyType is not JSON-compatible, the caller must provide their 75 // own key generation and parsing functions, as the default ones won't work! 76 opts: Opts<KeyType, CachedContentType, CacheKeyType> & 77 Required< 78 Pick<Opts<KeyType, CachedContentType, CacheKeyType>, 'keyGeneration'> 79 >, 80): CachedFn<KeyType, CachedContentType>; 81export function cached< 82 KeyType, 83 CachedContentType, 84 CacheKeyType extends string = string, 85>(opts: Opts<KeyType, CachedContentType, CacheKeyType>) { 86 const { 87 directives, 88 producer, 89 numItemsLimit, 90 collapseOverlappingRequestsTime, 91 onItemEviction: givenOnItemEviction, 92 keyGeneration = { 93 // NB: these casts are not safe as far as TS is concerned because 94 // CacheKeyType can be instantiated with an arbitrary subtype of string, 95 // and jsonStringify obv can't produce all of those. However, 96 // jsonStringify and jsonParse are only used if the caller doesn't provide 97 // their own keyGeneration functions, and, in that case, `CacheKeyType` is 98 // totally unobservable outside this function, so it doesn't actually 99 // matter if it's instantiated with a string subtype that isn't 100 // JsonOf<KeyType>. 101 toString: jsonStringify as unknown as (it: KeyType) => CacheKeyType, 102 fromString: jsonParse as unknown as (it: CacheKeyType) => KeyType, 103 }, 104 } = opts; 105 const finalOnItemEvication = givenOnItemEviction 106 ? ( 107 it: Entry<ReadonlyDeep<CachedContentType>, AnyValidators, AnyParams>, 108 ) => { 109 givenOnItemEviction( 110 it.content, 111 keyGeneration.fromString(it.id as CacheKeyType), 112 ); 113 } 114 : undefined; 115 116 const getWithCache = wrapProducer( 117 new Cache( 118 new MemoryStore({ numItemsLimit, onItemEviction: finalOnItemEvication }), 119 { 120 onGetAfterClose: 'return-nothing', 121 onStoreAfterClose: 'return-nothing', 122 }, 123 ), 124 { collapseOverlappingRequestsTime }, 125 async (req) => { 126 const origKey = keyGeneration.fromString( 127 req.id satisfies string as CacheKeyType, 128 ); 129 130 // Always cast the produced result to be marked readonly, because the same 131 // cached object is gonna be returned for multiple requests, so making any 132 // mutations to that object would be an insanely bad idea that'd be 133 // impossible to reason about. 134 const content = (await producer( 135 origKey, 136 )) satisfies CachedContentType as ReadonlyDeep<CachedContentType>; 137 138 return { 139 content, 140 directives: 141 typeof directives === 'function' ? directives(content) : directives, 142 }; 143 }, 144 ); 145 146 async function exposedGet(key: KeyType, directives?: ConsumerDirectives) { 147 const cacheKey = keyGeneration.toString(key); 148 return (await getWithCache({ id: cacheKey, directives })).content; 149 } 150 151 exposedGet.close = async () => getWithCache.cache.close(); 152 exposedGet.invalidate = async (key: KeyType) => { 153 const cacheKey = keyGeneration.toString(key); 154 await getWithCache.cache.delete(cacheKey); 155 }; 156 return exposedGet; 157} 158 159/** 160 * The type of functions returned by {@link cached}, which matches the type of 161 * the original producer, except that there's an added close() property/method. 162 */ 163export type Cached<OriginalProducer extends (key: never) => Promise<unknown>> = 164 OriginalProducer extends ( 165 key: infer KeyType extends ReadonlyDeep<JSON>, 166 ) => Promise<infer CacheContentType> 167 ? CachedFn<KeyType, CacheContentType> 168 : never;