@data-eden/cache
Advanced tools
Comparing version 0.4.0 to 0.5.0
import { describe, it, expect } from 'vitest'; | ||
// TODO: add a tests tsconfig so we can import properly | ||
import { buildCache } from '@data-eden/cache'; | ||
import type { Cache, DefaultRegistry } from '@data-eden/cache'; | ||
import { expectTypeOf } from 'vitest'; | ||
@@ -9,2 +11,10 @@ // TODO: add tests for types | ||
describe('@data-eden/cache', function () { | ||
describe('user api', function () { | ||
it('returns the public interfaces', function () { | ||
expectTypeOf(buildCache()).toEqualTypeOf< | ||
Cache<DefaultRegistry, keyof DefaultRegistry> | ||
>(); | ||
}); | ||
}); | ||
describe('cache with no user registry', function () { | ||
@@ -11,0 +21,0 @@ it('can be built', async function () { |
@@ -1,240 +0,3 @@ | ||
type DefaultRegistry = Record<string, object>; | ||
export interface CacheDebugAPIs { | ||
size(): void; | ||
entries(): void; | ||
history(): void; | ||
} | ||
export interface CacheTransactionDebugAPIs { | ||
size(): void; | ||
entries(): void; | ||
} | ||
/** | ||
A 3-tuple of a cache entry that contains | ||
- *key* | ||
- *value* | ||
- *state* (optional) | ||
*/ | ||
type CacheEntry<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, UserExtensionData = unknown> = [ | ||
key: Key, | ||
value: CacheKeyRegistry[Key], | ||
state?: CacheEntryState<UserExtensionData> | ||
]; | ||
/** | ||
* A entry state (retention,last accessed) of each cache entry | ||
*/ | ||
export interface CacheEntryState<UserExtensionData = unknown> { | ||
retained: { | ||
lru: boolean; | ||
ttl: number; | ||
}; | ||
/** | ||
The last time this cache entry was accessed, either via `get`, `set`, or | ||
`merge`. | ||
Mainly useful for userland retention policies. | ||
*/ | ||
lastAccessed?: number; | ||
extensions?: UserExtensionData; | ||
} | ||
/** | ||
* LRU Cache | ||
*/ | ||
export interface LruCache<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry> { | ||
set(cacheKey: Key, value: CacheKeyRegistry[Key]): void; | ||
} | ||
type CacheKeyValue = Record<string, object | string | number> | string | number; | ||
export interface EntityMergeStrategy<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> { | ||
(cacheKey: Key, newEntityRevision: CachedEntityRevision<CacheKeyValue>, current: CacheKeyRegistry[Key] | undefined, tx: CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData>): CacheKeyValue; | ||
} | ||
export interface RevisionMergeStrategy<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> { | ||
(cacheKey: Key, tx: CommittingTransactionImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData>): void; | ||
} | ||
interface CachedEntityRevision<CacheKeyValue> { | ||
entity: CacheKeyValue; | ||
revision: number; | ||
revisionContext?: string; | ||
} | ||
type ExpirationPolicy = false | { | ||
lru: number; | ||
ttl: number; | ||
}; | ||
export interface CacheOptions<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> { | ||
hooks?: { | ||
/** | ||
An optional callback that is invoked just before a transaction is committed. | ||
This does not allow users to mutate the transaction, but it is a hook where | ||
custom retention policies can be implemented. | ||
The default retention policies are all implementable in userland as commit hooks. | ||
*/ | ||
commit?: (tx: CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData>) => void; | ||
/** | ||
An optional hook for merging new versions of an entity into the cache. This | ||
hook specifies the default behaviour for the cache -- a different merge | ||
strategy can be passed in per call to `LiveCacheTransaction.merge` | ||
The hook returns the updated merged entry -- it may not mutate any of its arguments. | ||
If unspecified, the default merge strategy is to deeply merge objects. | ||
*/ | ||
entitymergeStrategy?: EntityMergeStrategy<CacheKeyRegistry, Key, $Debug, UserExtensionData>; | ||
/** | ||
An optional hook for merging the list of revisions for a cache entry. | ||
If unspecified, the default retention strategy is to keep the full history | ||
of an entry as long as it's in the cache, evicting revisions only when the | ||
value itself is evicted. | ||
*/ | ||
revisionMergeStrategy?: RevisionMergeStrategy<CacheKeyRegistry, Key, $Debug, UserExtensionData>; | ||
}; | ||
expiration?: ExpirationPolicy; | ||
$debug?: $Debug; | ||
} | ||
export interface Cache<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> { | ||
/** | ||
Evict all entries from the cache. | ||
*/ | ||
clear(): Promise<void>; | ||
/** | ||
Restuns all cache options passed | ||
*/ | ||
getCacheOptions(): CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | undefined; | ||
/** | ||
Get Cache value based on cache key | ||
*/ | ||
get(cacheKey: Key): Promise<CacheKeyRegistry[Key] | undefined>; | ||
/** | ||
Calling `.save()` without a serializer will iterate over the cache entries | ||
and return an array of cache entry tuples. The values contained within the | ||
tuples are copied via `structuredClone`. | ||
If your cache entries are not structured clonable, (e.g. a function) | ||
`.save()` will throw an error. In this case, use the alternate form of | ||
`.save` passing in a `CacheEntrySerializer`. | ||
@see <https://developer.mozilla.org/en-US/docs/Web/API/structuredClone> | ||
*/ | ||
save(): Promise<[ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | undefined | ||
][]>; | ||
/** | ||
Calling `.load()` will add all entries passed to the cache. | ||
Note: `.load()` does not clear pre-existing entries, if you need to clear | ||
entries before loading call `.clear()`. | ||
*/ | ||
load(entries: CacheEntry<CacheKeyRegistry, Key, UserExtensionData>[]): Promise<void>; | ||
[Symbol.asyncIterator](): AsyncIterableIterator<[ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData>? | ||
]>; | ||
/** | ||
Generator function that yields each of the cache entries. Note that this | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
entries(): AsyncIterableIterator<[ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | ||
]>; | ||
/** | ||
Generator function that yields each of the cache entry revision | ||
*/ | ||
entryRevisions(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
/** | ||
Generator function that yields each of the cache entry keys | ||
*/ | ||
keys(): AsyncIterableIterator<Key>; | ||
/** | ||
Generator function that yields each of the cache entry values | ||
*/ | ||
values(): AsyncIterableIterator<CacheKeyRegistry[Key]>; | ||
/** | ||
Creates a live transaction instance | ||
*/ | ||
beginTransaction(): Promise<LiveCacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData>>; | ||
} | ||
export interface CacheTransaction<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> { | ||
/** | ||
Get the value of `cacheKey` in the cache. If `key` has been modified in this | ||
transaction (e.g. via `merge` or `set`), `tx.get` will return the updated | ||
entry in this transaction. The return value can therefore differ from | ||
`cache.get`. | ||
*/ | ||
get(cacheKey: Key): CacheKeyRegistry[Key] | CacheKeyValue | undefined; | ||
[Symbol.asyncIterator](entryMap: Map<Key, CacheKeyRegistry[Key]>): AsyncIterableIterator<[ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | ||
]>; | ||
/** | ||
Generator function that yields each of the transaction entries including local entries and entries before transaction began. | ||
*/ | ||
entries(): AsyncIterableIterator<[ | ||
Key, | ||
CacheKeyRegistry[Key] | CacheKeyValue, | ||
CacheEntryState<UserExtensionData> | ||
]>; | ||
/** | ||
Generator function that yields each of the transaction local entries. | ||
*/ | ||
localEntries(): AsyncIterableIterator<[ | ||
Key, | ||
CacheKeyRegistry[Key] | CacheKeyValue, | ||
CacheEntryState<UserExtensionData> | ||
]>; | ||
/** | ||
An async generator that produces the revisions of `key` within this transaction. | ||
*/ | ||
localRevisions(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
/** | ||
An async generator that produces the complete list of revisions for `key`, | ||
from the time the transaction began and including the revisions added in this | ||
transaction. | ||
*/ | ||
entryRevisions(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
$debug?: $Debug & CacheTransactionDebugAPIs; | ||
} | ||
/** | ||
* Interface specifc to handle Live transaction | ||
*/ | ||
export interface LiveCacheTransaction<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> extends CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> { | ||
/** | ||
* Merges cache entries based on merge strategy | ||
*/ | ||
merge(cacheKey: Key, value: CachedEntityRevision<CacheKeyValue>, options?: { | ||
$debug: $Debug; | ||
}): Promise<CacheKeyRegistry[Key] | CacheKeyValue>; | ||
/** | ||
* sets cache values within the transaction | ||
*/ | ||
set(cacheKey: Key, value: CacheKeyRegistry[Key] | CacheKeyValue): CacheKeyRegistry[Key] | CacheKeyValue; | ||
/** | ||
* Deletes an entry from live transction | ||
*/ | ||
delete(cacheKey: Key): Promise<boolean>; | ||
/** | ||
* Commits live transction entries. | ||
*/ | ||
commit(): Promise<void>; | ||
} | ||
export interface CommittingTransaction<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> extends Omit<CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData>, 'get' | 'entries' | 'localEntries' | 'localRevisions' | 'entryRevisions'> { | ||
cache: { | ||
clearRevisions(tx: CommittingTransactionImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData>, id: Key): void; | ||
appendRevisions(tx: CommittingTransactionImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData>, id: Key, revisions: CachedEntityRevision<CacheKeyValue>[]): void; | ||
}; | ||
} | ||
declare class CommittingTransactionImpl<CacheKeyRegistry extends DefaultRegistry, Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown> implements CommittingTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> { | ||
#private; | ||
$debug?: ($Debug & CacheTransactionDebugAPIs) | undefined; | ||
cache: { | ||
clearRevisions(tx: CommittingTransactionImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData>, id: Key): void; | ||
appendRevisions(tx: CommittingTransactionImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData>, id: Key, revisions: CachedEntityRevision<CacheKeyValue>[]): void; | ||
}; | ||
constructor(); | ||
[Symbol.asyncIterator](entryMap: Map<Key, CacheKeyRegistry[Key]>): AsyncIterableIterator<[ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | ||
]>; | ||
mergedRevisions(): Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
} | ||
import type { Cache, CacheOptions, DefaultRegistry } from './index.js'; | ||
export declare function buildCache<CacheKeyRegistry extends DefaultRegistry = DefaultRegistry, Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, $Debug = unknown, UserExtensionData = unknown>(options?: CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData>): Cache<CacheKeyRegistry, Key, $Debug, UserExtensionData>; | ||
export {}; | ||
//# sourceMappingURL=cache.d.ts.map |
@@ -12,65 +12,161 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
}; | ||
var _LruCacheImpl_max, _LruCacheImpl_lruCache, _CommittingTransactionImpl_mergedRevisions, _LiveCacheTransactionImpl_originalCacheReference, _LiveCacheTransactionImpl_transactionalCache, _LiveCacheTransactionImpl_localUpdatedEntries, _LiveCacheTransactionImpl_commitingTransaction, _LiveCacheTransactionImpl_cacheEntryState, _LiveCacheTransactionImpl_userOptionRetentionPolicy, _LiveCacheTransactionImpl_ttlPolicy, _LiveCacheTransactionImpl_lruPolicy, _LiveCacheTransactionImpl_localRevisions, _LiveCacheTransactionImpl_entryRevisions, _CacheImpl_weakCache, _CacheImpl_entryRevisions, _CacheImpl_cacheOptions, _CacheImpl_cacheEntryState, _CacheImpl_lruCache, _CacheImpl_lruPolicy; | ||
// eslint-disable-next-line | ||
function structuredClone(x) { | ||
try { | ||
return JSON.parse(JSON.stringify(x)); | ||
var _CacheImpl_weakCache, _CacheImpl_entryRevisions, _CacheImpl_cacheOptions, _CacheImpl_cacheEntryState, _CacheImpl_lruCache, _CacheImpl_lruPolicy, _LiveCacheTransactionImpl_originalCacheReference, _LiveCacheTransactionImpl_transactionalCache, _LiveCacheTransactionImpl_localUpdatedEntries, _LiveCacheTransactionImpl_commitingTransaction, _LiveCacheTransactionImpl_cacheEntryState, _LiveCacheTransactionImpl_userOptionRetentionPolicy, _LiveCacheTransactionImpl_ttlPolicy, _LiveCacheTransactionImpl_lruPolicy, _LiveCacheTransactionImpl_localRevisions, _LiveCacheTransactionImpl_entryRevisions, _CommittingTransactionImpl_mergedRevisions, _LruCacheImpl_max, _LruCacheImpl_lruCache; | ||
class CacheImpl { | ||
constructor(options) { | ||
_CacheImpl_weakCache.set(this, void 0); | ||
_CacheImpl_entryRevisions.set(this, void 0); | ||
_CacheImpl_cacheOptions.set(this, void 0); | ||
_CacheImpl_cacheEntryState.set(this, void 0); | ||
_CacheImpl_lruCache.set(this, void 0); | ||
_CacheImpl_lruPolicy.set(this, void 0); | ||
__classPrivateFieldSet(this, _CacheImpl_weakCache, new Map(), "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_cacheOptions, options, "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_lruPolicy, DEFAULT_EXPIRATION.lru, "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_entryRevisions, new Map(), "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_cacheEntryState, new Map(), "f"); | ||
const expiration = __classPrivateFieldGet(this, _CacheImpl_cacheOptions, "f")?.expiration || DEFAULT_EXPIRATION; | ||
if (expiration && expiration?.lru && typeof expiration.lru === 'number') { | ||
__classPrivateFieldSet(this, _CacheImpl_lruPolicy, expiration.lru, "f"); | ||
} | ||
__classPrivateFieldSet(this, _CacheImpl_lruCache, new LruCacheImpl(__classPrivateFieldGet(this, _CacheImpl_lruPolicy, "f")), "f"); | ||
} | ||
catch (error) { | ||
throw new Error('The cache value is not structured clonable use `save` with serializer'); | ||
/** | ||
Evict all entries from the cache. | ||
*/ | ||
async clear() { | ||
for await (const [key] of this.entries()) { | ||
__classPrivateFieldGet(this, _CacheImpl_weakCache, "f").delete(key); | ||
__classPrivateFieldGet(this, _CacheImpl_lruCache, "f").getCache().delete(key); | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").delete(key); | ||
} | ||
} | ||
} | ||
const DEFAULT_EXPIRATION = { lru: 1000, ttl: 60000 }; | ||
const DEFAULT_ENTRY_STATE = { | ||
retained: { lru: false, ttl: DEFAULT_EXPIRATION.ttl }, | ||
}; | ||
class LruCacheImpl { | ||
constructor(maxCapacity) { | ||
_LruCacheImpl_max.set(this, void 0); | ||
_LruCacheImpl_lruCache.set(this, void 0); | ||
__classPrivateFieldSet(this, _LruCacheImpl_max, maxCapacity, "f"); | ||
__classPrivateFieldSet(this, _LruCacheImpl_lruCache, new Map(), "f"); | ||
getCacheOptions() { | ||
return __classPrivateFieldGet(this, _CacheImpl_cacheOptions, "f"); | ||
} | ||
set(cacheKey, value) { | ||
// refresh data | ||
if (__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").has(cacheKey)) { | ||
__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").delete(cacheKey); | ||
async get(cacheKey) { | ||
let ref = __classPrivateFieldGet(this, _CacheImpl_weakCache, "f").get(cacheKey); | ||
return ref?.deref(); | ||
} | ||
/** | ||
Calling `.save()` without a serializer will iterate over the cache entries | ||
and return an array of cache entry tuples. | ||
*/ | ||
async save() { | ||
const arrayOfCacheEntryTuples = []; | ||
for await (const [key, value, state] of this.entries()) { | ||
// TODO create state? | ||
const structuredClonedValue = structuredClone(value); | ||
arrayOfCacheEntryTuples.push([key, structuredClonedValue, state]); | ||
} | ||
else if (__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").size === __classPrivateFieldGet(this, _LruCacheImpl_max, "f")) { | ||
// find and evict the LRU entry | ||
const lruEntryKey = __classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").keys().next().value; | ||
__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").delete(lruEntryKey); | ||
return arrayOfCacheEntryTuples; | ||
} | ||
/** | ||
Calling `.load()` will add all entries passed to the cache. | ||
Note: `.load()` does not clear pre-existing entries, if you need to clear | ||
entries before loading call `.clear()`. | ||
*/ | ||
async load(entries) { | ||
let revisionCounter = 0; | ||
for await (let entry of entries) { | ||
let [key, value, state] = entry; | ||
// TODO: finalizregistry | ||
let clone = structuredClone(value); | ||
__classPrivateFieldGet(this, _CacheImpl_weakCache, "f").set(key, new WeakRef(clone)); | ||
__classPrivateFieldGet(this, _CacheImpl_lruCache, "f").set(key, clone); | ||
__classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").set(key, state); | ||
const entityRevision = { | ||
entity: value, | ||
revision: ++revisionCounter, | ||
}; | ||
if (__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").has(key)) { | ||
const revisions = __classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").get(key)?.concat(entityRevision) || []; | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(key, revisions); | ||
} | ||
else { | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(key, [entityRevision]); | ||
} | ||
} | ||
__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").set(cacheKey, value); | ||
} | ||
getCache() { | ||
return __classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f"); | ||
async commitTransaction(entries, entryRevisions) { | ||
const sortEntries = entries.sort(([, , state], [, , state1]) => state?.lastAccessed && | ||
state1?.lastAccessed && | ||
state?.lastAccessed < state1?.lastAccessed | ||
? 1 | ||
: -1); | ||
for await (let entry of sortEntries) { | ||
let [key, value, state] = entry; | ||
// TODO: finalizregistry | ||
__classPrivateFieldGet(this, _CacheImpl_weakCache, "f").set(key, new WeakRef(value)); | ||
__classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").set(key, state); | ||
if (state?.retained.lru) { | ||
__classPrivateFieldGet(this, _CacheImpl_lruCache, "f").set(key, value); | ||
} | ||
} | ||
for await (const [cacheKey, revision] of entryRevisions) { | ||
if (__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").has(cacheKey)) { | ||
const revisions = __classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").get(cacheKey)?.concat(revision) || []; | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(cacheKey, revisions); | ||
} | ||
else { | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(cacheKey, revision); | ||
} | ||
} | ||
} | ||
} | ||
_LruCacheImpl_max = new WeakMap(), _LruCacheImpl_lruCache = new WeakMap(); | ||
class CommittingTransactionImpl { | ||
constructor() { | ||
_CommittingTransactionImpl_mergedRevisions.set(this, void 0); | ||
this.cache = { | ||
clearRevisions(tx, id) { | ||
__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").delete(id); | ||
}, | ||
appendRevisions(tx, id, revisions) { | ||
if (__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").has(id)) { | ||
const appendedRevisions = __classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").get(id)?.concat(revisions) || []; | ||
__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").set(id, appendedRevisions); | ||
/** | ||
Generator function for async iterable that yields iterable cache entries. This | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
async *[(_CacheImpl_weakCache = new WeakMap(), _CacheImpl_entryRevisions = new WeakMap(), _CacheImpl_cacheOptions = new WeakMap(), _CacheImpl_cacheEntryState = new WeakMap(), _CacheImpl_lruCache = new WeakMap(), _CacheImpl_lruPolicy = new WeakMap(), Symbol.asyncIterator)]() { | ||
// yield weekly held values | ||
for await (const [key] of __classPrivateFieldGet(this, _CacheImpl_weakCache, "f")) { | ||
const valueRef = __classPrivateFieldGet(this, _CacheImpl_weakCache, "f").get(key)?.deref(); | ||
// Because of the limited guarantees of `FinalizationRegistry`, when yielding | ||
// weakly-held values to the user in `entries` we have to check that the | ||
// value is actually present, | ||
if (!valueRef) { | ||
throw new Error('ref is undefined'); | ||
} | ||
const state = __classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
} | ||
/** | ||
Generator function that yields each of the iterable cache entries. Note that this | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
entries() { | ||
return this[Symbol.asyncIterator](); | ||
} | ||
entryRevisions(cacheKey) { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator](revisions) { | ||
for (const revision of revisions) { | ||
yield revision; | ||
} | ||
else { | ||
__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").set(id, revisions); | ||
} | ||
}, | ||
}; | ||
__classPrivateFieldSet(this, _CommittingTransactionImpl_mergedRevisions, new Map(), "f"); | ||
const revisions = __classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").get(cacheKey) || []; | ||
return entryRevisionIterator[Symbol.asyncIterator](revisions); | ||
} | ||
[(_CommittingTransactionImpl_mergedRevisions = new WeakMap(), Symbol.asyncIterator)](entryMap) { | ||
throw new Error('Method not implemented.'); | ||
/** | ||
* Generator function that yields each of the iterable cache entry Keys. | ||
*/ | ||
async *keys() { | ||
for await (const [key] of this.entries()) { | ||
yield key; | ||
} | ||
} | ||
mergedRevisions() { | ||
return __classPrivateFieldGet(this, _CommittingTransactionImpl_mergedRevisions, "f"); | ||
/** | ||
* Generator function that yields each of the iterable cache entry Values. | ||
*/ | ||
async *values() { | ||
for await (const [, value] of this.entries()) { | ||
yield value; | ||
} | ||
} | ||
async beginTransaction() { | ||
return await LiveCacheTransactionImpl.beginLiveTransaction(this); | ||
} | ||
} | ||
@@ -275,160 +371,27 @@ class LiveCacheTransactionImpl { | ||
} | ||
class CacheImpl { | ||
constructor(options) { | ||
_CacheImpl_weakCache.set(this, void 0); | ||
_CacheImpl_entryRevisions.set(this, void 0); | ||
_CacheImpl_cacheOptions.set(this, void 0); | ||
_CacheImpl_cacheEntryState.set(this, void 0); | ||
_CacheImpl_lruCache.set(this, void 0); | ||
_CacheImpl_lruPolicy.set(this, void 0); | ||
__classPrivateFieldSet(this, _CacheImpl_weakCache, new Map(), "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_cacheOptions, options, "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_lruPolicy, DEFAULT_EXPIRATION.lru, "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_entryRevisions, new Map(), "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_cacheEntryState, new Map(), "f"); | ||
const expiration = __classPrivateFieldGet(this, _CacheImpl_cacheOptions, "f")?.expiration || DEFAULT_EXPIRATION; | ||
if (expiration && expiration?.lru && typeof expiration.lru === 'number') { | ||
__classPrivateFieldSet(this, _CacheImpl_lruPolicy, expiration.lru, "f"); | ||
} | ||
__classPrivateFieldSet(this, _CacheImpl_lruCache, new LruCacheImpl(__classPrivateFieldGet(this, _CacheImpl_lruPolicy, "f")), "f"); | ||
} | ||
/** | ||
Evict all entries from the cache. | ||
*/ | ||
async clear() { | ||
for await (const [key] of this.entries()) { | ||
__classPrivateFieldGet(this, _CacheImpl_weakCache, "f").delete(key); | ||
__classPrivateFieldGet(this, _CacheImpl_lruCache, "f").getCache().delete(key); | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").delete(key); | ||
} | ||
} | ||
getCacheOptions() { | ||
return __classPrivateFieldGet(this, _CacheImpl_cacheOptions, "f"); | ||
} | ||
async get(cacheKey) { | ||
let ref = __classPrivateFieldGet(this, _CacheImpl_weakCache, "f").get(cacheKey); | ||
return ref?.deref(); | ||
} | ||
/** | ||
Calling `.save()` without a serializer will iterate over the cache entries | ||
and return an array of cache entry tuples. | ||
*/ | ||
async save() { | ||
const arrayOfCacheEntryTuples = []; | ||
for await (const [key, value, state] of this.entries()) { | ||
// TODO create state? | ||
const structuredClonedValue = structuredClone(value); | ||
arrayOfCacheEntryTuples.push([key, structuredClonedValue, state]); | ||
} | ||
return arrayOfCacheEntryTuples; | ||
} | ||
/** | ||
Calling `.load()` will add all entries passed to the cache. | ||
Note: `.load()` does not clear pre-existing entries, if you need to clear | ||
entries before loading call `.clear()`. | ||
*/ | ||
async load(entries) { | ||
let revisionCounter = 0; | ||
for await (let entry of entries) { | ||
let [key, value, state] = entry; | ||
// TODO: finalizregistry | ||
let clone = structuredClone(value); | ||
__classPrivateFieldGet(this, _CacheImpl_weakCache, "f").set(key, new WeakRef(clone)); | ||
__classPrivateFieldGet(this, _CacheImpl_lruCache, "f").set(key, clone); | ||
__classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").set(key, state); | ||
const entityRevision = { | ||
entity: value, | ||
revision: ++revisionCounter, | ||
}; | ||
if (__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").has(key)) { | ||
const revisions = __classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").get(key)?.concat(entityRevision) || []; | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(key, revisions); | ||
} | ||
else { | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(key, [entityRevision]); | ||
} | ||
} | ||
} | ||
async commitTransaction(entries, entryRevisions) { | ||
const sortEntries = entries.sort(([, , state], [, , state1]) => state?.lastAccessed && | ||
state1?.lastAccessed && | ||
state?.lastAccessed < state1?.lastAccessed | ||
? 1 | ||
: -1); | ||
for await (let entry of sortEntries) { | ||
let [key, value, state] = entry; | ||
// TODO: finalizregistry | ||
__classPrivateFieldGet(this, _CacheImpl_weakCache, "f").set(key, new WeakRef(value)); | ||
__classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").set(key, state); | ||
if (state?.retained.lru) { | ||
__classPrivateFieldGet(this, _CacheImpl_lruCache, "f").set(key, value); | ||
} | ||
} | ||
for await (const [cacheKey, revision] of entryRevisions) { | ||
if (__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").has(cacheKey)) { | ||
const revisions = __classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").get(cacheKey)?.concat(revision) || []; | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(cacheKey, revisions); | ||
} | ||
else { | ||
__classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").set(cacheKey, revision); | ||
} | ||
} | ||
} | ||
/** | ||
Generator function for async iterable that yields iterable cache entries. This | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
async *[(_CacheImpl_weakCache = new WeakMap(), _CacheImpl_entryRevisions = new WeakMap(), _CacheImpl_cacheOptions = new WeakMap(), _CacheImpl_cacheEntryState = new WeakMap(), _CacheImpl_lruCache = new WeakMap(), _CacheImpl_lruPolicy = new WeakMap(), Symbol.asyncIterator)]() { | ||
// yield weekly held values | ||
for await (const [key] of __classPrivateFieldGet(this, _CacheImpl_weakCache, "f")) { | ||
const valueRef = __classPrivateFieldGet(this, _CacheImpl_weakCache, "f").get(key)?.deref(); | ||
// Because of the limited guarantees of `FinalizationRegistry`, when yielding | ||
// weakly-held values to the user in `entries` we have to check that the | ||
// value is actually present, | ||
if (!valueRef) { | ||
throw new Error('ref is undefined'); | ||
} | ||
const state = __classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
} | ||
/** | ||
Generator function that yields each of the iterable cache entries. Note that this | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
entries() { | ||
return this[Symbol.asyncIterator](); | ||
} | ||
entryRevisions(cacheKey) { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator](revisions) { | ||
for (const revision of revisions) { | ||
yield revision; | ||
class CommittingTransactionImpl { | ||
constructor() { | ||
_CommittingTransactionImpl_mergedRevisions.set(this, void 0); | ||
this.cache = { | ||
clearRevisions(tx, id) { | ||
__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").delete(id); | ||
}, | ||
appendRevisions(tx, id, revisions) { | ||
if (__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").has(id)) { | ||
const appendedRevisions = __classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").get(id)?.concat(revisions) || []; | ||
__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").set(id, appendedRevisions); | ||
} | ||
else { | ||
__classPrivateFieldGet(tx, _CommittingTransactionImpl_mergedRevisions, "f").set(id, revisions); | ||
} | ||
}, | ||
}; | ||
const revisions = __classPrivateFieldGet(this, _CacheImpl_entryRevisions, "f").get(cacheKey) || []; | ||
return entryRevisionIterator[Symbol.asyncIterator](revisions); | ||
__classPrivateFieldSet(this, _CommittingTransactionImpl_mergedRevisions, new Map(), "f"); | ||
} | ||
/** | ||
* Generator function that yields each of the iterable cache entry Keys. | ||
*/ | ||
async *keys() { | ||
for await (const [key] of this.entries()) { | ||
yield key; | ||
} | ||
[(_CommittingTransactionImpl_mergedRevisions = new WeakMap(), Symbol.asyncIterator)](entryMap) { | ||
throw new Error('Method not implemented.'); | ||
} | ||
/** | ||
* Generator function that yields each of the iterable cache entry Values. | ||
*/ | ||
async *values() { | ||
for await (const [, value] of this.entries()) { | ||
yield value; | ||
} | ||
mergedRevisions() { | ||
return __classPrivateFieldGet(this, _CommittingTransactionImpl_mergedRevisions, "f"); | ||
} | ||
async beginTransaction() { | ||
return await LiveCacheTransactionImpl.beginLiveTransaction(this); | ||
} | ||
} | ||
@@ -438,2 +401,30 @@ export function buildCache(options) { | ||
} | ||
class LruCacheImpl { | ||
constructor(maxCapacity) { | ||
_LruCacheImpl_max.set(this, void 0); | ||
_LruCacheImpl_lruCache.set(this, void 0); | ||
__classPrivateFieldSet(this, _LruCacheImpl_max, maxCapacity, "f"); | ||
__classPrivateFieldSet(this, _LruCacheImpl_lruCache, new Map(), "f"); | ||
} | ||
set(cacheKey, value) { | ||
// refresh data | ||
if (__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").has(cacheKey)) { | ||
__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").delete(cacheKey); | ||
} | ||
else if (__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").size === __classPrivateFieldGet(this, _LruCacheImpl_max, "f")) { | ||
// find and evict the LRU entry | ||
const lruEntryKey = __classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").keys().next().value; | ||
__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").delete(lruEntryKey); | ||
} | ||
__classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f").set(cacheKey, value); | ||
} | ||
getCache() { | ||
return __classPrivateFieldGet(this, _LruCacheImpl_lruCache, "f"); | ||
} | ||
} | ||
_LruCacheImpl_max = new WeakMap(), _LruCacheImpl_lruCache = new WeakMap(); | ||
const DEFAULT_EXPIRATION = { lru: 10000, ttl: 60000 }; | ||
const DEFAULT_ENTRY_STATE = { | ||
retained: { lru: false, ttl: DEFAULT_EXPIRATION.ttl }, | ||
}; | ||
const defaultMergeStrategy = function deepMergeStratey(id, { entity, revision }, current, tx) { | ||
@@ -480,2 +471,11 @@ return deepMerge(current, entity); | ||
} | ||
// eslint-disable-next-line | ||
function structuredClone(x) { | ||
try { | ||
return JSON.parse(JSON.stringify(x)); | ||
} | ||
catch (error) { | ||
throw new Error('The cache value is not structured clonable use `save` with serializer'); | ||
} | ||
} | ||
//# sourceMappingURL=cache.js.map |
export { buildCache } from './cache.js'; | ||
export type { Cache, CacheTransaction, LiveCacheTransaction, CommittingTransaction, CacheEntry, CacheEntryState, CacheKeyValue, EntityMergeStrategy, RevisionMergeStrategy, CachedEntityRevision, ExpirationPolicy, CacheOptions, DefaultRegistry, CacheDebugAPIs, LruCache, CacheTransactionDebugAPIs, } from '../types/api.js'; | ||
//# sourceMappingURL=index.d.ts.map |
{ | ||
"$schema": "https://json.schemastore.org/package.json", | ||
"name": "@data-eden/cache", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"repository": { | ||
@@ -24,6 +24,9 @@ "type": "git", | ||
"scripts": { | ||
"build": "tsc --build", | ||
"test": "vitest run", | ||
"test:debug": "node --inspect-brk --inspect ../../node_modules/.bin/vitest --threads=false" | ||
}, | ||
"dependencies": {}, | ||
"devDependencies": { | ||
"ts-expect": "^1.3.0" | ||
}, | ||
"volta": { | ||
@@ -30,0 +33,0 @@ "extends": "../../package.json" |
952
src/cache.ts
@@ -1,228 +0,84 @@ | ||
// eslint-disable-next-line | ||
function structuredClone(x: any): any { | ||
try { | ||
return JSON.parse(JSON.stringify(x)); | ||
} catch (error) { | ||
throw new Error( | ||
'The cache value is not structured clonable use `save` with serializer' | ||
); | ||
} | ||
} | ||
import type { | ||
Cache, | ||
CacheTransaction, | ||
LiveCacheTransaction, | ||
CommittingTransaction, | ||
CacheEntry, | ||
CacheEntryState, | ||
CacheKeyValue, | ||
CachedEntityRevision, | ||
ExpirationPolicy, | ||
CacheOptions, | ||
DefaultRegistry, | ||
LruCache, | ||
CacheTransactionDebugAPIs, | ||
} from './index.js'; | ||
const DEFAULT_EXPIRATION = { lru: 1000, ttl: 60000 }; | ||
const DEFAULT_ENTRY_STATE = { | ||
retained: { lru: false, ttl: DEFAULT_EXPIRATION.ttl }, | ||
}; | ||
type DefaultRegistry = Record<string, object>; | ||
export interface CacheDebugAPIs { | ||
size(): void; | ||
entries(): void; | ||
history(): void; | ||
} | ||
export interface CacheTransactionDebugAPIs { | ||
size(): void; | ||
entries(): void; | ||
} | ||
/** | ||
A 3-tuple of a cache entry that contains | ||
- *key* | ||
- *value* | ||
- *state* (optional) | ||
*/ | ||
type CacheEntry< | ||
class CacheImpl< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> = [ | ||
key: Key, | ||
value: CacheKeyRegistry[Key], | ||
state?: CacheEntryState<UserExtensionData> | ||
]; | ||
/** | ||
* A entry state (retention,last accessed) of each cache entry | ||
*/ | ||
export interface CacheEntryState<UserExtensionData = unknown> { | ||
retained: { | ||
lru: boolean; | ||
ttl: number; | ||
}; | ||
/** | ||
The last time this cache entry was accessed, either via `get`, `set`, or | ||
`merge`. | ||
Mainly useful for userland retention policies. | ||
*/ | ||
lastAccessed?: number; // timestamp | ||
extensions?: UserExtensionData; | ||
} | ||
/** | ||
* LRU Cache | ||
*/ | ||
export interface LruCache< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry | ||
> { | ||
set(cacheKey: Key, value: CacheKeyRegistry[Key]): void; | ||
} | ||
class LruCacheImpl< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry | ||
> implements LruCache<CacheKeyRegistry, Key> | ||
> implements Cache<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
{ | ||
#max: number; | ||
#lruCache: Map<Key, CacheKeyRegistry[Key]>; | ||
#weakCache: Map<Key, WeakRef<CacheKeyRegistry[Key]>>; | ||
#entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
#cacheOptions: | ||
| CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
| undefined; | ||
#cacheEntryState: Map<Key, CacheEntryState<UserExtensionData> | undefined>; | ||
#lruCache: LruCacheImpl<CacheKeyRegistry, Key>; | ||
#lruPolicy: number; | ||
constructor(maxCapacity: number) { | ||
this.#max = maxCapacity; | ||
this.#lruCache = new Map<Key, CacheKeyRegistry[Key]>(); | ||
} | ||
constructor( | ||
options: | ||
| CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
| undefined | ||
) { | ||
this.#weakCache = new Map<Key, WeakRef<CacheKeyRegistry[Key]>>(); | ||
this.#cacheOptions = options; | ||
this.#lruPolicy = DEFAULT_EXPIRATION.lru; | ||
this.#entryRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
>(); | ||
this.#cacheEntryState = new Map< | ||
Key, | ||
CacheEntryState<UserExtensionData> | undefined | ||
>(); | ||
set(cacheKey: Key, value: CacheKeyRegistry[Key]) { | ||
// refresh data | ||
if (this.#lruCache.has(cacheKey)) { | ||
this.#lruCache.delete(cacheKey); | ||
} else if (this.#lruCache.size === this.#max) { | ||
// find and evict the LRU entry | ||
const lruEntryKey = this.#lruCache.keys().next().value as Key; | ||
this.#lruCache.delete(lruEntryKey); | ||
const expiration = this.#cacheOptions?.expiration || DEFAULT_EXPIRATION; | ||
if (expiration && expiration?.lru && typeof expiration.lru === 'number') { | ||
this.#lruPolicy = expiration.lru; | ||
} | ||
this.#lruCache.set(cacheKey, value); | ||
this.#lruCache = new LruCacheImpl<CacheKeyRegistry, Key>(this.#lruPolicy); | ||
} | ||
getCache(): Map<Key, CacheKeyRegistry[Key]> { | ||
return this.#lruCache; | ||
} | ||
} | ||
type CacheKeyValue = Record<string, object | string | number> | string | number; | ||
export interface EntityMergeStrategy< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
( | ||
cacheKey: Key, | ||
newEntityRevision: CachedEntityRevision<CacheKeyValue>, | ||
current: CacheKeyRegistry[Key] | undefined, | ||
tx: CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
): CacheKeyValue; | ||
} | ||
export interface RevisionMergeStrategy< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
( | ||
cacheKey: Key, | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
): void; | ||
} | ||
interface CachedEntityRevision<CacheKeyValue> { | ||
entity: CacheKeyValue; | ||
revision: number; | ||
revisionContext?: string; // Use to store queryIds that can be used for debugging | ||
} | ||
type ExpirationPolicy = | ||
| false | ||
| { | ||
lru: number; | ||
ttl: number; | ||
}; | ||
export interface CacheOptions< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
hooks?: { | ||
/** | ||
An optional callback that is invoked just before a transaction is committed. | ||
This does not allow users to mutate the transaction, but it is a hook where | ||
custom retention policies can be implemented. | ||
The default retention policies are all implementable in userland as commit hooks. | ||
*/ | ||
commit?: ( | ||
tx: CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
) => void; | ||
/** | ||
An optional hook for merging new versions of an entity into the cache. This | ||
hook specifies the default behaviour for the cache -- a different merge | ||
strategy can be passed in per call to `LiveCacheTransaction.merge` | ||
The hook returns the updated merged entry -- it may not mutate any of its arguments. | ||
If unspecified, the default merge strategy is to deeply merge objects. | ||
*/ | ||
entitymergeStrategy?: EntityMergeStrategy< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
/** | ||
An optional hook for merging the list of revisions for a cache entry. | ||
If unspecified, the default retention strategy is to keep the full history | ||
of an entry as long as it's in the cache, evicting revisions only when the | ||
value itself is evicted. | ||
*/ | ||
revisionMergeStrategy?: RevisionMergeStrategy< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
}; | ||
expiration?: ExpirationPolicy; | ||
$debug?: $Debug; | ||
} | ||
export interface Cache< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
/** | ||
Evict all entries from the cache. | ||
*/ | ||
clear(): Promise<void>; | ||
async clear(): Promise<void> { | ||
for await (const [key] of this.entries()) { | ||
this.#weakCache.delete(key); | ||
this.#lruCache.getCache().delete(key); | ||
this.#entryRevisions.delete(key); | ||
} | ||
} | ||
/** | ||
Restuns all cache options passed | ||
*/ | ||
getCacheOptions(): | ||
| CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
| undefined; | ||
| undefined { | ||
return this.#cacheOptions; | ||
} | ||
/** | ||
Get Cache value based on cache key | ||
*/ | ||
get(cacheKey: Key): Promise<CacheKeyRegistry[Key] | undefined>; | ||
async get(cacheKey: Key): Promise<CacheKeyRegistry[Key] | undefined> { | ||
let ref = this.#weakCache.get(cacheKey); | ||
return ref?.deref(); | ||
} | ||
/** | ||
Calling `.save()` without a serializer will iterate over the cache entries | ||
and return an array of cache entry tuples. The values contained within the | ||
tuples are copied via `structuredClone`. | ||
If your cache entries are not structured clonable, (e.g. a function) | ||
`.save()` will throw an error. In this case, use the alternate form of | ||
`.save` passing in a `CacheEntrySerializer`. | ||
@see <https://developer.mozilla.org/en-US/docs/Web/API/structuredClone> | ||
and return an array of cache entry tuples. | ||
*/ | ||
save(): Promise< | ||
async save(): Promise< | ||
[ | ||
@@ -233,3 +89,17 @@ Key, | ||
][] | ||
>; | ||
> { | ||
const arrayOfCacheEntryTuples: [ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | undefined | ||
][] = []; | ||
for await (const [key, value, state] of this.entries()) { | ||
// TODO create state? | ||
const structuredClonedValue = structuredClone( | ||
value | ||
) as CacheKeyRegistry[Key]; | ||
arrayOfCacheEntryTuples.push([key, structuredClonedValue, state]); | ||
} | ||
return arrayOfCacheEntryTuples; | ||
} | ||
@@ -241,258 +111,142 @@ /** | ||
*/ | ||
load( | ||
async load( | ||
entries: CacheEntry<CacheKeyRegistry, Key, UserExtensionData>[] | ||
): Promise<void>; | ||
): Promise<void> { | ||
let revisionCounter = 0; | ||
for await (let entry of entries) { | ||
let [key, value, state] = entry; | ||
[Symbol.asyncIterator](): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>?] | ||
>; | ||
// TODO: finalizregistry | ||
let clone = structuredClone(value) as CacheKeyRegistry[Key]; | ||
this.#weakCache.set(key, new WeakRef(clone)); | ||
/** | ||
Generator function that yields each of the cache entries. Note that this | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
entries(): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
>; | ||
this.#lruCache.set(key, clone); | ||
this.#cacheEntryState.set(key, state); | ||
/** | ||
Generator function that yields each of the cache entry revision | ||
*/ | ||
entryRevisions( | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
const entityRevision = { | ||
entity: value as CacheKeyValue, | ||
revision: ++revisionCounter, | ||
}; | ||
if (this.#entryRevisions.has(key)) { | ||
const revisions = | ||
this.#entryRevisions.get(key)?.concat(entityRevision) || []; | ||
this.#entryRevisions.set(key, revisions); | ||
} else { | ||
this.#entryRevisions.set(key, [entityRevision]); | ||
} | ||
} | ||
} | ||
/** | ||
Generator function that yields each of the cache entry keys | ||
*/ | ||
keys(): AsyncIterableIterator<Key>; | ||
async commitTransaction( | ||
entries: CacheEntry<CacheKeyRegistry, Key, UserExtensionData>[], | ||
entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]> | ||
): Promise<void> { | ||
const sortEntries = entries.sort(([, , state], [, , state1]) => | ||
state?.lastAccessed && | ||
state1?.lastAccessed && | ||
state?.lastAccessed < state1?.lastAccessed | ||
? 1 | ||
: -1 | ||
); | ||
/** | ||
Generator function that yields each of the cache entry values | ||
*/ | ||
values(): AsyncIterableIterator<CacheKeyRegistry[Key]>; | ||
for await (let entry of sortEntries) { | ||
let [key, value, state] = entry; | ||
/** | ||
Creates a live transaction instance | ||
*/ | ||
beginTransaction(): Promise< | ||
LiveCacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
>; | ||
} | ||
// TODO: finalizregistry | ||
this.#weakCache.set(key, new WeakRef(value)); | ||
export interface CacheTransaction< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
/** | ||
Get the value of `cacheKey` in the cache. If `key` has been modified in this | ||
transaction (e.g. via `merge` or `set`), `tx.get` will return the updated | ||
entry in this transaction. The return value can therefore differ from | ||
`cache.get`. | ||
*/ | ||
get(cacheKey: Key): CacheKeyRegistry[Key] | CacheKeyValue | undefined; | ||
this.#cacheEntryState.set(key, state); | ||
[Symbol.asyncIterator]( | ||
entryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
>; | ||
if (state?.retained.lru) { | ||
this.#lruCache.set(key, value); | ||
} | ||
} | ||
/** | ||
Generator function that yields each of the transaction entries including local entries and entries before transaction began. | ||
*/ | ||
entries(): AsyncIterableIterator< | ||
[ | ||
Key, | ||
CacheKeyRegistry[Key] | CacheKeyValue, | ||
CacheEntryState<UserExtensionData> | ||
] | ||
>; | ||
for await (const [cacheKey, revision] of entryRevisions) { | ||
if (this.#entryRevisions.has(cacheKey)) { | ||
const revisions = | ||
this.#entryRevisions.get(cacheKey)?.concat(revision) || []; | ||
this.#entryRevisions.set(cacheKey, revisions); | ||
} else { | ||
this.#entryRevisions.set(cacheKey, revision); | ||
} | ||
} | ||
} | ||
/** | ||
Generator function that yields each of the transaction local entries. | ||
Generator function for async iterable that yields iterable cache entries. This | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
localEntries(): AsyncIterableIterator< | ||
[ | ||
Key, | ||
CacheKeyRegistry[Key] | CacheKeyValue, | ||
CacheEntryState<UserExtensionData> | ||
] | ||
>; | ||
async *[Symbol.asyncIterator](): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
// yield weekly held values | ||
for await (const [key] of this.#weakCache) { | ||
const valueRef = this.#weakCache.get(key)?.deref(); | ||
// Because of the limited guarantees of `FinalizationRegistry`, when yielding | ||
// weakly-held values to the user in `entries` we have to check that the | ||
// value is actually present, | ||
if (!valueRef) { | ||
throw new Error('ref is undefined'); | ||
} | ||
const state = this.#cacheEntryState.get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
} | ||
/** | ||
An async generator that produces the revisions of `key` within this transaction. | ||
Generator function that yields each of the iterable cache entries. Note that this | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
localRevisions( | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
entries(): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
return this[Symbol.asyncIterator](); | ||
} | ||
/** | ||
An async generator that produces the complete list of revisions for `key`, | ||
from the time the transaction began and including the revisions added in this | ||
transaction. | ||
*/ | ||
entryRevisions( | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator]( | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
for (const revision of revisions) { | ||
yield revision; | ||
} | ||
}, | ||
}; | ||
$debug?: $Debug & CacheTransactionDebugAPIs; | ||
} | ||
const revisions = this.#entryRevisions.get(cacheKey) || []; | ||
return entryRevisionIterator[Symbol.asyncIterator](revisions); | ||
} | ||
/** | ||
* Interface specifc to handle Live transaction | ||
*/ | ||
export interface LiveCacheTransaction< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> extends CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> { | ||
/** | ||
* Merges cache entries based on merge strategy | ||
* Generator function that yields each of the iterable cache entry Keys. | ||
*/ | ||
merge( | ||
cacheKey: Key, | ||
value: CachedEntityRevision<CacheKeyValue>, | ||
options?: { | ||
$debug: $Debug; | ||
async *keys(): AsyncIterableIterator<Key> { | ||
for await (const [key] of this.entries()) { | ||
yield key; | ||
} | ||
): Promise<CacheKeyRegistry[Key] | CacheKeyValue>; | ||
} | ||
/** | ||
* sets cache values within the transaction | ||
* Generator function that yields each of the iterable cache entry Values. | ||
*/ | ||
set( | ||
cacheKey: Key, | ||
value: CacheKeyRegistry[Key] | CacheKeyValue | ||
): CacheKeyRegistry[Key] | CacheKeyValue; | ||
/** | ||
* Deletes an entry from live transction | ||
*/ | ||
delete(cacheKey: Key): Promise<boolean>; | ||
/** | ||
* Commits live transction entries. | ||
*/ | ||
commit(): Promise<void>; | ||
} | ||
export interface CommittingTransaction< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> extends Omit< | ||
CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData>, | ||
'get' | 'entries' | 'localEntries' | 'localRevisions' | 'entryRevisions' | ||
> { | ||
cache: { | ||
clearRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key | ||
): void; | ||
appendRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): void; | ||
}; | ||
} | ||
class CommittingTransactionImpl< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> implements | ||
CommittingTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
{ | ||
$debug?: ($Debug & CacheTransactionDebugAPIs) | undefined; | ||
#mergedRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
cache: { | ||
clearRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key | ||
): void; | ||
appendRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): void; | ||
} = { | ||
clearRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key | ||
): void { | ||
tx.#mergedRevisions.delete(id); | ||
}, | ||
appendRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): void { | ||
if (tx.#mergedRevisions.has(id)) { | ||
const appendedRevisions = | ||
tx.#mergedRevisions.get(id)?.concat(revisions) || []; | ||
tx.#mergedRevisions.set(id, appendedRevisions); | ||
} else { | ||
tx.#mergedRevisions.set(id, revisions); | ||
} | ||
}, | ||
}; | ||
constructor() { | ||
this.#mergedRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
>(); | ||
async *values(): AsyncIterableIterator<CacheKeyRegistry[Key]> { | ||
for await (const [, value] of this.entries()) { | ||
yield value; | ||
} | ||
} | ||
[Symbol.asyncIterator]( | ||
entryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
async beginTransaction(): Promise< | ||
LiveCacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
> { | ||
throw new Error('Method not implemented.'); | ||
return await LiveCacheTransactionImpl.beginLiveTransaction(this); | ||
} | ||
mergedRevisions(): Map<Key, CachedEntityRevision<CacheKeyValue>[]> { | ||
return this.#mergedRevisions; | ||
} | ||
} | ||
@@ -860,236 +614,84 @@ | ||
} | ||
class CacheImpl< | ||
class CommittingTransactionImpl< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry = keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> implements Cache<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
> implements | ||
CommittingTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
{ | ||
#weakCache: Map<Key, WeakRef<CacheKeyRegistry[Key]>>; | ||
#entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
#cacheOptions: | ||
| CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
| undefined; | ||
#cacheEntryState: Map<Key, CacheEntryState<UserExtensionData> | undefined>; | ||
#lruCache: LruCacheImpl<CacheKeyRegistry, Key>; | ||
#lruPolicy: number; | ||
$debug?: ($Debug & CacheTransactionDebugAPIs) | undefined; | ||
#mergedRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
constructor( | ||
options: | ||
| CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
| undefined | ||
) { | ||
this.#weakCache = new Map<Key, WeakRef<CacheKeyRegistry[Key]>>(); | ||
this.#cacheOptions = options; | ||
this.#lruPolicy = DEFAULT_EXPIRATION.lru; | ||
this.#entryRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
>(); | ||
this.#cacheEntryState = new Map< | ||
Key, | ||
CacheEntryState<UserExtensionData> | undefined | ||
>(); | ||
cache: { | ||
clearRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key | ||
): void; | ||
appendRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): void; | ||
} = { | ||
clearRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key | ||
): void { | ||
tx.#mergedRevisions.delete(id); | ||
}, | ||
const expiration = this.#cacheOptions?.expiration || DEFAULT_EXPIRATION; | ||
if (expiration && expiration?.lru && typeof expiration.lru === 'number') { | ||
this.#lruPolicy = expiration.lru; | ||
} | ||
this.#lruCache = new LruCacheImpl<CacheKeyRegistry, Key>(this.#lruPolicy); | ||
} | ||
/** | ||
Evict all entries from the cache. | ||
*/ | ||
async clear(): Promise<void> { | ||
for await (const [key] of this.entries()) { | ||
this.#weakCache.delete(key); | ||
this.#lruCache.getCache().delete(key); | ||
this.#entryRevisions.delete(key); | ||
} | ||
} | ||
getCacheOptions(): | ||
| CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
| undefined { | ||
return this.#cacheOptions; | ||
} | ||
async get(cacheKey: Key): Promise<CacheKeyRegistry[Key] | undefined> { | ||
let ref = this.#weakCache.get(cacheKey); | ||
return ref?.deref(); | ||
} | ||
/** | ||
Calling `.save()` without a serializer will iterate over the cache entries | ||
and return an array of cache entry tuples. | ||
*/ | ||
async save(): Promise< | ||
[ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | undefined | ||
][] | ||
> { | ||
const arrayOfCacheEntryTuples: [ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | undefined | ||
][] = []; | ||
for await (const [key, value, state] of this.entries()) { | ||
// TODO create state? | ||
const structuredClonedValue = structuredClone( | ||
value | ||
) as CacheKeyRegistry[Key]; | ||
arrayOfCacheEntryTuples.push([key, structuredClonedValue, state]); | ||
} | ||
return arrayOfCacheEntryTuples; | ||
} | ||
/** | ||
Calling `.load()` will add all entries passed to the cache. | ||
Note: `.load()` does not clear pre-existing entries, if you need to clear | ||
entries before loading call `.clear()`. | ||
*/ | ||
async load( | ||
entries: CacheEntry<CacheKeyRegistry, Key, UserExtensionData>[] | ||
): Promise<void> { | ||
let revisionCounter = 0; | ||
for await (let entry of entries) { | ||
let [key, value, state] = entry; | ||
// TODO: finalizregistry | ||
let clone = structuredClone(value) as CacheKeyRegistry[Key]; | ||
this.#weakCache.set(key, new WeakRef(clone)); | ||
this.#lruCache.set(key, clone); | ||
this.#cacheEntryState.set(key, state); | ||
const entityRevision = { | ||
entity: value as CacheKeyValue, | ||
revision: ++revisionCounter, | ||
}; | ||
if (this.#entryRevisions.has(key)) { | ||
const revisions = | ||
this.#entryRevisions.get(key)?.concat(entityRevision) || []; | ||
this.#entryRevisions.set(key, revisions); | ||
appendRevisions( | ||
tx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): void { | ||
if (tx.#mergedRevisions.has(id)) { | ||
const appendedRevisions = | ||
tx.#mergedRevisions.get(id)?.concat(revisions) || []; | ||
tx.#mergedRevisions.set(id, appendedRevisions); | ||
} else { | ||
this.#entryRevisions.set(key, [entityRevision]); | ||
tx.#mergedRevisions.set(id, revisions); | ||
} | ||
} | ||
} | ||
}, | ||
}; | ||
async commitTransaction( | ||
entries: CacheEntry<CacheKeyRegistry, Key, UserExtensionData>[], | ||
entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]> | ||
): Promise<void> { | ||
const sortEntries = entries.sort(([, , state], [, , state1]) => | ||
state?.lastAccessed && | ||
state1?.lastAccessed && | ||
state?.lastAccessed < state1?.lastAccessed | ||
? 1 | ||
: -1 | ||
); | ||
for await (let entry of sortEntries) { | ||
let [key, value, state] = entry; | ||
// TODO: finalizregistry | ||
this.#weakCache.set(key, new WeakRef(value)); | ||
this.#cacheEntryState.set(key, state); | ||
if (state?.retained.lru) { | ||
this.#lruCache.set(key, value); | ||
} | ||
} | ||
for await (const [cacheKey, revision] of entryRevisions) { | ||
if (this.#entryRevisions.has(cacheKey)) { | ||
const revisions = | ||
this.#entryRevisions.get(cacheKey)?.concat(revision) || []; | ||
this.#entryRevisions.set(cacheKey, revisions); | ||
} else { | ||
this.#entryRevisions.set(cacheKey, revision); | ||
} | ||
} | ||
constructor() { | ||
this.#mergedRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
>(); | ||
} | ||
/** | ||
Generator function for async iterable that yields iterable cache entries. This | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
async *[Symbol.asyncIterator](): AsyncIterableIterator< | ||
[Symbol.asyncIterator]( | ||
entryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
// yield weekly held values | ||
for await (const [key] of this.#weakCache) { | ||
const valueRef = this.#weakCache.get(key)?.deref(); | ||
// Because of the limited guarantees of `FinalizationRegistry`, when yielding | ||
// weakly-held values to the user in `entries` we have to check that the | ||
// value is actually present, | ||
if (!valueRef) { | ||
throw new Error('ref is undefined'); | ||
} | ||
const state = this.#cacheEntryState.get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
throw new Error('Method not implemented.'); | ||
} | ||
/** | ||
Generator function that yields each of the iterable cache entries. Note that this | ||
will include both strongly held (unexpired entries) as well as weakly held | ||
entries. | ||
*/ | ||
entries(): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
return this[Symbol.asyncIterator](); | ||
mergedRevisions(): Map<Key, CachedEntityRevision<CacheKeyValue>[]> { | ||
return this.#mergedRevisions; | ||
} | ||
entryRevisions( | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator]( | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
for (const revision of revisions) { | ||
yield revision; | ||
} | ||
}, | ||
}; | ||
const revisions = this.#entryRevisions.get(cacheKey) || []; | ||
return entryRevisionIterator[Symbol.asyncIterator](revisions); | ||
} | ||
/** | ||
* Generator function that yields each of the iterable cache entry Keys. | ||
*/ | ||
async *keys(): AsyncIterableIterator<Key> { | ||
for await (const [key] of this.entries()) { | ||
yield key; | ||
} | ||
} | ||
/** | ||
* Generator function that yields each of the iterable cache entry Values. | ||
*/ | ||
async *values(): AsyncIterableIterator<CacheKeyRegistry[Key]> { | ||
for await (const [, value] of this.entries()) { | ||
yield value; | ||
} | ||
} | ||
async beginTransaction(): Promise< | ||
LiveCacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
> { | ||
return await LiveCacheTransactionImpl.beginLiveTransaction(this); | ||
} | ||
} | ||
@@ -1110,2 +712,39 @@ | ||
class LruCacheImpl< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry | ||
> implements LruCache<CacheKeyRegistry, Key> | ||
{ | ||
#max: number; | ||
#lruCache: Map<Key, CacheKeyRegistry[Key]>; | ||
constructor(maxCapacity: number) { | ||
this.#max = maxCapacity; | ||
this.#lruCache = new Map<Key, CacheKeyRegistry[Key]>(); | ||
} | ||
set(cacheKey: Key, value: CacheKeyRegistry[Key]) { | ||
// refresh data | ||
if (this.#lruCache.has(cacheKey)) { | ||
this.#lruCache.delete(cacheKey); | ||
} else if (this.#lruCache.size === this.#max) { | ||
// find and evict the LRU entry | ||
const lruEntryKey = this.#lruCache.keys().next().value as Key; | ||
this.#lruCache.delete(lruEntryKey); | ||
} | ||
this.#lruCache.set(cacheKey, value); | ||
} | ||
getCache(): Map<Key, CacheKeyRegistry[Key]> { | ||
return this.#lruCache; | ||
} | ||
} | ||
const DEFAULT_EXPIRATION = { lru: 10000, ttl: 60000 }; | ||
const DEFAULT_ENTRY_STATE = { | ||
retained: { lru: false, ttl: DEFAULT_EXPIRATION.ttl }, | ||
}; | ||
const defaultMergeStrategy = function deepMergeStratey< | ||
@@ -1198,1 +837,12 @@ CacheKeyRegistry extends DefaultRegistry, | ||
} | ||
// eslint-disable-next-line | ||
function structuredClone(x: any): any { | ||
try { | ||
return JSON.parse(JSON.stringify(x)); | ||
} catch (error) { | ||
throw new Error( | ||
'The cache value is not structured clonable use `save` with serializer' | ||
); | ||
} | ||
} |
export { buildCache } from './cache.js'; | ||
export type { | ||
Cache, | ||
CacheTransaction, | ||
LiveCacheTransaction, | ||
CommittingTransaction, | ||
CacheEntry, | ||
CacheEntryState, | ||
CacheKeyValue, | ||
EntityMergeStrategy, | ||
RevisionMergeStrategy, | ||
CachedEntityRevision, | ||
ExpirationPolicy, | ||
CacheOptions, | ||
DefaultRegistry, | ||
CacheDebugAPIs, | ||
LruCache, | ||
CacheTransactionDebugAPIs, | ||
} from '../types/api.js'; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
391564
21
1
2247
2