@data-eden/cache
Advanced tools
Comparing version 0.2.1 to 0.2.2-beta.1
import { describe, it, expect } from 'vitest'; | ||
// TODO: add a tests tsconfig so we can import properly | ||
import { buildCache } from '@data-eden/cache'; | ||
// TODO: add tests for types | ||
// TODO test live trasaction where original cache has enitiy that is GCd (memory management tests) | ||
describe('@data-eden/cache', function () { | ||
it('can run tests', function () { | ||
expect(true).toBe(true); | ||
describe('cache with no user registry', function () { | ||
it('can be built', async function () { | ||
// TODO: this valid call fails if we switch module resolution to node16 | ||
// see #36 | ||
let cache = buildCache(); | ||
expect(await cache.get('missing-key')).toBeUndefined(); | ||
}); | ||
it('can load serialized values', async function () { | ||
let cache = buildCache(); | ||
// without a serializer, cache.load assumes serialized entries have values that are structured-cloneable | ||
// TODO: update to put these in the LRU | ||
await cache.load([ | ||
['book:1', { title: 'A History of the English speaking peoples' }], | ||
['book:2', { title: 'Marlborough: his life and times' }], | ||
]); | ||
let book1 = await cache.get('book:1'); | ||
expect(book1).toMatchInlineSnapshot(` | ||
{ | ||
"title": "A History of the English speaking peoples", | ||
} | ||
`); | ||
}); | ||
it('test iterable cache.entries', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
['book:1', { title: 'A History of the English speaking peoples' }], | ||
['book:2', { title: 'Marlborough: his life and times' }], | ||
]); | ||
const entries = cache.entries(); | ||
const entry1 = await entries.next(); | ||
const defaultEntryState = { retained: { lru: false, ttl: 60000 } }; | ||
// TODO setup & validate weekly held and strongly held entries | ||
expect(entry1.value).toEqual([ | ||
'book:1', | ||
{ title: 'A History of the English speaking peoples' }, | ||
defaultEntryState, | ||
]); | ||
const entry2 = await entries.next(); | ||
expect(entry2.value).toEqual([ | ||
'book:2', | ||
{ title: 'Marlborough: his life and times' }, | ||
defaultEntryState, | ||
]); | ||
for await (const [key, value] of cache.entries()) { | ||
expect(key).toBeTypeOf('string'); | ||
expect(value).toBeTypeOf('object'); | ||
} | ||
}); | ||
it('test keys iterator', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
['book:1', { title: 'A History of the English speaking peoples' }], | ||
['book:2', { title: 'Marlborough: his life and times' }], | ||
]); | ||
const entryKeys = cache.keys(); | ||
const entryKey1 = await entryKeys.next(); | ||
expect(entryKey1.value).toEqual('book:1'); | ||
const entryKey2 = await entryKeys.next(); | ||
expect(entryKey2.value).toEqual('book:2'); | ||
for await (const key of cache.keys()) { | ||
expect(key).toBeTypeOf('string'); | ||
} | ||
}); | ||
it('test values iterator', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
['book:1', { title: 'A History of the English speaking peoples' }], | ||
['book:2', { title: 'Marlborough: his life and times' }], | ||
]); | ||
const entryValues = cache.values(); | ||
const entryValue1 = await entryValues.next(); | ||
expect(entryValue1.value).toEqual({ | ||
title: 'A History of the English speaking peoples', | ||
}); | ||
const entryValue2 = await entryValues.next(); | ||
expect(entryValue2.value).toEqual({ | ||
title: 'Marlborough: his life and times', | ||
}); | ||
for await (const value of cache.values()) { | ||
expect(value).toBeTypeOf('object'); | ||
} | ||
}); | ||
it('test cache.save returns array of cache entry tuple', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
['book:1', { title: 'A History of the English speaking peoples' }], | ||
['book:2', { title: 'Marlborough: his life and times' }], | ||
]); | ||
const arrayOfCacheEntryTuples = await cache.save(); | ||
const cacheEntryTuple1 = arrayOfCacheEntryTuples[0]; | ||
const cacheEntryTuple2 = arrayOfCacheEntryTuples[1]; | ||
expect(arrayOfCacheEntryTuples.length).toEqual(2); | ||
expect(cacheEntryTuple1?.length).toEqual(3); | ||
expect(cacheEntryTuple1[0]).toEqual('book:1'); | ||
expect(cacheEntryTuple1[0]).toBeTypeOf('string'); | ||
expect(cacheEntryTuple1[1]).toEqual({ | ||
title: 'A History of the English speaking peoples', | ||
}); | ||
expect(cacheEntryTuple1[1]).toBeTypeOf('object'); | ||
expect(cacheEntryTuple2?.length).toEqual(3); | ||
expect(cacheEntryTuple2[0]).toEqual('book:2'); | ||
expect(cacheEntryTuple2[0]).toBeTypeOf('string'); | ||
expect(cacheEntryTuple2[1]).toEqual({ | ||
title: 'Marlborough: his life and times', | ||
}); | ||
expect(cacheEntryTuple2[1]).toBeTypeOf('object'); | ||
// TODO verify cache entry state | ||
}); | ||
it('test cache.load w/o serializer throws error when values are not structured clonable', async () => { | ||
let cache = buildCache(); | ||
void expect(async () => { | ||
await cache.load([['book:1', function () {}]]); | ||
}).rejects.toThrow( | ||
'The cache value is not structured clonable use `save` with serializer' | ||
); | ||
}); | ||
it('test cache.clear (load, get, clear, get)', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
['book:1', { title: 'A History of the English speaking peoples' }], | ||
['book:2', { title: 'Marlborough: his life and times' }], | ||
]); | ||
expect(await cache.get('book:1')).toEqual({ | ||
title: 'A History of the English speaking peoples', | ||
}); | ||
expect(await cache.get('book:2')).toEqual({ | ||
title: 'Marlborough: his life and times', | ||
}); | ||
await cache.clear(); | ||
expect(await cache.get('book:1')).toEqual(undefined); | ||
expect(await cache.get('book:2')).toEqual(undefined); | ||
}); | ||
// 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 | ||
// TODO: requires fixing up types &c. but otherwise illustrates how a user | ||
// could have a very simple cache that differentiated between types | ||
// | ||
// it('enables custom user retention -- retention-by-type', function() { | ||
// async function awaitAll(itr: AsyncIterableIterator<unknown>) { | ||
// let result = [] | ||
// for await (let item of itr) { | ||
// result.push(item); | ||
// } | ||
// return result; | ||
// } | ||
// type CachedTypes = 'book' | 'author'; | ||
// let typeCacheMap = new Map<CachedTypes, unknown>([ | ||
// 'book', null, | ||
// 'author', null, | ||
// ]); | ||
// function typeBasedLRU(tx) { | ||
// for (let [key, value] of tx.entries()) { | ||
// let match = /(book|author):/i.exec(key); | ||
// if (match) { | ||
// // TODO: assert match[1] is a CachedTypes | ||
// typeCacheMap.set(match[1], value); | ||
// } | ||
// } | ||
// } | ||
// let cache = buildCache({ | ||
// hooks: { | ||
// commit: typeBasedLRU | ||
// } | ||
// }); | ||
// let tx = cache.beginTransaction(); | ||
// tx.set('book:1', { title: 'Marlborough: His Life and Times Volume I' }); | ||
// tx.set('book:2', { title: 'Marlborough: His Life and Times Volume II' }); | ||
// tx.set('author:1', { name: 'Winston Churchill' }); | ||
// tx.set('character:1', { name: 'John Churchill' }); | ||
// await tx.commit(); | ||
// expect([...typeCacheMap.values()]).toMatchInlineSnapshot([{ | ||
// title: 'Marlborough: His Life and Times Volume II' | ||
// }, { | ||
// name: 'Winston Churchill' | ||
// }]); | ||
// expect(awaitAll(cache.values())).toMatchInlineSnapshot([{ | ||
// title: 'Marlborough: His Life and Times Volume I' | ||
// }, { | ||
// title: 'Marlborough: His Life and Times Volume II' | ||
// }, { | ||
// name: 'Winston Churchill' | ||
// }, { | ||
// name: 'John Churchill' | ||
// }]); | ||
// // TODO: gc? | ||
// expect(awaitAll(cache.values())).toMatchInlineSnapshot([{ | ||
// title: 'Marlborough: His Life and Times Volume II' | ||
// }, { | ||
// name: 'Winston Churchill' | ||
// }]); | ||
// }); | ||
}); | ||
describe('with a user registry', function () { | ||
// let cache = buildCache<UserRegistry>() | ||
// see https:/tsplay.dev/NrnDlN | ||
// TODO: try to test the types with expect-type | ||
it('can be built', function () {}); | ||
}); | ||
describe('with a user registry and user extension data', function () { | ||
it('can be built', function () {}); | ||
}); | ||
describe('test live transactions', function () { | ||
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 single transaction with commit & rollback', 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' }, | ||
}); | ||
await tx.rollback(); | ||
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); | ||
}); | ||
it('test cache with multiple transaction commits is masked from trasaction changes', 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 tx1 = await cache.beginTransaction(); | ||
// transaction 2 starts | ||
let tx2 = await cache.beginTransaction(); | ||
// Merge entities from transaction 1 | ||
await tx1.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book TX1' } }, | ||
revision: 1, | ||
}); | ||
await tx1.merge('book:1', { | ||
entity: { 'book:1': { title: 'original book Conflict', sub: 'j3' } }, | ||
revision: 1, | ||
}); | ||
// Merge entities from transaction 2 | ||
await tx2.merge('book:3', { | ||
entity: { 'book:3': { title: 'New Merged book by TX2' } }, | ||
revision: 1, | ||
}); | ||
await tx2.merge('book:1', { | ||
entity: { | ||
'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, | ||
}); | ||
// Validate entries in Transaction 1 | ||
expect(tx1.get('book:1')).toEqual({ | ||
'book:1': { title: 'original book Conflict', sub: 'j3' }, | ||
}); | ||
expect(tx1.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx1.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book TX1' }, | ||
}); | ||
// Validate entries in Transaction 2 | ||
expect(tx2.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict updated by TX2', sub: 'j32', sub2: '12' }, | ||
}); | ||
expect(tx2.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx2.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book by TX2' }, | ||
}); | ||
expect(tx2.get('book:4')).toEqual({ | ||
'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' }, | ||
}); | ||
// Validate entries in original Cache | ||
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); | ||
// commit transaction 1 | ||
await tx1.commit(); | ||
// Validate entries in original Cache after 1st commit | ||
expect(await cache.get('book:1')).toEqual({ | ||
'book:1': { title: 'original book 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 TX1' }, | ||
}); | ||
expect(await cache.get('book:4')).toEqual(undefined); | ||
// Validate entries in Transaction 2 Cache after 1st transaction commit and it remains masked | ||
expect(tx2.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict updated by TX2', sub: 'j32', sub2: '12' }, | ||
}); | ||
expect(tx2.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx2.get('book:3')).toEqual({ | ||
'book:3': { title: 'New Merged book by TX2' }, | ||
}); | ||
expect(tx2.get('book:4')).toEqual({ | ||
'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' }, | ||
}); | ||
// commit transaction 1 | ||
await tx2.commit(); | ||
// Validate entries in original Cache after 2nd commit | ||
expect(await cache.get('book:1')).toEqual({ | ||
'book:1': { title: 'Conflict updated by TX2', sub: 'j32', sub2: '12' }, | ||
}); | ||
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 by TX2' }, | ||
}); | ||
expect(await cache.get('book:4')).toEqual({ | ||
'book:4': { title: 'new book 4', sub: 'j32', sub2: '12' }, | ||
}); | ||
}); | ||
it('test local entries', 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' } }], | ||
]); | ||
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, | ||
}); | ||
const localEntries = []; | ||
for await (const [key, value, state] of tx.localEntries()) { | ||
localEntries.push([key, value, state]); | ||
} | ||
expect(localEntries[0][1]).toEqual({ | ||
'book:3': { title: 'New Merged book' }, | ||
}); | ||
expect(localEntries[1][1]).toEqual({ | ||
'book:1': { title: 'Conflict', sub: 'j3' }, | ||
}); | ||
}); | ||
it('test merging entities with array values', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
[ | ||
'book:1', | ||
{ | ||
'book:1': { | ||
title: 'A History of the English speaking peoples', | ||
subjects: [{ a: 1 }], | ||
}, | ||
}, | ||
], | ||
['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', | ||
subjects: [{ a: 1 }, { b: 2 }], | ||
}, | ||
}, | ||
revision: 1, | ||
}); | ||
// Validate Transactional entries | ||
expect(tx.get('book:1')).toEqual({ | ||
'book:1': { | ||
title: 'Conflict', | ||
sub: 'j3', | ||
subjects: [{ a: 1 }, { b: 2 }], | ||
}, | ||
}); | ||
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', | ||
subjects: [{ a: 1 }], | ||
}, | ||
}); | ||
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', | ||
subjects: [{ a: 1 }, { b: 2 }], | ||
}, | ||
}); | ||
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 delete', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
[ | ||
'book:1', | ||
{ | ||
'book:1': { | ||
title: 'A History of the English speaking peoples', | ||
subjects: [{ a: 1 }], | ||
}, | ||
}, | ||
], | ||
['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.delete('book:1'); | ||
expect(tx.get('book:2')).toEqual({ | ||
'book:2': { title: 'Marlborough: his life and times' }, | ||
}); | ||
expect(tx.get('book:1')).toEqual(undefined); | ||
}); | ||
}); | ||
describe('test revision strategy', function () { | ||
it('test entry revisions are merged correctly', async function () { | ||
let cache = buildCache(); | ||
await cache.load([ | ||
[ | ||
'book:1', | ||
{ 'book:1': { title: 'A History of the English speaking peoples' } }, | ||
], | ||
]); | ||
let tx = await cache.beginTransaction(); | ||
await tx.merge('book:1', { | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 2, | ||
}); | ||
await tx.commit(); | ||
const entryRevisions = cache.entryRevisions('book:1'); | ||
const revisions = []; | ||
for await (const entry of entryRevisions) { | ||
revisions.push(entry); | ||
} | ||
expect(revisions.length).toEqual(3); | ||
expect( | ||
revisions.includes({ | ||
entity: { | ||
'book:1': { title: 'A History of the English speaking peoples' }, | ||
}, | ||
revision: 1, | ||
}) | ||
); | ||
expect( | ||
revisions.includes({ | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 2, | ||
}) | ||
); | ||
expect( | ||
revisions.includes({ | ||
entity: { 'book:1': { title: 'Conflict', sub: 'j3' } }, | ||
revision: 3, | ||
}) | ||
); | ||
}); | ||
}); | ||
describe('test LRU', function () { | ||
it('test LRU policy', async function () { | ||
let cache = buildCache({ expiration: { lru: 4, ttl: 5000 } }); | ||
await cache.load([ | ||
['book:1', { 'book:1': { title: 'A History1' } }], | ||
['book:2', { 'book:2': { title: 'A History2' } }], | ||
['book:3', { 'book:3': { title: 'A History3' } }], | ||
['book:4', { 'book:4': { title: 'A History4' } }], | ||
]); | ||
let tx = await cache.beginTransaction(); | ||
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.commit(); | ||
const cacheEntries = await cache.save(); | ||
const lru = cacheEntries.filter((entry) => { | ||
const entryState = entry[2] as { retained: { lru: true | false } }; | ||
if (entry[2]) { | ||
return entryState.retained?.lru === true; | ||
} | ||
}); | ||
expect(lru[0][0]).toEqual('book:1'); | ||
expect(lru[1][0]).toEqual('book:4'); | ||
expect(lru[2][0]).toEqual('book:5'); | ||
}); | ||
}); | ||
}); |
@@ -1,2 +0,2 @@ | ||
export {}; | ||
export { buildCache } from './cache.js'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,2 +0,2 @@ | ||
export {}; | ||
export { buildCache } from './cache.js'; | ||
//# sourceMappingURL=index.js.map |
{ | ||
"$schema": "https://json.schemastore.org/package.json", | ||
"name": "@data-eden/cache", | ||
"version": "0.2.1", | ||
"version": "0.2.2-beta.1", | ||
"repository": { | ||
@@ -14,4 +15,4 @@ "type": "git", | ||
"import": { | ||
"types": "./dist/index.d.ts", | ||
"default": "./dist/index.js" | ||
"types": "./src/index.ts", | ||
"default": "./src/index.ts" | ||
} | ||
@@ -23,3 +24,4 @@ } | ||
"scripts": { | ||
"test": "vitest run" | ||
"test": "vitest run", | ||
"test:debug": "node --inspect-brk --inspect ../../node_modules/.bin/vitest --threads=false" | ||
}, | ||
@@ -26,0 +28,0 @@ "dependencies": {}, |
100
README.md
@@ -70,6 +70,11 @@ # @data-eden/cache | ||
export interface Cache<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> { | ||
export interface Cache< | ||
CacheKeyRegistry extends DefaultRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
beginTransaction(): CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData>; | ||
async get<Key extends keyof CacheKeyRegistry>(cacheKey: Key): CacheKeyRegistry[Key] | undefined; | ||
async get(cacheKey: Key): CacheKeyRegistry[Key] | undefined; | ||
@@ -81,3 +86,3 @@ /** | ||
*/ | ||
[Symbol.asyncIterator]<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]> | ||
[Symbol.asyncIterator](): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]> | ||
@@ -89,6 +94,6 @@ /** | ||
*/ | ||
entries<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]> | ||
entryRevisions<Key extends keyof CacheKeyRegistry>(cacheKey: Key): AsyncIterableIterator<[entity: CacheKeyRegistry[Key], revision: number][]>; | ||
keys<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<Key> | ||
values<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<CacheKeyRegistry[Key]> | ||
entries(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]> | ||
entryRevisions(cacheKey: Key): AsyncIterableIterator<[entity: CacheKeyRegistry[Key], revision: number][]>; | ||
keys(): AsyncIterableIterator<Key> | ||
values(): AsyncIterableIterator<CacheKeyRegistry[Key]> | ||
@@ -106,3 +111,3 @@ /** | ||
*/ | ||
async save<Key extends keyof CacheKeyRegistry>(): [Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>][]; | ||
async save(): [Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>][]; | ||
@@ -122,3 +127,3 @@ /** | ||
*/ | ||
async save<Key extends keyof CacheKeyRegistry>(serializer: CacheEntrySerializer): ReturnType<CacheEntrySerializer>[]; | ||
async save(serializer: CacheEntrySerializer): ReturnType<CacheEntrySerializer>[]; | ||
@@ -131,3 +136,4 @@ /** | ||
*/ | ||
async load<Key extends keyof CacheKeyRegistry>(entries: CacheEntry<CacheKeyRegistry>[]): void; | ||
async load<Key extends keyof CacheKeyRegistry>(entries: CacheEntry<CacheKeyRegistry, Key, UserExtensionData>[]): void; | ||
// TODO: needs entries | ||
async load<Key extends keyof CacheKeyRegistry>(serializer: CacheEntrySerializer): ReturnType<CacheEntrySerializer>[]; | ||
@@ -184,11 +190,21 @@ | ||
export interface EntityMergeStrategy<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> { | ||
<Key extends keyof CacheKeyRegistry>(cacheKey: Key, newEntityRevision: CachedEntityRevision<CacheKeyRegistry[Key]>, current: CacheKeyRegistry[Key] | undefined, tx: CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData>): CacheKeyValue; | ||
export interface EntityMergeStrategy< | ||
CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
(cacheKey: Key, newEntityRevision: CachedEntityRevision<CacheKeyRegistry[Key]>, current: CacheKeyRegistry[Key] | undefined, tx: CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData>): CacheKeyValue; | ||
} | ||
export interface RevisionMergeStrategy<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> { | ||
<Key extends keyof CacheKeyRegistry>(cacheKey: Key, tx: CommittingTransaction<CacheKeyRegistry, $Debug, UserExtensionData>): void; | ||
export interface RevisionMergeStrategy< | ||
CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
(cacheKey: Key, tx: CommittingTransaction<CacheKeyRegistry, $Debug, UserExtensionData>): void; | ||
} | ||
export interface CacheEntryState<UserExtenionData=unknown> { | ||
export interface CacheEntryState<UserExtensionData=unknown> { | ||
retained: { | ||
@@ -223,3 +239,8 @@ lru: boolean; | ||
export interface CacheTransaction<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> { | ||
export interface CacheTransaction< | ||
CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> { | ||
/** | ||
@@ -231,13 +252,13 @@ Get the value of `cacheKey` in the cache. If `key` has been modified in this | ||
*/ | ||
async get<Key extends keyof CacheKeyRegistry>(cacheKey: Key): CacheKeyRegistry[Key] | undefined; | ||
localEntries<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState]> | ||
entries<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState]> | ||
async get(cacheKey: Key): CacheKeyRegistry[Key] | undefined; | ||
localEntries(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState]> | ||
entries(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState]> | ||
/** | ||
Generator function that yields each of the transaction local entries. | ||
*/ | ||
[Symbol.asyncIterator]<Key extends keyof CacheKeyRegistry>(): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]> | ||
[Symbol.asyncIterator](): AsyncIterableIterator<[Key, CacheKeyRegistry[Key], CacheEntryState<UserExtensionData>]> | ||
/** | ||
An async generator that produces the revisions of `key` within this transaction. | ||
*/ | ||
localRevisions<Key extends keyof CacheKeyRegistry>(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry[Key]>>; | ||
localRevisions(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry[Key]>>; | ||
/** | ||
@@ -248,14 +269,27 @@ An async generator that produces the complete list of revisions for `key`, | ||
*/ | ||
entryRevisions<Key extends keyof CacheKeyRegistry>(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry[Key]>>; | ||
entryRevisions(cacheKey: Key): AsyncIterableIterator<CachedEntityRevision<CacheKeyRegistry[Key]>>; | ||
$debug: $Debug & CacheTransactionDebugAPIs; | ||
} | ||
export interface CommittingTransaction<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> extends CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData> { | ||
export interface CommittingTransaction< | ||
CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> extends CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData> { | ||
cache: { | ||
clearRevisions<Key extends keyof CacheKeyRegistry>(id: Key): void; | ||
appendRevisions<Key extends keyof CacheKeyRegistry>(id: Key, revisions: CachedEntityRevision<CacheKeyRegistry[Key]>[]): void; | ||
clearRevisions(id: Key): void; | ||
appendRevisions(id: Key, revisions: CachedEntityRevision<CacheKeyRegistry[Key]>[]): void; | ||
} | ||
} | ||
export interface LiveCacheTransaction<CacheKeyRegistry, $Debug=unknown, UserExtensionData=unknown> extends CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData> { | ||
async merge<Key extends keyof CacheKeyRegistry>(cacheKey: Key, value: CachedEntityRevision<CacheKeyRegistry[Key]>, options?: { | ||
export interface LiveCacheTransaction< | ||
CacheKeyRegistry, | ||
Key extends keyof CacheKeyRegistry, | ||
$Debug = unknown, | ||
UserExtensionData = unknown | ||
> extends CacheTransaction<CacheKeyRegistry, $Debug, UserExtensionData> { | ||
// let mergedEntity = await tx.merge(id, entity, { revision, entityMergeStrategy, revisionMergeStrategy, $debug: { rawDocument } }); | ||
async merge(cacheKey: Key, value: CachedEntityRevision<CacheKeyRegistry[Key]>, options?: { | ||
entityMergeStrategy: EntityMergeStrategy<CacheKeyRegistry, $Debug, UserExtensionData>; | ||
@@ -265,4 +299,4 @@ revisionMergeStrategy: RevisionMergeStrategy<CacheKeyRegistry, $Debug, UserExtensionData>; | ||
}): Promise<CacheKeyRegistry[Key]>; | ||
async set<Key extends keyof CacheKeyRegistry>(cacheKey: Key, value: CacheKeyRegistry[Key]): Promise<CacheKeyRegistry[Key]>; | ||
async delete<Key extends keyof CacheKeyRegistry>(cacheKey: Key): Promise<boolean>; | ||
async set(cacheKey: Key, value: CacheKeyRegistry[Key]): Promise<CacheKeyRegistry[Key]>; | ||
async delete(cacheKey: Key): Promise<boolean>; | ||
@@ -425,3 +459,3 @@ /** | ||
// For example, parent === book, prop === 'author' | ||
// Because all userland calls go through GraphQL operations, we have | ||
// Because all userland calls go through Graphql operations, we have | ||
// the metadata necessary to differentiate strings from references | ||
@@ -438,3 +472,2 @@ parent[prop] = id; | ||
// override any previous item for this documentKey | ||
await tx.set(documentKey, document); | ||
@@ -452,2 +485,7 @@ await tx.commit(); | ||
// Popopulating transaction ids | ||
// entityUrn_a ... timestamp1 transactionId1 | ||
// entityUrn_a ... timestamp2 transactionId2 | ||
const defaultMergeStrategy = deepMergeStrategy; | ||
@@ -454,0 +492,0 @@ async function shallowMergeStrategy<CacheKeyRegistry>(id, { entity, revision }, current: CacheKeyValue | undefined, tx: CacheTransaction<CacheKeyRegistry>) { |
@@ -1,1 +0,1 @@ | ||
export {}; | ||
export { buildCache } from './cache.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
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
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
186101
18
2517
569
1