import type { Cache, CacheTransaction, CacheEntryState, CachedEntityRevision, DefaultRegistry, ExpirationPolicy, } from './index.js'; import { DEFAULT_EXPIRATION } from './cache.js'; export class CacheTransactionImpl< CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown, Context extends object = object > implements CacheTransaction { #originalCacheReference: Cache< CacheKeyRegistry, Key, $Debug, UserExtensionData, Context >; #transactionalCache: Map; #cacheEntriesBeforeTransaction: Map; #cacheRevisionsBeforeTransaction: Map< Key, CachedEntityRevision[] >; #ttlPolicy: number; #lruPolicy: number; #userOptionExpirationPolicy: ExpirationPolicy; #localRevisionsMap: Map[]>; #cacheEntryState: Map>; context: Context; constructor( originalCache: Cache< CacheKeyRegistry, Key, $Debug, UserExtensionData, Context >, context: Context = {} as Context ) { this.#originalCacheReference = originalCache; this.#transactionalCache = new Map(); this.#cacheEntryState = new Map>(); this.#ttlPolicy = DEFAULT_EXPIRATION.ttl; this.#lruPolicy = DEFAULT_EXPIRATION.lru; this.#userOptionExpirationPolicy = this.#originalCacheReference.getCacheOptions()?.expiration || DEFAULT_EXPIRATION; this.context = context; if ( this.#userOptionExpirationPolicy && this.#userOptionExpirationPolicy?.lru && typeof this.#userOptionExpirationPolicy.lru === 'number' ) { this.#lruPolicy = this.#userOptionExpirationPolicy.lru; } if ( this.#userOptionExpirationPolicy && this.#userOptionExpirationPolicy?.ttl && typeof this.#userOptionExpirationPolicy.ttl === 'number' ) { this.#ttlPolicy = this.#userOptionExpirationPolicy.ttl; } this.#localRevisionsMap = new Map< Key, CachedEntityRevision[] >(); this.#cacheRevisionsBeforeTransaction = new Map< Key, CachedEntityRevision[] >(); this.#cacheEntriesBeforeTransaction = new Map(); } protected getExpirationPolicies() { return { userOptionExpirationPolicy: this.#userOptionExpirationPolicy, lruPolicy: this.#lruPolicy, ttlPolicy: this.#ttlPolicy, }; } protected setLocalRevisionsByEntry( cacheKey: Key, revision: CachedEntityRevision ) { if (this.#localRevisionsMap.has(cacheKey)) { this.#localRevisionsMap.get(cacheKey)?.push(revision); } else { this.#localRevisionsMap.set(cacheKey, [revision]); } } protected getLocalRevisions(): Map< Key, CachedEntityRevision[] > { return this.#localRevisionsMap; } protected setLocalRevisions( localRevisionsMap: Map[]> ): Map[]> { return (this.#localRevisionsMap = localRevisionsMap); } protected getLocalRevisionsByEntry( cacheKey: Key ): CachedEntityRevision[] | undefined { return this.#localRevisionsMap.get(cacheKey); } protected setRevisionsBeforeTransactionStart( revisions: Map[]> ) { this.#cacheRevisionsBeforeTransaction = revisions; } protected setCacheEntryState( cacheKey: Key, cacheEntryState: CacheEntryState ) { this.#cacheEntryState.set(cacheKey, cacheEntryState); } protected getCacheEntryState( cacheKey: Key ): CacheEntryState | undefined { return this.#cacheEntryState.get(cacheKey); } protected setCacheEntriesBeforeTransaction( entries: Map ) { this.#cacheEntriesBeforeTransaction = entries; } protected getTransactionalCache() { return this.#transactionalCache; } async *[Symbol.asyncIterator](): AsyncIterableIterator< [Key, CacheKeyRegistry[Key], CacheEntryState] > { for await (const [key, value] of this.localEntries()) { const state = this.#cacheEntryState.get( key ) as CacheEntryState; yield [key, value, state]; } } async get(cacheKey: Key): Promise { // will check the transaction entries and fall back to the cache if the transaction hasn't written to the key yet. let cachedValue; for await (const [key, value] of this.localEntries()) { if (key === cacheKey) { cachedValue = value; break; } } // Update cache entry state this.#cacheEntryState.set(cacheKey, { retained: { lru: true, ttl: this.#ttlPolicy }, lastAccessed: Date.now(), }); return cachedValue || (await this.#originalCacheReference.get(cacheKey)); } localEntries(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> { const localEntriesIterator = { async *[Symbol.asyncIterator]( localEntryMap: Map ): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> { for (const [key, value] of localEntryMap) { yield [key, value]; } }, }; return localEntriesIterator[Symbol.asyncIterator](this.#transactionalCache); } async entries(): Promise< AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> > { const entriesIterator = { async *[Symbol.asyncIterator]( localEntriesIterator: AsyncIterableIterator< [Key, CacheKeyRegistry[Key]] >, cacheEntriesBeforeTransaction: Map ): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> { for await (const [key, transactionValue] of localEntriesIterator) { yield [key, transactionValue]; const cacheValue = cacheEntriesBeforeTransaction.get(key); if (cacheValue) { yield [key, cacheValue]; } } }, }; return entriesIterator[Symbol.asyncIterator]( this.localEntries(), this.#cacheEntriesBeforeTransaction ); } localRevisions( cacheKey: Key ): AsyncIterableIterator> { const entryRevisionIterator = { async *[Symbol.asyncIterator]( revisions: CachedEntityRevision[] ): AsyncIterableIterator> { for (const revision of revisions) { yield revision; } }, }; const revisions = this.#localRevisionsMap.get(cacheKey) || []; return entryRevisionIterator[Symbol.asyncIterator](revisions); } entryRevisions( cacheKey: Key ): AsyncIterableIterator> { const entryRevisionIterator = { async *[Symbol.asyncIterator]( revisions: CachedEntityRevision[] ): AsyncIterableIterator> { for (const revision of revisions) { yield revision; } }, }; const entryRevisions = this.#cacheRevisionsBeforeTransaction.get(cacheKey) || []; const localRevisions = this.#localRevisionsMap.get(cacheKey) || []; return entryRevisionIterator[Symbol.asyncIterator]( entryRevisions.concat(localRevisions) ); } }