@data-eden/cache
Advanced tools
Comparing version 0.5.0 to 0.6.0
@@ -188,12 +188,2 @@ import { describe, it, expect } from 'vitest'; | ||
// TODO: test clear (load, get, clear, get) | ||
// TODO: test save (with values, save then clear, then load, values should be restored) | ||
// transaction testing ---------------- | ||
// TODO: test transactions | ||
// memory testing ------------------- | ||
// TODO: test lru (unit test lru) | ||
// TODO: test ttl? | ||
// TODO: --expose-gc + setTimeout global.gc() + another setTimeout() + assert weakly held things are cleaned up | ||
@@ -293,23 +283,17 @@ | ||
await tx.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:1', { | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:3', { 'book:3': { title: 'New Merged book' } }); | ||
await tx.merge('book:1', { 'book:1': { title: 'Conflict', sub: 'j3' } }); | ||
// Validate Transactional entries | ||
expect(tx.get('book:1')).toEqual({ | ||
expect(await tx.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict', sub: 'j3' }, | ||
}); | ||
expect(tx.get('book:2')).toEqual({ | ||
expect(await tx.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx.get('book:3')).toEqual({ | ||
expect(await tx.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book' }, | ||
}); | ||
// Validate Cache before commit | ||
//Validate Cache before commit | ||
expect(await cache.get('book:1')).toEqual({ | ||
@@ -340,62 +324,2 @@ 'book:1': { title: 'A History of the English speaking peoples' }, | ||
it('test single transaction with commit', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
[ | ||
'book:1', | ||
{ 'book:1': { title: 'A History of the English speaking peoples' } }, | ||
], | ||
['book:2', { 'book:2': { title: 'Marlborough: his life and times' } }], | ||
]); | ||
// transaction 1 starts | ||
let tx = await cache.beginTransaction(); | ||
await tx.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:1', { | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 1, | ||
}); | ||
// Validate Transactional entries | ||
expect(tx.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict', sub: 'j3' }, | ||
}); | ||
expect(tx.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book' }, | ||
}); | ||
// Validate Cache before commit | ||
expect(await cache.get('book:1')).toEqual({ | ||
'book:1': { title: 'A History of the English speaking peoples' }, | ||
}); | ||
expect(await cache.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(await cache.get('book:3')).toEqual(undefined); | ||
const cacheEntriesBeforeCommit = await cache.save(); | ||
expect(cacheEntriesBeforeCommit.length).toEqual(2); | ||
await tx.commit(); | ||
// Validate Cache after commit | ||
expect(await cache.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict', sub: 'j3' }, | ||
}); | ||
expect(await cache.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(await cache.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book' }, | ||
}); | ||
}); | ||
it('test cache with multiple transaction commits is masked from trasaction changes', async function () { | ||
@@ -419,9 +343,5 @@ let cache = buildCache(); | ||
// Merge entities from transaction 1 | ||
await tx1.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book TX1' } }, | ||
revision: 1, | ||
}); | ||
await tx1.merge('book:3', { 'book:3': { title: 'New Merged book TX1' } }); | ||
await tx1.merge('book:1', { | ||
entity: { 'book:1': { title: 'original book Conflict', sub: 'j3' } }, | ||
revision: 1, | ||
'book:1': { title: 'original book Conflict', sub: 'j3' }, | ||
}); | ||
@@ -431,28 +351,24 @@ | ||
await tx2.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book by TX2' } }, | ||
revision: 1, | ||
'book:3': { title: 'New Merged book by TX2' }, | ||
}); | ||
await tx2.merge('book:1', { | ||
entity: { | ||
'book:1': { | ||
title: 'Conflict updated by TX2', | ||
sub: 'j32', | ||
sub2: '12', | ||
}, | ||
'book:1': { | ||
title: 'Conflict updated by TX2', | ||
sub: 'j32', | ||
sub2: '12', | ||
}, | ||
revision: 1, | ||
}); | ||
await tx2.merge('book:4', { | ||
entity: { 'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' } }, | ||
revision: 1, | ||
'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' }, | ||
}); | ||
// Validate entries in Transaction 1 | ||
expect(tx1.get('book:1')).toEqual({ | ||
expect(await tx1.get('book:1')).toEqual({ | ||
'book:1': { title: 'original book Conflict', sub: 'j3' }, | ||
}); | ||
expect(tx1.get('book:2')).toEqual({ | ||
expect(await tx1.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx1.get('book:3')).toEqual({ | ||
expect(await tx1.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book TX1' }, | ||
@@ -462,12 +378,12 @@ }); | ||
// Validate entries in Transaction 2 | ||
expect(tx2.get('book:1')).toEqual({ | ||
expect(await tx2.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict updated by TX2', sub: 'j32', sub2: '12' }, | ||
}); | ||
expect(tx2.get('book:2')).toEqual({ | ||
expect(await tx2.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx2.get('book:3')).toEqual({ | ||
expect(await tx2.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book by TX2' }, | ||
}); | ||
expect(tx2.get('book:4')).toEqual({ | ||
expect(await tx2.get('book:4')).toEqual({ | ||
'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' }, | ||
@@ -501,12 +417,12 @@ }); | ||
// Validate entries in Transaction 2 Cache after 1st transaction commit and it remains masked | ||
expect(tx2.get('book:1')).toEqual({ | ||
expect(await tx2.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict updated by TX2', sub: 'j32', sub2: '12' }, | ||
}); | ||
expect(tx2.get('book:2')).toEqual({ | ||
expect(await tx2.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx2.get('book:3')).toEqual({ | ||
expect(await tx2.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book by TX2' }, | ||
}); | ||
expect(tx2.get('book:4')).toEqual({ | ||
expect(await tx2.get('book:4')).toEqual({ | ||
'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' }, | ||
@@ -546,14 +462,8 @@ }); | ||
await tx.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:1', { | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:3', { 'book:3': { title: 'New Merged book' } }); | ||
await tx.merge('book:1', { 'book:1': { title: 'Conflict', sub: 'j3' } }); | ||
const localEntries = []; | ||
for await (const [key, value, state] of tx.localEntries()) { | ||
localEntries.push([key, value, state]); | ||
for await (const [key, value] of tx.localEntries()) { | ||
localEntries.push([key, value]); | ||
} | ||
@@ -588,19 +498,13 @@ | ||
await tx.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:3', { 'book:3': { title: 'New Merged book' } }); | ||
await tx.merge('book:1', { | ||
entity: { | ||
'book:1': { | ||
title: 'Conflict', | ||
sub: 'j3', | ||
subjects: [{ a: 1 }, { b: 2 }], | ||
}, | ||
'book:1': { | ||
title: 'Conflict', | ||
sub: 'j3', | ||
subjects: [{ a: 1 }, { b: 2 }], | ||
}, | ||
revision: 1, | ||
}); | ||
// Validate Transactional entries | ||
expect(tx.get('book:1')).toEqual({ | ||
expect(await tx.get('book:1')).toEqual({ | ||
'book:1': { | ||
@@ -612,6 +516,6 @@ title: 'Conflict', | ||
}); | ||
expect(tx.get('book:2')).toEqual({ | ||
expect(await tx.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx.get('book:3')).toEqual({ | ||
expect(await tx.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book' }, | ||
@@ -672,13 +576,5 @@ }); | ||
await tx.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book' } }, | ||
revision: 1, | ||
}); | ||
await tx.merge('book:3', { 'book:3': { title: 'New Merged book' } }); | ||
await tx.delete('book:1'); | ||
expect(tx.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx.get('book:1')).toEqual(undefined); | ||
expect(await tx.delete('book:1')).toEqual(true); | ||
}); | ||
@@ -700,6 +596,3 @@ }); | ||
await tx.merge('book:1', { | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 2, | ||
}); | ||
await tx.merge('book:1', { 'book:1': { title: 'Conflict', sub: 'j3' } }); | ||
@@ -751,12 +644,6 @@ await tx.commit(); | ||
tx.set('book:5', { 'book:5': { title: 'A History5_lru' } }); | ||
tx.get('book:3'); | ||
await tx.merge('book:4', { | ||
entity: { 'book:4': { title: 'A History4_lru' } }, | ||
revision: 2, | ||
}); | ||
await tx.merge('book:1', { | ||
entity: { 'book:1': { title: 'A History1_lru' } }, | ||
revision: 1, | ||
}); | ||
await tx.set('book:5', { 'book:5': { title: 'A History5_lru' } }); | ||
await tx.get('book:3'); | ||
await tx.merge('book:4', { 'book:4': { title: 'A History4_lru' } }); | ||
await tx.merge('book:1', { 'book:1': { title: 'A History1_lru' } }); | ||
@@ -779,2 +666,42 @@ await tx.commit(); | ||
}); | ||
describe('test commit queue & lock', function () { | ||
it('test commit for a deferred transaction from the queue', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
['book:1', { 'book:1': { title: 'My book1' } }], | ||
['book:2', { 'book:2': { title: 'My book2' } }], | ||
]); | ||
// transaction 1 starts | ||
let tx1 = await cache.beginTransaction(); | ||
// Merge entities from transaction 1 | ||
await tx1.merge('book:1', { | ||
'book:1': { title: 'My Merged book1' }, | ||
}); | ||
// transaction 2 starts | ||
let tx2 = await cache.beginTransaction(); | ||
// Merge entities from transaction 2 | ||
await tx2.merge('book:2', { | ||
'book:2': { title: 'My Merged book2' }, | ||
}); | ||
// Hold transaction 1 commit | ||
const commitHoldingLock = tx1.commit(); | ||
// commit transaction 2 so it gets deferred | ||
await tx2.commit(); | ||
await commitHoldingLock; | ||
expect(await cache.get('book:1')).toEqual({ | ||
'book:1': { title: 'My Merged book1' }, | ||
}); | ||
expect(await cache.get('book:2')).toEqual({ | ||
'book:2': { title: 'My Merged book2' }, | ||
}); | ||
}); | ||
}); | ||
}); |
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 declare function assert<T>(value: T, message: string | (() => string)): asserts value; | ||
//# sourceMappingURL=cache.d.ts.map |
@@ -12,5 +12,7 @@ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { | ||
}; | ||
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; | ||
var _CacheImpl_instances, _CacheImpl_weakCache, _CacheImpl_entryRevisions, _CacheImpl_cacheOptions, _CacheImpl_cacheEntryState, _CacheImpl_lruCache, _CacheImpl_lruPolicy, _CacheImpl_txCommitLockOwner, _CacheImpl_txCommitLockQueue, _CacheImpl_deferTxLock, _CacheImpl_aquireTxCommitLock, _CacheImpl_releaseTxCommitLock, _CacheImpl_commitUpdatesAndReleaseLock, _LiveCacheTransactionImpl_instances, _LiveCacheTransactionImpl_originalCacheReference, _LiveCacheTransactionImpl_transactionalCache, _LiveCacheTransactionImpl_commitingTransaction, _LiveCacheTransactionImpl_cacheEntryState, _LiveCacheTransactionImpl_userOptionRetentionPolicy, _LiveCacheTransactionImpl_ttlPolicy, _LiveCacheTransactionImpl_lruPolicy, _LiveCacheTransactionImpl_localRevisions, _LiveCacheTransactionImpl_entryRevisions, _LiveCacheTransactionImpl_transactionOperations, _LiveCacheTransactionImpl_getMergeStrategy, _LiveCacheTransactionImpl_getRevisionStrategy, _LiveCacheTransactionImpl_prepareTransaction, _CommittingTransactionImpl_mergedRevisions, _LruCacheImpl_max, _LruCacheImpl_lruCache; | ||
let REVISION_COUNTER = 0; | ||
class CacheImpl { | ||
constructor(options) { | ||
_CacheImpl_instances.add(this); | ||
_CacheImpl_weakCache.set(this, void 0); | ||
@@ -22,2 +24,4 @@ _CacheImpl_entryRevisions.set(this, void 0); | ||
_CacheImpl_lruPolicy.set(this, void 0); | ||
_CacheImpl_txCommitLockOwner.set(this, void 0); | ||
_CacheImpl_txCommitLockQueue.set(this, void 0); | ||
__classPrivateFieldSet(this, _CacheImpl_weakCache, new Map(), "f"); | ||
@@ -33,2 +37,4 @@ __classPrivateFieldSet(this, _CacheImpl_cacheOptions, options, "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_lruCache, new LruCacheImpl(__classPrivateFieldGet(this, _CacheImpl_lruPolicy, "f")), "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_txCommitLockOwner, null, "f"); | ||
__classPrivateFieldSet(this, _CacheImpl_txCommitLockQueue, [], "f"); | ||
} | ||
@@ -92,27 +98,2 @@ /** | ||
} | ||
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); | ||
} | ||
} | ||
} | ||
/** | ||
@@ -123,3 +104,3 @@ Generator function for async iterable that yields iterable cache entries. This | ||
*/ | ||
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)]() { | ||
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(), _CacheImpl_txCommitLockOwner = new WeakMap(), _CacheImpl_txCommitLockQueue = new WeakMap(), _CacheImpl_instances = new WeakSet(), Symbol.asyncIterator)]() { | ||
// yield weekly held values | ||
@@ -131,7 +112,6 @@ for await (const [key] of __classPrivateFieldGet(this, _CacheImpl_weakCache, "f")) { | ||
// value is actually present, | ||
if (!valueRef) { | ||
throw new Error('ref is undefined'); | ||
if (valueRef) { | ||
const state = __classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
const state = __classPrivateFieldGet(this, _CacheImpl_cacheEntryState, "f").get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
@@ -175,10 +155,83 @@ } | ||
async beginTransaction() { | ||
return await LiveCacheTransactionImpl.beginLiveTransaction(this); | ||
const aquireTxCommitLock = (transaction) => __classPrivateFieldGet(this, _CacheImpl_instances, "m", _CacheImpl_aquireTxCommitLock).call(this, transaction); | ||
const releaseTxCommitLock = (transaction) => __classPrivateFieldGet(this, _CacheImpl_instances, "m", _CacheImpl_releaseTxCommitLock).call(this, transaction); | ||
const commitUpdatesAndReleaseLock = (transaction, txUpdates) => __classPrivateFieldGet(this, _CacheImpl_instances, "m", _CacheImpl_commitUpdatesAndReleaseLock).call(this, transaction, txUpdates); | ||
return new LiveCacheTransactionImpl(this, { | ||
aquireTxCommitLock, | ||
releaseTxCommitLock, | ||
commitUpdatesAndReleaseLock, | ||
}); | ||
} | ||
} | ||
_CacheImpl_deferTxLock = function _CacheImpl_deferTxLock(transaction) { | ||
let resolveTxLock; | ||
let rejectTxLock; | ||
const promiseTxLock = new Promise((resolve, reject) => { | ||
resolveTxLock = resolve; | ||
rejectTxLock = reject; | ||
}); | ||
return { | ||
resolve: resolveTxLock, | ||
reject: rejectTxLock, | ||
promise: promiseTxLock, | ||
owner: transaction, | ||
}; | ||
}, _CacheImpl_aquireTxCommitLock = function _CacheImpl_aquireTxCommitLock(transaction) { | ||
if (__classPrivateFieldGet(this, _CacheImpl_txCommitLockOwner, "f") === null) { | ||
__classPrivateFieldSet(this, _CacheImpl_txCommitLockOwner, transaction, "f"); | ||
return new Promise((resolve) => { | ||
// start a timeout to ensure tx cannot hold on to the lock indefinitely | ||
resolve(__classPrivateFieldGet(this, _CacheImpl_txCommitLockOwner, "f")); | ||
setTimeout(() => { | ||
// if transaction is still locked after 3 seconds then release the lock | ||
__classPrivateFieldGet(this, _CacheImpl_instances, "m", _CacheImpl_releaseTxCommitLock).call(this, transaction); | ||
}, 3000); | ||
return; | ||
}); | ||
} | ||
let deferredTransactionLock = __classPrivateFieldGet(this, _CacheImpl_instances, "m", _CacheImpl_deferTxLock).call(this, transaction); | ||
__classPrivateFieldGet(this, _CacheImpl_txCommitLockQueue, "f").push(deferredTransactionLock); | ||
return deferredTransactionLock.promise; | ||
}, _CacheImpl_releaseTxCommitLock = function _CacheImpl_releaseTxCommitLock(transaction) { | ||
//assert(this.#txCommitLockOwner === transaction, 'transaction owner incorrectly assigned when releasing lock'); | ||
if (__classPrivateFieldGet(this, _CacheImpl_txCommitLockOwner, "f") === transaction) { | ||
__classPrivateFieldSet(this, _CacheImpl_txCommitLockOwner, null, "f"); | ||
} | ||
}, _CacheImpl_commitUpdatesAndReleaseLock = function _CacheImpl_commitUpdatesAndReleaseLock(transaction, txUpdates) { | ||
assert(__classPrivateFieldGet(this, _CacheImpl_txCommitLockOwner, "f") === transaction, 'transaction owner incorrectly assigned when commiting updates'); | ||
// Write transaction entries to the main cache | ||
for (const entry of txUpdates.entries) { | ||
const [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); | ||
} | ||
} | ||
// Write transaction revisions entries to the main cache | ||
for (const [cacheKey, revision] of txUpdates.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); | ||
} | ||
} | ||
__classPrivateFieldGet(this, _CacheImpl_instances, "m", _CacheImpl_releaseTxCommitLock).call(this, transaction); | ||
if (__classPrivateFieldGet(this, _CacheImpl_txCommitLockQueue, "f").length > 0) { | ||
const waitingTxDeferred = __classPrivateFieldGet(this, _CacheImpl_txCommitLockQueue, "f").shift(); | ||
if (waitingTxDeferred) { | ||
//this.#txCommitLockOwner = null; | ||
__classPrivateFieldSet(this, _CacheImpl_txCommitLockOwner, waitingTxDeferred.owner, "f"); | ||
} | ||
waitingTxDeferred.resolve(); | ||
} | ||
}; | ||
class LiveCacheTransactionImpl { | ||
constructor(originalCache, transactionalCacheEntryMap, entryRevisions) { | ||
constructor(originalCache, transactionOperations) { | ||
_LiveCacheTransactionImpl_instances.add(this); | ||
_LiveCacheTransactionImpl_originalCacheReference.set(this, void 0); | ||
_LiveCacheTransactionImpl_transactionalCache.set(this, void 0); | ||
_LiveCacheTransactionImpl_localUpdatedEntries.set(this, void 0); | ||
_LiveCacheTransactionImpl_commitingTransaction.set(this, void 0); | ||
@@ -191,5 +244,5 @@ _LiveCacheTransactionImpl_cacheEntryState.set(this, void 0); | ||
_LiveCacheTransactionImpl_entryRevisions.set(this, void 0); | ||
_LiveCacheTransactionImpl_transactionOperations.set(this, void 0); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_originalCacheReference, originalCache, "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_transactionalCache, transactionalCacheEntryMap, "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_localUpdatedEntries, new Map(), "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_transactionalCache, new Map(), "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_cacheEntryState, new Map(), "f"); | ||
@@ -199,3 +252,3 @@ __classPrivateFieldSet(this, _LiveCacheTransactionImpl_ttlPolicy, DEFAULT_EXPIRATION.ttl, "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_localRevisions, new Map(), "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_entryRevisions, entryRevisions, "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_entryRevisions, new Map(), "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_userOptionRetentionPolicy, __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.expiration || | ||
@@ -214,37 +267,45 @@ DEFAULT_EXPIRATION, "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_commitingTransaction, new CommittingTransactionImpl(), "f"); | ||
__classPrivateFieldSet(this, _LiveCacheTransactionImpl_transactionOperations, transactionOperations, "f"); | ||
} | ||
static async beginLiveTransaction(originalCache) { | ||
const transactionalCache = new Map(); | ||
const entryRevisions = new Map(); | ||
for await (const [key, value] of originalCache.entries()) { | ||
transactionalCache.set(key, { ...value }); | ||
for await (const entryRevision of originalCache.entryRevisions(key)) { | ||
entryRevisions.set(key, [entryRevision]); | ||
} | ||
async *[(_LiveCacheTransactionImpl_originalCacheReference = new WeakMap(), _LiveCacheTransactionImpl_transactionalCache = new WeakMap(), _LiveCacheTransactionImpl_commitingTransaction = new WeakMap(), _LiveCacheTransactionImpl_cacheEntryState = new WeakMap(), _LiveCacheTransactionImpl_userOptionRetentionPolicy = new WeakMap(), _LiveCacheTransactionImpl_ttlPolicy = new WeakMap(), _LiveCacheTransactionImpl_lruPolicy = new WeakMap(), _LiveCacheTransactionImpl_localRevisions = new WeakMap(), _LiveCacheTransactionImpl_entryRevisions = new WeakMap(), _LiveCacheTransactionImpl_transactionOperations = new WeakMap(), _LiveCacheTransactionImpl_instances = new WeakSet(), Symbol.asyncIterator)]() { | ||
for await (const [key, value] of this.localEntries()) { | ||
const state = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_cacheEntryState, "f").get(key); | ||
yield [key, value, state]; | ||
} | ||
return new LiveCacheTransactionImpl(originalCache, transactionalCache, entryRevisions); | ||
} | ||
get(cacheKey) { | ||
const cacheValue = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").get(cacheKey); | ||
if (cacheValue) { | ||
// Update cache entry state | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_cacheEntryState, "f").set(cacheKey, { | ||
retained: { lru: true, ttl: __classPrivateFieldGet(this, _LiveCacheTransactionImpl_ttlPolicy, "f") }, | ||
lastAccessed: Date.now(), | ||
}); | ||
async get(cacheKey) { | ||
// 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; | ||
} | ||
} | ||
return cacheValue; | ||
return cachedValue || (await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").get(cacheKey)); | ||
} | ||
async *[(_LiveCacheTransactionImpl_originalCacheReference = new WeakMap(), _LiveCacheTransactionImpl_transactionalCache = new WeakMap(), _LiveCacheTransactionImpl_localUpdatedEntries = new WeakMap(), _LiveCacheTransactionImpl_commitingTransaction = new WeakMap(), _LiveCacheTransactionImpl_cacheEntryState = new WeakMap(), _LiveCacheTransactionImpl_userOptionRetentionPolicy = new WeakMap(), _LiveCacheTransactionImpl_ttlPolicy = new WeakMap(), _LiveCacheTransactionImpl_lruPolicy = new WeakMap(), _LiveCacheTransactionImpl_localRevisions = new WeakMap(), _LiveCacheTransactionImpl_entryRevisions = new WeakMap(), Symbol.asyncIterator)](entryMap) { | ||
for (const [key, value] of entryMap) { | ||
const state = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_cacheEntryState, "f").get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, value, state]; | ||
} | ||
localEntries() { | ||
const localEntriesIterator = { | ||
async *[Symbol.asyncIterator](localEntryMap) { | ||
for (const [key, value] of localEntryMap) { | ||
yield [key, value]; | ||
} | ||
}, | ||
}; | ||
return localEntriesIterator[Symbol.asyncIterator](__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f")); | ||
} | ||
entries() { | ||
return this[Symbol.asyncIterator](__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f")); | ||
async entries() { | ||
const entriesIterator = { | ||
async *[Symbol.asyncIterator](localEntriesIterator, cacheRef) { | ||
for await (const [key, transactionValue] of localEntriesIterator) { | ||
yield [key, transactionValue]; | ||
const cacheValue = await cacheRef.get(key); | ||
if (cacheValue) { | ||
yield [key, cacheValue]; | ||
} | ||
} | ||
}, | ||
}; | ||
return entriesIterator[Symbol.asyncIterator](this.localEntries(), __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f")); | ||
} | ||
localEntries() { | ||
return this[Symbol.asyncIterator](__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localUpdatedEntries, "f")); | ||
} | ||
localRevisions(cacheKey) { | ||
@@ -273,5 +334,4 @@ const entryRevisionIterator = { | ||
} | ||
set(cacheKey, value) { | ||
async set(cacheKey, value) { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").set(cacheKey, value); | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localUpdatedEntries, "f").set(cacheKey, value); | ||
// Update cache entry state | ||
@@ -282,36 +342,40 @@ __classPrivateFieldGet(this, _LiveCacheTransactionImpl_cacheEntryState, "f").set(cacheKey, { | ||
}); | ||
return value; | ||
return (await this.get(cacheKey)); | ||
} | ||
async delete(cacheKey) { | ||
return new Promise((resolve) => { | ||
if (__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").has(cacheKey)) { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").delete(cacheKey); | ||
} | ||
if (__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localUpdatedEntries, "f").has(cacheKey)) { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localUpdatedEntries, "f").delete(cacheKey); | ||
} | ||
return resolve(__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").has(cacheKey) === false && | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localUpdatedEntries, "f").has(cacheKey) === false); | ||
}); | ||
// tx.delete will actually need to write a tombstone in the transaction entries and the actual delete will occur when the transaction is committed to the cache. | ||
// The semantics of tx.delete's return value should be "did i delete something?" | ||
if (await this.get(cacheKey)) { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").delete(cacheKey); | ||
// Update cache entry state to indicate as delete in order to actually be deleted from cache when commit | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_cacheEntryState, "f").set(cacheKey, { | ||
retained: { lru: false, ttl: 0 }, | ||
deletedRecordInTransaction: true, | ||
lastAccessed: Date.now(), | ||
}); | ||
return true; | ||
} | ||
return false; | ||
} | ||
async merge(cacheKey, entityRevision, options) { | ||
// assign custom merge strategy if specified else use default | ||
const mergeStrategyFromCacheOptionHook = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks | ||
?.entitymergeStrategy; | ||
const mergeStrategy = mergeStrategyFromCacheOptionHook || defaultMergeStrategy; | ||
async merge(cacheKey, entity, options) { | ||
const mergeStrategy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_instances, "m", _LiveCacheTransactionImpl_getMergeStrategy).call(this, options?.entityMergeStrategy); | ||
// get current cache value within this transaction | ||
const currentValue = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionalCache, "f").get(cacheKey); | ||
const mergedEntity = mergeStrategy(cacheKey, { | ||
entity: entityRevision.entity, | ||
revision: entityRevision.revision, | ||
revisionContext: entityRevision?.revisionContext, | ||
}, currentValue, this); | ||
// TODO throw error if Merge entity is undefined | ||
const currentValue = await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").get(cacheKey); | ||
const revisionCounter = REVISION_COUNTER++; | ||
// get merged entity | ||
const mergedEntity = currentValue | ||
? entity | ||
: mergeStrategy(cacheKey, { | ||
entity, | ||
revision: REVISION_COUNTER++, | ||
revisionContext: options?.revisionContext, | ||
}, currentValue, this); | ||
// Update transactional cache with merged entity | ||
this.set(cacheKey, mergedEntity); | ||
// Calling set here will in turn also update cacheEntryState | ||
await this.set(cacheKey, mergedEntity); | ||
// Update local & entry revisions with new revision values | ||
const revision = { | ||
entity: mergedEntity, | ||
revision: entityRevision.revision, | ||
revisionContext: entityRevision?.revisionContext, | ||
revision: revisionCounter, | ||
revisionContext: options?.revisionContext, | ||
}; | ||
@@ -327,57 +391,70 @@ if (__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").has(cacheKey)) { | ||
async commit(options) { | ||
const timeout = options?.timeout ? options.timeout : 10000; | ||
const commitLock = new Promise((resolve, reject) => setTimeout(reject, timeout)); | ||
const writeToCache = async () => { | ||
const trasactionCacheEntries = []; | ||
for await (const [cacheKey, value, state] of this.localEntries()) { | ||
const latestCacheValue = await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").get(cacheKey); | ||
let entityToCommit; | ||
// assign custom merge strategy if specified else use default | ||
const mergeStrategyFromCacheOptionHook = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks | ||
?.entitymergeStrategy; | ||
const mergeStrategy = mergeStrategyFromCacheOptionHook || defaultMergeStrategy; | ||
if (latestCacheValue) { | ||
// TODO fix revision | ||
entityToCommit = mergeStrategy(cacheKey, { entity: value, revision: 3 }, latestCacheValue, this); | ||
} | ||
else { | ||
entityToCommit = value; | ||
} | ||
const structuredClonedValue = structuredClone(entityToCommit); | ||
trasactionCacheEntries.push([cacheKey, structuredClonedValue, state]); | ||
// Update saved revisions of the entity | ||
const localRevisions = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").get(cacheKey); | ||
let revisionNumber = localRevisions && localRevisions[localRevisions.length - 1].revision | ||
? localRevisions[localRevisions.length - 1].revision | ||
: 0; | ||
const entityRevision = { | ||
entity: entityToCommit, | ||
revision: ++revisionNumber, | ||
}; | ||
if (__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").has(cacheKey)) { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").get(cacheKey)?.push(entityRevision); | ||
} | ||
else { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").set(cacheKey, [entityRevision]); | ||
} | ||
const revisionStrategy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions() | ||
?.hooks?.revisionMergeStrategy | ||
? async (id, commitTx, liveTx) => __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks | ||
?.revisionMergeStrategy | ||
: defaultRevisionStrategy; | ||
// Update revisions based on revision strategy | ||
await revisionStrategy(cacheKey, __classPrivateFieldGet(this, _LiveCacheTransactionImpl_commitingTransaction, "f"), this); | ||
} | ||
// Call commit hook to apply custom retention policies before commit (if passed by cache options) | ||
const customRetentionPolicy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks?.commit; | ||
if (customRetentionPolicy) { | ||
customRetentionPolicy(this); | ||
} | ||
const mergedRevisions = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_commitingTransaction, "f").mergedRevisions(); | ||
// commit merged transaction & revisions entries to main cache | ||
await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").commitTransaction(trasactionCacheEntries, mergedRevisions); | ||
await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionOperations, "f").aquireTxCommitLock(this); | ||
let transactionUpdates; | ||
try { | ||
transactionUpdates = await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_instances, "m", _LiveCacheTransactionImpl_prepareTransaction).call(this); | ||
return __classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionOperations, "f").commitUpdatesAndReleaseLock(this, transactionUpdates); | ||
} | ||
catch (e) { | ||
throw new Error('Failed to prepare transaction updates'); | ||
} | ||
finally { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_transactionOperations, "f").releaseTxCommitLock(this); | ||
} | ||
} | ||
} | ||
_LiveCacheTransactionImpl_getMergeStrategy = function _LiveCacheTransactionImpl_getMergeStrategy(transactionMergeStrategy) { | ||
const cacheWideMergeStrategy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks | ||
?.entitymergeStrategy; | ||
return (transactionMergeStrategy || cacheWideMergeStrategy || defaultMergeStrategy); | ||
}, _LiveCacheTransactionImpl_getRevisionStrategy = function _LiveCacheTransactionImpl_getRevisionStrategy() { | ||
const cacheWideRevisionStrategy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks | ||
?.revisionMergeStrategy; | ||
return cacheWideRevisionStrategy || defaultRevisionStrategy; | ||
}, _LiveCacheTransactionImpl_prepareTransaction = async function _LiveCacheTransactionImpl_prepareTransaction() { | ||
const trasactionCacheEntries = []; | ||
for await (const [cacheKey, value] of this.localEntries()) { | ||
const latestCacheValue = await __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").get(cacheKey); | ||
let mergedEntityToCommit; | ||
const mergeStrategy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_instances, "m", _LiveCacheTransactionImpl_getMergeStrategy).call(this); | ||
if (latestCacheValue) { | ||
// TODO fix revision | ||
mergedEntityToCommit = mergeStrategy(cacheKey, { entity: value, revision: 3 }, latestCacheValue, this); | ||
} | ||
else { | ||
mergedEntityToCommit = value; | ||
} | ||
const structuredClonedValue = structuredClone(mergedEntityToCommit); | ||
const state = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_cacheEntryState, "f").get(cacheKey) || DEFAULT_ENTRY_STATE; | ||
trasactionCacheEntries.push([cacheKey, structuredClonedValue, state]); | ||
// Update saved revisions of the entity | ||
const localRevisions = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").get(cacheKey); | ||
let revisionNumber = localRevisions && localRevisions[localRevisions.length - 1].revision | ||
? localRevisions[localRevisions.length - 1].revision | ||
: 0; | ||
const entityRevision = { | ||
entity: mergedEntityToCommit, | ||
revision: ++revisionNumber, | ||
}; | ||
await Promise.race([writeToCache(), commitLock]); | ||
if (__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").has(cacheKey)) { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").get(cacheKey)?.push(entityRevision); | ||
} | ||
else { | ||
__classPrivateFieldGet(this, _LiveCacheTransactionImpl_localRevisions, "f").set(cacheKey, [entityRevision]); | ||
} | ||
const revisionStrategy = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_instances, "m", _LiveCacheTransactionImpl_getRevisionStrategy).call(this); | ||
// Update revisions based on revision strategy | ||
await revisionStrategy(cacheKey, __classPrivateFieldGet(this, _LiveCacheTransactionImpl_commitingTransaction, "f"), this); | ||
} | ||
} | ||
// Call commit hook to apply custom retention policies before commit (if passed by cache options) | ||
const commitCallback = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_originalCacheReference, "f").getCacheOptions()?.hooks?.commit; | ||
if (commitCallback) { | ||
await commitCallback(this); | ||
} | ||
const mergedRevisions = __classPrivateFieldGet(this, _LiveCacheTransactionImpl_commitingTransaction, "f").mergedRevisions(); | ||
return { | ||
entries: trasactionCacheEntries, | ||
entryRevisions: mergedRevisions, | ||
}; | ||
}; | ||
class CommittingTransactionImpl { | ||
@@ -402,3 +479,3 @@ constructor() { | ||
} | ||
[(_CommittingTransactionImpl_mergedRevisions = new WeakMap(), Symbol.asyncIterator)](entryMap) { | ||
[(_CommittingTransactionImpl_mergedRevisions = new WeakMap(), Symbol.asyncIterator)]() { | ||
throw new Error('Method not implemented.'); | ||
@@ -441,3 +518,3 @@ } | ||
}; | ||
const defaultMergeStrategy = function deepMergeStratey(id, { entity, revision }, current, tx) { | ||
const defaultMergeStrategy = function deepMergeStratey(id, { entity }, current, tx) { | ||
return deepMerge(current, entity); | ||
@@ -492,2 +569,12 @@ }; | ||
} | ||
export function assert(value, message) { | ||
if (!value) { | ||
if (typeof message === 'string') { | ||
throw new Error(`[@data-eden/cache] internal error: ${message}`); | ||
} | ||
if (typeof message === 'function') { | ||
throw new Error(`[@data-eden/cache] internal error: ${message()}`); | ||
} | ||
} | ||
} | ||
//# 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'; | ||
export type { Cache, CacheTransaction, LiveCacheTransaction, CommittingTransaction, CacheEntry, CacheEntryState, CacheKeyValue, EntityMergeStrategy, RevisionMergeStrategy, CachedEntityRevision, ExpirationPolicy, CacheOptions, DefaultRegistry, CacheDebugAPIs, LruCache, CacheTransactionDebugAPIs, TransactionUpdates, TransactionOperations, DeferredTransactionLock, } from '../types/api.js'; | ||
//# sourceMappingURL=index.d.ts.map |
{ | ||
"$schema": "https://json.schemastore.org/package.json", | ||
"name": "@data-eden/cache", | ||
"version": "0.5.0", | ||
"version": "0.6.0", | ||
"repository": { | ||
@@ -6,0 +6,0 @@ "type": "git", |
709
src/cache.ts
@@ -15,4 +15,11 @@ import type { | ||
CacheTransactionDebugAPIs, | ||
EntityMergeStrategy, | ||
// RevisionMergeStrategy, | ||
TransactionUpdates, | ||
TransactionOperations, | ||
DeferredTransactionLock, | ||
} from './index.js'; | ||
let REVISION_COUNTER = 0; | ||
class CacheImpl< | ||
@@ -26,3 +33,3 @@ CacheKeyRegistry extends DefaultRegistry, | ||
#weakCache: Map<Key, WeakRef<CacheKeyRegistry[Key]>>; | ||
#entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
#entryRevisions: Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]>; | ||
#cacheOptions: | ||
@@ -35,2 +42,16 @@ | CacheOptions<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
#txCommitLockOwner: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | null; | ||
#txCommitLockQueue: DeferredTransactionLock< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>[]; | ||
constructor( | ||
@@ -46,3 +67,3 @@ options: | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
>(); | ||
@@ -59,2 +80,5 @@ this.#cacheEntryState = new Map< | ||
this.#lruCache = new LruCacheImpl<CacheKeyRegistry, Key>(this.#lruPolicy); | ||
this.#txCommitLockOwner = null; | ||
this.#txCommitLockQueue = []; | ||
} | ||
@@ -130,3 +154,3 @@ | ||
const entityRevision = { | ||
entity: value as CacheKeyValue, | ||
entity: value, | ||
revision: ++revisionCounter, | ||
@@ -144,38 +168,2 @@ }; | ||
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); | ||
} | ||
} | ||
} | ||
/** | ||
@@ -196,9 +184,7 @@ Generator function for async iterable that yields iterable cache entries. This | ||
// value is actually present, | ||
if (!valueRef) { | ||
throw new Error('ref is undefined'); | ||
if (valueRef) { | ||
const state = this.#cacheEntryState.get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
const state = this.#cacheEntryState.get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, valueRef, state]; | ||
} | ||
@@ -220,7 +206,7 @@ } | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>> { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator]( | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>> { | ||
for (const revision of revisions) { | ||
@@ -254,6 +240,175 @@ yield revision; | ||
#deferTxLock( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) { | ||
let resolveTxLock; | ||
let rejectTxLock; | ||
const promiseTxLock = new Promise((resolve, reject) => { | ||
resolveTxLock = resolve; | ||
rejectTxLock = reject; | ||
}); | ||
return { | ||
resolve: resolveTxLock as unknown as () => void, | ||
reject: rejectTxLock as unknown as () => void, | ||
promise: promiseTxLock, | ||
owner: transaction, | ||
}; | ||
} | ||
#aquireTxCommitLock( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
): Promise<unknown> { | ||
if (this.#txCommitLockOwner === null) { | ||
this.#txCommitLockOwner = transaction; | ||
return new Promise((resolve) => { | ||
// start a timeout to ensure tx cannot hold on to the lock indefinitely | ||
resolve(this.#txCommitLockOwner); | ||
setTimeout(() => { | ||
// if transaction is still locked after 3 seconds then release the lock | ||
this.#releaseTxCommitLock(transaction); | ||
}, 3000); | ||
return; | ||
}); | ||
} | ||
let deferredTransactionLock = this.#deferTxLock(transaction); | ||
this.#txCommitLockQueue.push(deferredTransactionLock); | ||
return deferredTransactionLock.promise; | ||
} | ||
#releaseTxCommitLock( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
): void { | ||
//assert(this.#txCommitLockOwner === transaction, 'transaction owner incorrectly assigned when releasing lock'); | ||
if (this.#txCommitLockOwner === transaction) { | ||
this.#txCommitLockOwner = null; | ||
} | ||
} | ||
#commitUpdatesAndReleaseLock( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
txUpdates: TransactionUpdates<CacheKeyRegistry, Key, UserExtensionData> | ||
): void { | ||
assert( | ||
this.#txCommitLockOwner === transaction, | ||
'transaction owner incorrectly assigned when commiting updates' | ||
); | ||
// Write transaction entries to the main cache | ||
for (const entry of txUpdates.entries) { | ||
const [key, value, state] = entry as CacheEntry< | ||
CacheKeyRegistry, | ||
Key, | ||
UserExtensionData | ||
>; | ||
// TODO: finalizregistry | ||
this.#weakCache.set(key, new WeakRef(value)); | ||
this.#cacheEntryState.set(key, state); | ||
if (state?.retained.lru) { | ||
this.#lruCache.set(key, value); | ||
} | ||
} | ||
// Write transaction revisions entries to the main cache | ||
for (const [cacheKey, revision] of txUpdates.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); | ||
} | ||
} | ||
this.#releaseTxCommitLock(transaction); | ||
if (this.#txCommitLockQueue.length > 0) { | ||
const waitingTxDeferred = | ||
this.#txCommitLockQueue.shift() as DeferredTransactionLock< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
if (waitingTxDeferred) { | ||
//this.#txCommitLockOwner = null; | ||
this.#txCommitLockOwner = | ||
waitingTxDeferred.owner as LiveCacheTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
} | ||
waitingTxDeferred.resolve(); | ||
} | ||
} | ||
async beginTransaction(): Promise< | ||
LiveCacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
> { | ||
return await LiveCacheTransactionImpl.beginLiveTransaction(this); | ||
const aquireTxCommitLock = ( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) => this.#aquireTxCommitLock(transaction); | ||
const releaseTxCommitLock = ( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) => this.#releaseTxCommitLock(transaction); | ||
const commitUpdatesAndReleaseLock = ( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
txUpdates: TransactionUpdates<CacheKeyRegistry, Key, UserExtensionData> | ||
) => this.#commitUpdatesAndReleaseLock(transaction, txUpdates); | ||
return new LiveCacheTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>(this, { | ||
aquireTxCommitLock, | ||
releaseTxCommitLock, | ||
commitUpdatesAndReleaseLock, | ||
}); | ||
} | ||
@@ -277,3 +432,2 @@ } | ||
#transactionalCache: Map<Key, CacheKeyRegistry[Key]>; | ||
#localUpdatedEntries: Map<Key, CacheKeyRegistry[Key]>; | ||
#commitingTransaction: CommittingTransactionImpl< | ||
@@ -289,23 +443,33 @@ CacheKeyRegistry, | ||
#lruPolicy: number; | ||
#localRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
#entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
#localRevisions: Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]>; | ||
#entryRevisions: Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]>; | ||
#transactionOperations: TransactionOperations< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
constructor( | ||
originalCache: CacheImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData>, | ||
transactionalCacheEntryMap: Map<Key, CacheKeyRegistry[Key]>, | ||
entryRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]> | ||
transactionOperations: TransactionOperations< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) { | ||
this.#originalCacheReference = originalCache; | ||
this.#transactionalCache = transactionalCacheEntryMap; | ||
this.#localUpdatedEntries = new Map<Key, CacheKeyRegistry[Key]>(); | ||
this.#transactionalCache = new Map<Key, CacheKeyRegistry[Key]>(); | ||
this.#cacheEntryState = new Map<Key, CacheEntryState<UserExtensionData>>(); | ||
this.#ttlPolicy = DEFAULT_EXPIRATION.ttl; | ||
this.#lruPolicy = DEFAULT_EXPIRATION.lru; | ||
this.#localRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
>(); | ||
this.#entryRevisions = entryRevisions; | ||
this.#entryRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
>(); | ||
this.#userOptionRetentionPolicy = | ||
@@ -337,68 +501,68 @@ this.#originalCacheReference.getCacheOptions()?.expiration || | ||
>(); | ||
this.#transactionOperations = transactionOperations; | ||
} | ||
static async beginLiveTransaction< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
>( | ||
originalCache: CacheImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
) { | ||
const transactionalCache = new Map<Key, CacheKeyRegistry[Key]>(); | ||
const entryRevisions = new Map< | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
>(); | ||
for await (const [key, value] of originalCache.entries()) { | ||
transactionalCache.set(key, { ...value }); | ||
for await (const entryRevision of originalCache.entryRevisions(key)) { | ||
entryRevisions.set(key, [entryRevision]); | ||
} | ||
async *[Symbol.asyncIterator](): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
for await (const [key, value] of this.localEntries()) { | ||
const state = this.#cacheEntryState.get( | ||
key | ||
) as CacheEntryState<UserExtensionData>; | ||
yield [key, value, state]; | ||
} | ||
return new LiveCacheTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>(originalCache, transactionalCache, entryRevisions); | ||
} | ||
get(cacheKey: Key): CacheKeyRegistry[Key] | undefined { | ||
const cacheValue = this.#transactionalCache.get(cacheKey); | ||
async get(cacheKey: Key): Promise<CacheKeyRegistry[Key] | undefined> { | ||
// will check the transaction entries and fall back to the cache if the transaction hasn't written to the key yet. | ||
let cachedValue; | ||
if (cacheValue) { | ||
// Update cache entry state | ||
this.#cacheEntryState.set(cacheKey, { | ||
retained: { lru: true, ttl: this.#ttlPolicy }, | ||
lastAccessed: Date.now(), | ||
}); | ||
for await (const [key, value] of this.localEntries()) { | ||
if (key === cacheKey) { | ||
cachedValue = value; | ||
break; | ||
} | ||
} | ||
return cacheValue; | ||
return cachedValue || (await this.#originalCacheReference.get(cacheKey)); | ||
} | ||
async *[Symbol.asyncIterator]( | ||
entryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
for (const [key, value] of entryMap) { | ||
const state = this.#cacheEntryState.get(key) || DEFAULT_ENTRY_STATE; | ||
yield [key, value, state]; | ||
} | ||
localEntries(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> { | ||
const localEntriesIterator = { | ||
async *[Symbol.asyncIterator]( | ||
localEntryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> { | ||
for (const [key, value] of localEntryMap) { | ||
yield [key, value]; | ||
} | ||
}, | ||
}; | ||
return localEntriesIterator[Symbol.asyncIterator](this.#transactionalCache); | ||
} | ||
entries(): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
async entries(): Promise< | ||
AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> | ||
> { | ||
return this[Symbol.asyncIterator](this.#transactionalCache); | ||
} | ||
const entriesIterator = { | ||
async *[Symbol.asyncIterator]( | ||
localEntriesIterator: AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key]] | ||
>, | ||
cacheRef: CacheImpl<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]> { | ||
for await (const [key, transactionValue] of localEntriesIterator) { | ||
yield [key, transactionValue]; | ||
const cacheValue = await cacheRef.get(key); | ||
if (cacheValue) { | ||
yield [key, cacheValue]; | ||
} | ||
} | ||
}, | ||
}; | ||
localEntries(): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
> { | ||
return this[Symbol.asyncIterator](this.#localUpdatedEntries); | ||
return entriesIterator[Symbol.asyncIterator]( | ||
this.localEntries(), | ||
this.#originalCacheReference | ||
); | ||
} | ||
@@ -408,7 +572,7 @@ | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>> { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator]( | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>> { | ||
for (const revision of revisions) { | ||
@@ -426,7 +590,7 @@ yield revision; | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>> { | ||
const entryRevisionIterator = { | ||
async *[Symbol.asyncIterator]( | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>> { | ||
revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>> { | ||
for (const revision of revisions) { | ||
@@ -446,5 +610,7 @@ yield revision; | ||
set(cacheKey: Key, value: CacheKeyRegistry[Key]): CacheKeyRegistry[Key] { | ||
async set( | ||
cacheKey: Key, | ||
value: CacheKeyRegistry[Key] | ||
): Promise<CacheKeyRegistry[Key]> { | ||
this.#transactionalCache.set(cacheKey, value); | ||
this.#localUpdatedEntries.set(cacheKey, value); | ||
@@ -457,60 +623,92 @@ // Update cache entry state | ||
return value; | ||
return (await this.get(cacheKey)) as CacheKeyRegistry[Key]; | ||
} | ||
async delete(cacheKey: Key): Promise<boolean> { | ||
return new Promise((resolve) => { | ||
if (this.#transactionalCache.has(cacheKey)) { | ||
this.#transactionalCache.delete(cacheKey); | ||
} | ||
// tx.delete will actually need to write a tombstone in the transaction entries and the actual delete will occur when the transaction is committed to the cache. | ||
// The semantics of tx.delete's return value should be "did i delete something?" | ||
if (await this.get(cacheKey)) { | ||
this.#transactionalCache.delete(cacheKey); | ||
if (this.#localUpdatedEntries.has(cacheKey)) { | ||
this.#localUpdatedEntries.delete(cacheKey); | ||
} | ||
// Update cache entry state to indicate as delete in order to actually be deleted from cache when commit | ||
this.#cacheEntryState.set(cacheKey, { | ||
retained: { lru: false, ttl: 0 }, | ||
deletedRecordInTransaction: true, | ||
lastAccessed: Date.now(), | ||
}); | ||
return true; | ||
} | ||
return resolve( | ||
this.#transactionalCache.has(cacheKey) === false && | ||
this.#localUpdatedEntries.has(cacheKey) === false | ||
); | ||
}); | ||
return false; | ||
} | ||
// assign transaction or cache level overriden merge strategy else use default | ||
#getMergeStrategy( | ||
transactionMergeStrategy?: EntityMergeStrategy< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) { | ||
const cacheWideMergeStrategy = | ||
this.#originalCacheReference.getCacheOptions()?.hooks | ||
?.entitymergeStrategy; | ||
return ( | ||
transactionMergeStrategy || cacheWideMergeStrategy || defaultMergeStrategy | ||
); | ||
} | ||
#getRevisionStrategy() { | ||
const cacheWideRevisionStrategy = | ||
this.#originalCacheReference.getCacheOptions()?.hooks | ||
?.revisionMergeStrategy; | ||
return cacheWideRevisionStrategy || defaultRevisionStrategy; | ||
} | ||
async merge( | ||
cacheKey: Key, | ||
entityRevision: CachedEntityRevision<CacheKeyValue>, | ||
entity: CacheKeyRegistry[Key], | ||
options?: { | ||
entityMergeStrategy: EntityMergeStrategy< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
revisionContext: string; | ||
$debug: $Debug; | ||
} | ||
): Promise<CacheKeyRegistry[Key] | CacheKeyValue> { | ||
// assign custom merge strategy if specified else use default | ||
const mergeStrategyFromCacheOptionHook = | ||
this.#originalCacheReference.getCacheOptions()?.hooks | ||
?.entitymergeStrategy; | ||
const mergeStrategy = | ||
mergeStrategyFromCacheOptionHook || defaultMergeStrategy; | ||
const mergeStrategy = this.#getMergeStrategy(options?.entityMergeStrategy); | ||
// get current cache value within this transaction | ||
const currentValue = this.#transactionalCache.get(cacheKey); | ||
const currentValue = await this.#originalCacheReference.get(cacheKey); | ||
const revisionCounter = REVISION_COUNTER++; | ||
const mergedEntity = mergeStrategy( | ||
cacheKey, | ||
{ | ||
entity: entityRevision.entity, | ||
revision: entityRevision.revision, | ||
revisionContext: entityRevision?.revisionContext, | ||
}, | ||
currentValue, | ||
this | ||
); | ||
// get merged entity | ||
const mergedEntity = currentValue | ||
? entity | ||
: mergeStrategy( | ||
cacheKey, | ||
{ | ||
entity, | ||
revision: REVISION_COUNTER++, | ||
revisionContext: options?.revisionContext, | ||
}, | ||
currentValue, | ||
this | ||
); | ||
// TODO throw error if Merge entity is undefined | ||
// Update transactional cache with merged entity | ||
this.set(cacheKey, mergedEntity as CacheKeyRegistry[Key]); | ||
// Calling set here will in turn also update cacheEntryState | ||
await this.set(cacheKey, mergedEntity as CacheKeyRegistry[Key]); | ||
// Update local & entry revisions with new revision values | ||
const revision = { | ||
entity: mergedEntity, | ||
revision: entityRevision.revision, | ||
revisionContext: entityRevision?.revisionContext, | ||
entity: mergedEntity as CacheKeyRegistry[Key], | ||
revision: revisionCounter, | ||
revisionContext: options?.revisionContext, | ||
}; | ||
@@ -526,105 +724,90 @@ if (this.#localRevisions.has(cacheKey)) { | ||
async commit(options?: { timeout: number | false }): Promise<void> { | ||
const timeout: number = options?.timeout ? options.timeout : 10000; | ||
const commitLock = new Promise((resolve, reject) => | ||
setTimeout(reject, timeout) | ||
); | ||
const writeToCache = async () => { | ||
const trasactionCacheEntries: [ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | undefined | ||
][] = []; | ||
async #prepareTransaction(): Promise< | ||
TransactionUpdates<CacheKeyRegistry, Key, UserExtensionData> | ||
> { | ||
const trasactionCacheEntries: [ | ||
Key, | ||
CacheKeyRegistry[Key], | ||
CacheEntryState<UserExtensionData> | ||
][] = []; | ||
for await (const [cacheKey, value, state] of this.localEntries()) { | ||
const latestCacheValue = await this.#originalCacheReference.get( | ||
cacheKey | ||
for await (const [cacheKey, value] of this.localEntries()) { | ||
const latestCacheValue = await this.#originalCacheReference.get(cacheKey); | ||
let mergedEntityToCommit; | ||
const mergeStrategy = this.#getMergeStrategy(); | ||
if (latestCacheValue) { | ||
// TODO fix revision | ||
mergedEntityToCommit = mergeStrategy( | ||
cacheKey, | ||
{ entity: value, revision: 3 }, | ||
latestCacheValue, | ||
this | ||
); | ||
let entityToCommit; | ||
} else { | ||
mergedEntityToCommit = value; | ||
} | ||
const structuredClonedValue = structuredClone( | ||
mergedEntityToCommit | ||
) as CacheKeyRegistry[Key]; | ||
// assign custom merge strategy if specified else use default | ||
const mergeStrategyFromCacheOptionHook = | ||
this.#originalCacheReference.getCacheOptions()?.hooks | ||
?.entitymergeStrategy; | ||
const mergeStrategy = | ||
mergeStrategyFromCacheOptionHook || defaultMergeStrategy; | ||
const state = this.#cacheEntryState.get(cacheKey) || DEFAULT_ENTRY_STATE; | ||
if (latestCacheValue) { | ||
// TODO fix revision | ||
entityToCommit = mergeStrategy( | ||
cacheKey, | ||
{ entity: value as CacheKeyValue, revision: 3 }, | ||
latestCacheValue, | ||
this | ||
); | ||
} else { | ||
entityToCommit = value; | ||
} | ||
const structuredClonedValue = structuredClone( | ||
entityToCommit | ||
) as CacheKeyRegistry[Key]; | ||
trasactionCacheEntries.push([cacheKey, structuredClonedValue, state]); | ||
trasactionCacheEntries.push([cacheKey, structuredClonedValue, state]); | ||
// Update saved revisions of the entity | ||
const localRevisions = this.#localRevisions.get(cacheKey); | ||
let revisionNumber = | ||
localRevisions && localRevisions[localRevisions.length - 1].revision | ||
? localRevisions[localRevisions.length - 1].revision | ||
: 0; | ||
// Update saved revisions of the entity | ||
const localRevisions = this.#localRevisions.get(cacheKey); | ||
let revisionNumber = | ||
localRevisions && localRevisions[localRevisions.length - 1].revision | ||
? localRevisions[localRevisions.length - 1].revision | ||
: 0; | ||
const entityRevision = { | ||
entity: mergedEntityToCommit as CacheKeyRegistry[Key], | ||
revision: ++revisionNumber, | ||
}; | ||
if (this.#localRevisions.has(cacheKey)) { | ||
this.#localRevisions.get(cacheKey)?.push(entityRevision); | ||
} else { | ||
this.#localRevisions.set(cacheKey, [entityRevision]); | ||
} | ||
const entityRevision = { | ||
entity: entityToCommit as CacheKeyValue, | ||
revision: ++revisionNumber, | ||
}; | ||
if (this.#localRevisions.has(cacheKey)) { | ||
this.#localRevisions.get(cacheKey)?.push(entityRevision); | ||
} else { | ||
this.#localRevisions.set(cacheKey, [entityRevision]); | ||
} | ||
const revisionStrategy = this.#getRevisionStrategy(); | ||
const revisionStrategy = this.#originalCacheReference.getCacheOptions() | ||
?.hooks?.revisionMergeStrategy | ||
? async ( | ||
id: Key, | ||
commitTx: CommittingTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
liveTx: LiveCacheTransactionImpl< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) => | ||
this.#originalCacheReference.getCacheOptions()?.hooks | ||
?.revisionMergeStrategy | ||
: defaultRevisionStrategy; | ||
// Update revisions based on revision strategy | ||
await revisionStrategy(cacheKey, this.#commitingTransaction, this); | ||
} | ||
// Update revisions based on revision strategy | ||
await revisionStrategy(cacheKey, this.#commitingTransaction, this); | ||
} | ||
// Call commit hook to apply custom retention policies before commit (if passed by cache options) | ||
const commitCallback = | ||
this.#originalCacheReference.getCacheOptions()?.hooks?.commit; | ||
if (commitCallback) { | ||
await commitCallback(this); | ||
} | ||
// Call commit hook to apply custom retention policies before commit (if passed by cache options) | ||
const customRetentionPolicy = | ||
this.#originalCacheReference.getCacheOptions()?.hooks?.commit; | ||
if (customRetentionPolicy) { | ||
customRetentionPolicy(this); | ||
} | ||
const mergedRevisions = this.#commitingTransaction.mergedRevisions(); | ||
return { | ||
entries: trasactionCacheEntries, | ||
entryRevisions: mergedRevisions, | ||
}; | ||
} | ||
const mergedRevisions = this.#commitingTransaction.mergedRevisions(); | ||
async commit(options?: { timeout: number | false }): Promise<void> { | ||
await this.#transactionOperations.aquireTxCommitLock(this); | ||
// commit merged transaction & revisions entries to main cache | ||
await this.#originalCacheReference.commitTransaction( | ||
trasactionCacheEntries, | ||
mergedRevisions | ||
let transactionUpdates; | ||
try { | ||
transactionUpdates = await this.#prepareTransaction(); | ||
return this.#transactionOperations.commitUpdatesAndReleaseLock( | ||
this, | ||
transactionUpdates | ||
); | ||
}; | ||
await Promise.race([writeToCache(), commitLock]); | ||
} catch (e) { | ||
throw new Error('Failed to prepare transaction updates'); | ||
} finally { | ||
this.#transactionOperations.releaseTxCommitLock(this); | ||
} | ||
} | ||
} | ||
class CommittingTransactionImpl< | ||
@@ -639,4 +822,3 @@ CacheKeyRegistry extends DefaultRegistry, | ||
$debug?: ($Debug & CacheTransactionDebugAPIs) | undefined; | ||
#mergedRevisions: Map<Key, CachedEntityRevision<CacheKeyValue>[]>; | ||
#mergedRevisions: Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]>; | ||
cache: { | ||
@@ -660,3 +842,3 @@ clearRevisions( | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
): void; | ||
@@ -684,3 +866,3 @@ } = { | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
): void { | ||
@@ -700,9 +882,7 @@ if (tx.#mergedRevisions.has(id)) { | ||
Key, | ||
CachedEntityRevision<CacheKeyValue>[] | ||
CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
>(); | ||
} | ||
[Symbol.asyncIterator]( | ||
entryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator< | ||
[Symbol.asyncIterator](): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
@@ -713,3 +893,3 @@ > { | ||
mergedRevisions(): Map<Key, CachedEntityRevision<CacheKeyValue>[]> { | ||
mergedRevisions(): Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]> { | ||
return this.#mergedRevisions; | ||
@@ -774,7 +954,7 @@ } | ||
id: Key, | ||
{ entity, revision }: CachedEntityRevision<CacheKeyValue>, | ||
{ entity }: CachedEntityRevision<CacheKeyRegistry, Key>, | ||
current: CacheKeyRegistry[Key] | undefined, | ||
tx: CacheTransaction<CacheKeyRegistry, Key> | ||
): CacheKeyValue { | ||
return deepMerge(current as CacheKeyValue, entity); | ||
return deepMerge(current as CacheKeyValue, entity as CacheKeyValue); | ||
}; | ||
@@ -802,3 +982,3 @@ | ||
): Promise<void> { | ||
const revisions: CachedEntityRevision<CacheKeyValue>[] = []; | ||
const revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] = []; | ||
@@ -869,1 +1049,16 @@ for await (const revision of liveTx.localRevisions(id)) { | ||
} | ||
export function assert<T>( | ||
value: T, | ||
message: string | (() => string) | ||
): asserts value { | ||
if (!value) { | ||
if (typeof message === 'string') { | ||
throw new Error(`[@data-eden/cache] internal error: ${message}`); | ||
} | ||
if (typeof message === 'function') { | ||
throw new Error(`[@data-eden/cache] internal error: ${message()}`); | ||
} | ||
} | ||
} |
@@ -19,2 +19,5 @@ export { buildCache } from './cache.js'; | ||
CacheTransactionDebugAPIs, | ||
TransactionUpdates, | ||
TransactionOperations, | ||
DeferredTransactionLock, | ||
} from '../types/api.js'; |
@@ -70,3 +70,3 @@ export { buildCache } from '../src/cache.js'; | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>>; | ||
@@ -103,7 +103,5 @@ /** | ||
*/ | ||
get(cacheKey: Key): CacheKeyRegistry[Key] | CacheKeyValue | undefined; | ||
get(cacheKey: Key): Promise<CacheKeyRegistry[Key] | undefined>; | ||
[Symbol.asyncIterator]( | ||
entryMap: Map<Key, CacheKeyRegistry[Key]> | ||
): AsyncIterableIterator< | ||
[Symbol.asyncIterator](): AsyncIterableIterator< | ||
[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>] | ||
@@ -115,9 +113,3 @@ >; | ||
*/ | ||
entries(): AsyncIterableIterator< | ||
[ | ||
Key, | ||
CacheKeyRegistry[Key] | CacheKeyValue, | ||
CacheEntryState<UserExtensionData> | ||
] | ||
>; | ||
entries(): Promise<AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]>>; | ||
@@ -127,9 +119,3 @@ /** | ||
*/ | ||
localEntries(): AsyncIterableIterator< | ||
[ | ||
Key, | ||
CacheKeyRegistry[Key] | CacheKeyValue, | ||
CacheEntryState<UserExtensionData> | ||
] | ||
>; | ||
localEntries(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key]]>; | ||
@@ -141,3 +127,3 @@ /** | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>>; | ||
@@ -151,3 +137,3 @@ /** | ||
cacheKey: Key | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyValue>>; | ||
): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry, Key>>; | ||
@@ -171,5 +157,12 @@ $debug?: $Debug & CacheTransactionDebugAPIs; | ||
cacheKey: Key, | ||
value: CachedEntityRevision<CacheKeyValue>, | ||
entity: CacheKeyRegistry[Key], | ||
options?: { | ||
$debug: $Debug; | ||
entityMergeStrategy?: EntityMergeStrategy< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>; | ||
revisionContext?: string; | ||
$debug?: $Debug; | ||
} | ||
@@ -184,3 +177,3 @@ ): Promise<CacheKeyRegistry[Key] | CacheKeyValue>; | ||
value: CacheKeyRegistry[Key] | CacheKeyValue | ||
): CacheKeyRegistry[Key] | CacheKeyValue; | ||
): Promise<CacheKeyRegistry[Key] | CacheKeyValue>; | ||
@@ -225,5 +218,6 @@ /** | ||
id: Key, | ||
revisions: CachedEntityRevision<CacheKeyValue>[] | ||
revisions: CachedEntityRevision<CacheKeyRegistry, Key>[] | ||
): void; | ||
}; | ||
mergedRevisions(): Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]>; | ||
} | ||
@@ -261,2 +255,6 @@ | ||
lastAccessed?: number; // timestamp | ||
/** | ||
If the cache entry is tombstone to be deleted | ||
*/ | ||
deletedRecordInTransaction?: boolean; | ||
extensions?: UserExtensionData; | ||
@@ -278,3 +276,3 @@ } | ||
cacheKey: Key, | ||
newEntityRevision: CachedEntityRevision<CacheKeyValue>, | ||
newEntityRevision: CachedEntityRevision<CacheKeyRegistry, Key>, | ||
current: CacheKeyRegistry[Key] | undefined, | ||
@@ -296,4 +294,7 @@ tx: CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
export interface CachedEntityRevision<CacheKeyValue> { | ||
entity: CacheKeyValue; | ||
export interface CachedEntityRevision< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry | ||
> { | ||
entity: CacheKeyRegistry[Key]; | ||
revision: number; | ||
@@ -325,3 +326,3 @@ revisionContext?: string; // Use to store queryIds that can be used for debugging | ||
tx: CacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData> | ||
) => void; | ||
) => Promise<void>; | ||
@@ -366,2 +367,45 @@ /** | ||
export interface TransactionUpdates< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
UserExtensionData = unknown | ||
> { | ||
entries: [Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>][]; | ||
entryRevisions: Map<Key, CachedEntityRevision<CacheKeyRegistry, Key>[]>; | ||
} | ||
export interface TransactionOperations< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
aquireTxCommitLock: ( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) => Promise<unknown>; | ||
releaseTxCommitLock: ( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
> | ||
) => void; | ||
commitUpdatesAndReleaseLock: ( | ||
transaction: LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key, | ||
$Debug, | ||
UserExtensionData | ||
>, | ||
txUpdates: TransactionUpdates<CacheKeyRegistry, Key, UserExtensionData> | ||
) => void; | ||
} | ||
export interface CacheTransactionDebugAPIs { | ||
@@ -372,2 +416,14 @@ size(): void; | ||
export interface DeferredTransactionLock< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
resolve: () => void; | ||
reject: () => void; | ||
promise: Promise<unknown>; | ||
owner: LiveCacheTransaction<CacheKeyRegistry, Key, $Debug, UserExtensionData>; | ||
} | ||
/** | ||
@@ -374,0 +430,0 @@ * LRU Cache |
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
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
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
2502
0
383472
20