deno.land / x / replicache@v10.0.0-beta.0 / persist / clients.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632import {expect} from '@esm-bundle/chai';import {assertNotUndefined} from '../asserts';import {BTreeRead} from '../btree/read';import * as dag from '../dag/mod';import {fromChunk, SnapshotMeta} from '../db/commit';import {assertHash, fakeHash, newTempHash} from '../hash';import { ClientMap, getClient, getClients, initClient, noUpdates, updateClients,} from './clients';import {SinonFakeTimers, useFakeTimers} from 'sinon';import { addGenesis, addIndexChange, addLocal, addSnapshot, Chain,} from '../db/test-helpers';import {makeClient, setClients} from './clients-test-helpers';
let clock: SinonFakeTimers;setup(() => { clock = useFakeTimers(0);});
teardown(() => { clock.restore();});
test('getClients with no existing ClientMap in dag store', async () => { const dagStore = new dag.TestStore(); await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap.size).to.equal(0); });});
test('updateClients and getClients', async () => { const dagStore = new dag.TestStore(); const clientMap = new Map( Object.entries({ client1: makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }), client2: makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }), }), ); await setClients(clientMap, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); });});
test('updateClients and getClients sequence', async () => { const dagStore = new dag.TestStore(); const clientMap1 = new Map( Object.entries({ client1: makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }), client2: makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }), }), );
const clientMap2 = new Map( Object.entries({ client3: makeClient({ heartbeatTimestampMs: 4000, headHash: fakeHash('headclient3'), }), }), ); await setClients(clientMap1, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const readClientMap1 = await getClients(read); expect(readClientMap1).to.deep.equal(clientMap1); });
await setClients(clientMap2, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const readClientMap2 = await getClients(read); expect(readClientMap2).to.deep.equal(clientMap2); });});
test('updateClients properly manages refs to client heads when clients are removed and added', async () => { const dagStore = new dag.TestStore(); const client1HeadHash = fakeHash('headclient1'); const client2HeadHash = fakeHash('headclient2');
const clientMap1 = new Map( Object.entries({ client1: makeClient({ heartbeatTimestampMs: 1000, headHash: client1HeadHash, }), client2: makeClient({ heartbeatTimestampMs: 3000, headHash: client2HeadHash, }), }), );
const client3HeadHash = fakeHash('headclient3'); const clientMap2 = new Map( Object.entries({ client3: makeClient({ heartbeatTimestampMs: 4000, headHash: client3HeadHash, }), }), ); await setClients(clientMap1, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const clientsHash = await read.getHead('clients'); assertHash(clientsHash); const clientsChunk = await read.getChunk(clientsHash); expect(clientsChunk?.meta).to.deep.equal([ client1HeadHash, client2HeadHash, ]); }); await setClients(clientMap2, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const clientsHash = await read.getHead('clients'); assertHash(clientsHash); const clientsChunk = await read.getChunk(clientsHash); expect(clientsChunk?.meta).to.deep.equal([client3HeadHash]); });});
test("updateClients properly manages refs to client heads when a client's head changes", async () => { const dagStore = new dag.TestStore(); const client1V1HeadHash = fakeHash('headclient1'); const client1V2HeadHash = fakeHash('headclient1v2'); const client2HeadHash = fakeHash('headclient2');
const client1V1 = makeClient({ heartbeatTimestampMs: 1000, headHash: client1V1HeadHash, }); const client1V2 = makeClient({ heartbeatTimestampMs: 2000, headHash: client1V2HeadHash, }); const client2 = makeClient({ heartbeatTimestampMs: 3000, headHash: client2HeadHash, });
const clientMap1 = new Map( Object.entries({ client1: client1V1, client2, }), );
await setClients(clientMap1, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const clientsHash = await read.getHead('clients'); assertHash(clientsHash); const clientsChunk = await read.getChunk(clientsHash); expect(clientsChunk?.meta).to.deep.equal([ client1V1HeadHash, client2HeadHash, ]); });
await setClients( new Map( Object.entries({ client1: client1V2, client2, }), ), dagStore, );
await dagStore.withRead(async (read: dag.Read) => { const clientsHash = await read.getHead('clients'); assertHash(clientsHash); const clientsChunk = await read.getChunk(clientsHash); expect(clientsChunk?.meta).to.deep.equal([ client1V2HeadHash, client2HeadHash, ]); });});
test('getClient', async () => { const dagStore = new dag.TestStore(); const client1 = makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }); const clientMap = new Map( Object.entries({ client1, client2: makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }), }), ); await setClients(clientMap, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const readClient1 = await getClient('client1', read); expect(readClient1).to.deep.equal(client1); });});
test('updateClients throws error if any client headHash is a temp hash', async () => { const dagStore = new dag.TestStore(); const client1 = makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }); const client2 = makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }); const clientMap = new Map( Object.entries({ client1, client2, }), );
await setClients(clientMap, dagStore);
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); });
const clientMapWTempHash = new Map( Object.entries({ client1, client2: makeClient({ heartbeatTimestampMs: 3000, headHash: newTempHash(), }), }), );
let e; try { await setClients(clientMapWTempHash, dagStore); } catch (ex) { e = ex; } expect(e).to.be.instanceOf(Error);
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); });});
test('updateClients throws errors if clients head exist but the chunk it refrences does not', async () => { const dagStore = new dag.TestStore(); await dagStore.withWrite(async (write: dag.Write) => { await write.setHead('clients', fakeHash('randomstuff')); await write.commit(); }); await dagStore.withRead(async (read: dag.Read) => { let e; try { await getClients(read); } catch (ex) { e = ex; } expect(e).to.be.instanceOf(Error); });});
test('updateClients is a noop if noUpdates is returned from update', async () => { const dagStore = new dag.TestStore(); const clientMap = new Map( Object.entries({ client1: makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }), client2: makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }), }), ); await setClients(clientMap, dagStore); await updateClients(_ => noUpdates, dagStore); await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); });});
test('updateClients puts chunksToPut returned by update', async () => { const dagStore = new dag.TestStore(); const chunksToPut = [ dag.createChunkWithHash(fakeHash('chunktoput1'), 'chunktoPut1', []), dag.createChunkWithHash(fakeHash('chunktoput2'), 'chunkToPut2', [ fakeHash('chunktoput1'), ]), ]; const clientMap = new Map( Object.entries({ client1: makeClient({ heartbeatTimestampMs: 1000, headHash: chunksToPut[1].hash, }), }), ); const update = async (_: ClientMap) => { return {clients: clientMap, chunksToPut}; };
await updateClients(update, dagStore); await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); }); await dagStore.withRead(async (read: dag.Read) => { expect(await read.getChunk(chunksToPut[0].hash)).to.deep.equal( chunksToPut[0], ); expect(await read.getChunk(chunksToPut[1].hash)).to.deep.equal( chunksToPut[1], ); });});
test('updateClients with conflict during update (i.e. testing race case with retry)', async () => { const dagStore = new dag.TestStore(); const client1 = makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }); const client2 = makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }); const clientMap = new Map( Object.entries({ client1, client2, }), );
const client3 = makeClient({ heartbeatTimestampMs: 5000, headHash: fakeHash('headclient3'), }); const clientMap2 = new Map(clientMap).set('client3', client3);
await setClients(clientMap, dagStore);
const chunksToPut = [ dag.createChunkWithHash(fakeHash('chunktoput1'), 'chunkToPut1', []), dag.createChunkWithHash(fakeHash('chunktoput2'), 'chunkToPut2', [ fakeHash('chunktoput1'), ]), ]; const client4 = makeClient({ heartbeatTimestampMs: 7000, headHash: chunksToPut[1].hash, });
let updateCallCount = 0; const update = async (clients: ClientMap) => { updateCallCount++; expect(updateCallCount).to.be.lessThan( 3, 'Expect update to only be called twice', ); if (updateCallCount === 1) { // create conflict await setClients(clientMap2, dagStore); } if (updateCallCount === 2) { expect(clients).to.deep.equal(clientMap2); } // chunksToPut are not written until there is not conflict. await dagStore.withRead(async (read: dag.Read) => { expect(await read.getChunk(chunksToPut[0].hash)).to.be.undefined; expect(await read.getChunk(chunksToPut[1].hash)).to.be.undefined; }); return {clients: new Map(clients).set('client4', client4), chunksToPut}; };
const expectedClientMap = new Map( Object.entries({ client1, client2, client3, client4, }), ); const updatedClients = await updateClients(update, dagStore); expect(updatedClients).to.deep.equal(expectedClientMap); await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(expectedClientMap); expect(await read.getChunk(chunksToPut[0].hash)).to.deep.equal( chunksToPut[0], ); expect(await read.getChunk(chunksToPut[1].hash)).to.deep.equal( chunksToPut[1], ); });});
test('updateClients where update return noUpdates after conflict during update', async () => { const dagStore = new dag.TestStore(); const client1 = makeClient({ heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), }); const client2 = makeClient({ heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), }); const clientMap = new Map( Object.entries({ client1, client2, }), );
const client3 = makeClient({ heartbeatTimestampMs: 5000, headHash: fakeHash('headclient3'), }); const clientMap2 = new Map(clientMap).set('client3', client3);
await setClients(clientMap, dagStore);
const chunksToPut = [ dag.createChunkWithHash(fakeHash('chunktoput1'), 'chunkToPut1', []), dag.createChunkWithHash(fakeHash('chunktoput2'), 'chunkToPut2', [ fakeHash('chunktoput1'), ]), ]; const client4 = makeClient({ heartbeatTimestampMs: 7000, headHash: chunksToPut[1].hash, });
let updateCallCount = 0; const update = async (clients: ClientMap) => { updateCallCount++; expect(updateCallCount).to.be.lessThan( 3, 'Expect update to only be called twice', ); if (updateCallCount === 1) { // create conflict await setClients(clientMap2, dagStore); return {clients: new Map(clients).set('client4', client4), chunksToPut}; } if (updateCallCount === 2) { expect(clients).to.deep.equal(clientMap2); } return noUpdates; };
// Client 4 never added. const expectedClientMap = new Map( Object.entries({ client1, client2, client3, }), ); const updatedClients = await updateClients(update, dagStore); expect(updatedClients).to.deep.equal(expectedClientMap); await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(expectedClientMap); // chunks never put expect(await read.getChunk(chunksToPut[0].hash)).to.be.undefined; expect(await read.getChunk(chunksToPut[1].hash)).to.be.undefined; });});
test('updateClients throws errors if chunk pointed to by clients head does not contain a valid ClientMap', async () => { const dagStore = new dag.TestStore(); await dagStore.withWrite(async (write: dag.Write) => { const headHash = fakeHash('headclient1'); const chunk = write.createChunk( { heartbeatTimestampMs: 'this should be a number', headHash, }, [headHash], );
await Promise.all([ write.putChunk(chunk), write.setHead('clients', chunk.hash), ]); await write.commit(); }); await dagStore.withRead(async (read: dag.Read) => { let e; try { await getClients(read); } catch (ex) { e = ex; } expect(e).to.be.instanceOf(Error); });});
test('initClient creates new empty snapshot when no existing snapshot to bootstrap from', async () => { const dagStore = new dag.TestStore(); clock.tick(4000); const [clientId, client, clients] = await initClient(dagStore);
expect(clients).to.deep.equal( new Map( Object.entries({ [clientId]: client, }), ), );
await dagStore.withRead(async (read: dag.Read) => { // New client was added to the client map. expect(await getClient(clientId, read)).to.deep.equal(client); expect(client.heartbeatTimestampMs).to.equal(clock.now); expect(client.mutationID).to.equal(0); expect(client.lastServerAckdMutationID).to.equal(0);
// New client's head hash points to an empty snapshot with an empty btree. const headChunk = await read.getChunk(client.headHash); assertNotUndefined(headChunk); const commit = fromChunk(headChunk); expect(commit.isSnapshot()).to.be.true; const snapshotMeta = commit.meta as SnapshotMeta; expect(snapshotMeta.basisHash).to.be.null; expect(snapshotMeta.cookieJSON).to.be.null; expect(commit.mutationID).to.equal(0); expect(commit.indexes).to.be.empty; expect(await new BTreeRead(read, commit.valueHash).isEmpty()).to.be.true; });});
test('initClient bootstraps from base snapshot of client with highest heartbeat', async () => { const dagStore = new dag.TestStore();
const chain: Chain = []; await addGenesis(chain, dagStore); await addSnapshot(chain, dagStore, [['foo', 'bar']]); await addLocal(chain, dagStore); const client1HeadCommit = chain[chain.length - 1]; await addIndexChange(chain, dagStore); await addSnapshot(chain, dagStore, [['fuz', 'bang']]); const client2BaseSnapshotCommit = chain[chain.length - 1]; await addLocal(chain, dagStore); await addLocal(chain, dagStore); const client2HeadCommit = chain[chain.length - 1];
const clientMap = new Map( Object.entries({ client1: makeClient({ heartbeatTimestampMs: 1000, headHash: client1HeadCommit.chunk.hash, }), client2: makeClient({ heartbeatTimestampMs: 3000, headHash: client2HeadCommit.chunk.hash, }), }), ); await setClients(clientMap, dagStore);
clock.tick(4000); const [clientId, client, clients] = await initClient(dagStore);
expect(clients).to.deep.equal(new Map(clientMap).set(clientId, client));
await dagStore.withRead(async (read: dag.Read) => { // New client was added to the client map. expect(await getClient(clientId, read)).to.deep.equal(client); expect(client.heartbeatTimestampMs).to.equal(clock.now); expect(client.mutationID).to.equal(0); expect(client.lastServerAckdMutationID).to.equal(0);
// New client's head hash points to a commit that matches client2BaseSnapshoCommit // but with a local mutation id of 0. const headChunk = await read.getChunk(client.headHash); assertNotUndefined(headChunk); const commit = fromChunk(headChunk); expect(commit.isSnapshot()).to.be.true; const snapshotMeta = commit.meta as SnapshotMeta; expect(client2BaseSnapshotCommit.isSnapshot()).to.be.true; const client2BaseSnapshotMeta = client2BaseSnapshotCommit.meta as SnapshotMeta;
expect(snapshotMeta.basisHash).to.equal(client2BaseSnapshotMeta.basisHash); expect(snapshotMeta.cookieJSON).to.equal( client2BaseSnapshotMeta.cookieJSON, ); expect(commit.mutationID).to.equal(0); expect(commit.indexes).to.not.be.empty; expect(commit.indexes).to.deep.equal(client2BaseSnapshotCommit.indexes); expect(commit.valueHash).to.equal(client2BaseSnapshotCommit.valueHash); });});
Version Info