deno.land / x / replicache@v10.0.0-beta.0 / persist / heartbeat.test.ts
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244import {LogContext} from '@rocicorp/logger';import {expect} from '@esm-bundle/chai';import * as sinon from 'sinon';import {SinonFakeTimers, useFakeTimers} from 'sinon';import * as dag from '../dag/mod';import { latestHeartbeatUpdate, startHeartbeats, writeHeartbeat,} from './heartbeat';import {ClientMap, ClientStateNotFoundError, getClients} from './clients';import {fakeHash} from '../hash';import {makeClient, setClients} from './clients-test-helpers';import {assertNotUndefined} from '../asserts';
let clock: SinonFakeTimers;const START_TIME = 100000;const ONE_MIN_IN_MS = 60 * 1000;setup(() => { clock = useFakeTimers(START_TIME);});
teardown(() => { clock.restore();});
function awaitLatestHeartbeatUpdate(): Promise<ClientMap> { const latest = latestHeartbeatUpdate; assertNotUndefined(latest); return latest;}
test('startHeartbeats starts interval that writes heartbeat each minute', async () => { const dagStore = new dag.TestStore(); const client1 = { heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), mutationID: 10, lastServerAckdMutationID: 10, }; const client2 = { heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), mutationID: 100, lastServerAckdMutationID: 90, }; const clientMap = new Map( Object.entries({ client1, client2, }), ); await setClients(clientMap, dagStore);
const controller = new AbortController(); startHeartbeats( 'client1', dagStore, () => undefined, new LogContext(), controller.signal, );
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); });
await clock.tickAsync(ONE_MIN_IN_MS); await awaitLatestHeartbeatUpdate();
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal( new Map( Object.entries({ client1: { ...client1, heartbeatTimestampMs: START_TIME + ONE_MIN_IN_MS, }, client2, }), ), ); });
await clock.tickAsync(ONE_MIN_IN_MS); await awaitLatestHeartbeatUpdate();
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal( new Map( Object.entries({ client1: { ...client1, heartbeatTimestampMs: START_TIME + ONE_MIN_IN_MS + ONE_MIN_IN_MS, }, client2, }), ), ); });});
test('calling function returned by startHeartbeats, stops heartbeats', 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);
const controller = new AbortController(); startHeartbeats( 'client1', dagStore, () => undefined, new LogContext(), controller.signal, );
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal(clientMap); });
await clock.tickAsync(ONE_MIN_IN_MS); await awaitLatestHeartbeatUpdate();
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(Object.fromEntries(readClientMap)).to.deep.equal({ client1: { ...client1, heartbeatTimestampMs: START_TIME + ONE_MIN_IN_MS, }, client2, }); });
controller.abort(); clock.tick(ONE_MIN_IN_MS); await awaitLatestHeartbeatUpdate();
await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal( new Map( Object.entries({ client1: { ...client1, // Heartbeat *NOT* updated to START_TIME + ONE_MIN_IN_MS + ONE_MIN_IN_MS heartbeatTimestampMs: START_TIME + ONE_MIN_IN_MS, }, client2, }), ), ); });});
test('writeHeartbeat writes heartbeat', async () => { const dagStore = new dag.TestStore(); const client1 = { heartbeatTimestampMs: 1000, headHash: fakeHash('headclient1'), mutationID: 10, lastServerAckdMutationID: 10, }; const client2 = { heartbeatTimestampMs: 3000, headHash: fakeHash('headclient2'), mutationID: 100, lastServerAckdMutationID: 90, }; const clientMap = new Map( Object.entries({ client1, client2, }), );
await setClients(clientMap, dagStore);
const TICK_IN_MS = 20000; clock.tick(TICK_IN_MS);
await writeHeartbeat('client1', dagStore); await dagStore.withRead(async (read: dag.Read) => { const readClientMap = await getClients(read); expect(readClientMap).to.deep.equal( new Map( Object.entries({ client1: { ...client1, heartbeatTimestampMs: START_TIME + TICK_IN_MS, }, client2, }), ), ); });});
test('writeHeartbeat throws Error if no Client is found for clientID', async () => { const dagStore = new dag.TestStore(); let e; try { await writeHeartbeat('client1', dagStore); } catch (ex) { e = ex; } expect(e) .to.be.instanceOf(ClientStateNotFoundError) .property('id', 'client1');});
test('heartbeat with missing client calls callback', async () => { const dagStore = new dag.TestStore(); const onClientStateNotFound = sinon.fake(); const controller = new AbortController(); startHeartbeats( 'client1', dagStore, onClientStateNotFound, new LogContext(), controller.signal, ); await clock.tickAsync(ONE_MIN_IN_MS); expect(onClientStateNotFound.callCount).to.equal(1); controller.abort();});
Version Info