@spooky-sync/core 0.0.1-canary.32 → 0.0.1-canary.321
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +81 -0
- package/README.md +56 -16
- package/dist/index.d.ts +2034 -137
- package/dist/index.js +15236 -2467
- package/dist/live.d.ts +141 -0
- package/dist/live.js +733 -0
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.js +3 -3
- package/dist/pure.d.ts +365 -0
- package/dist/pure.js +24 -0
- package/dist/sqlite-open.js +379 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +439 -0
- package/dist/sw.d.ts +207 -0
- package/dist/sw.js +393 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +472 -0
- package/dist/types.d.ts +1821 -38
- package/dist/worker-edges.js +470 -0
- package/dist/worker-impersonation.js +60 -0
- package/dist/worker-ref-tables.d.ts +48 -0
- package/dist/worker-types.d.ts +112 -0
- package/dist/worker-types.js +73 -0
- package/package.json +27 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/scripts/check-worker-entries.mjs +172 -0
- package/skills/sp00ky-core/SKILL.md +31 -6
- package/skills/sp00ky-core/references/config.md +6 -0
- package/src/boot/auth-flip.saga.test.ts +135 -0
- package/src/boot/auth-flip.saga.ts +77 -0
- package/src/boot/boot.saga.test.ts +180 -0
- package/src/boot/boot.saga.ts +142 -0
- package/src/boot/bucket-switch.saga.test.ts +187 -0
- package/src/boot/bucket-switch.saga.ts +133 -0
- package/src/boot/preload.saga.test.ts +51 -0
- package/src/boot/preload.saga.ts +31 -0
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/client/bucket-handle.test.ts +117 -0
- package/src/client/bucket-handle.ts +227 -0
- package/src/client/router.test.ts +58 -0
- package/src/client/router.ts +100 -0
- package/src/client/runtime.test.ts +195 -0
- package/src/client/runtime.ts +296 -0
- package/src/client/services.test.ts +60 -0
- package/src/client/services.ts +418 -0
- package/src/client/sp00ky-client.test.ts +303 -0
- package/src/client/sp00ky-client.ts +586 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +69 -1
- package/src/kernel/constants.test.ts +19 -0
- package/src/kernel/constants.ts +80 -0
- package/src/kernel/effects.test.ts +40 -0
- package/src/kernel/effects.ts +163 -0
- package/src/kernel/events.ts +57 -0
- package/src/kernel/interpreter.test.ts +116 -0
- package/src/kernel/interpreter.ts +148 -0
- package/src/kernel/saga.test.ts +89 -0
- package/src/kernel/saga.ts +73 -0
- package/src/live/index.test.ts +501 -0
- package/src/live/index.ts +1039 -0
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +209 -0
- package/src/modules/auth/auth.local-first.test.ts +169 -0
- package/src/modules/auth/events/index.ts +9 -2
- package/src/modules/auth/impersonation-banner.ts +443 -0
- package/src/modules/auth/impersonation.integration.test.ts +205 -0
- package/src/modules/auth/impersonation.test.ts +377 -0
- package/src/modules/auth/impersonation.ts +179 -0
- package/src/modules/auth/index.ts +466 -19
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +746 -95
- package/src/modules/devtools/mutations.test.ts +92 -0
- package/src/modules/devtools/mutations.ts +160 -0
- package/src/modules/devtools/notify-throttle.test.ts +166 -0
- package/src/modules/devtools/state-shape.test.ts +263 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +110 -0
- package/src/modules/feature-flag/index.test.ts +298 -0
- package/src/modules/feature-flag/index.ts +316 -0
- package/src/modules/ref-tables.test.ts +93 -0
- package/src/modules/ref-tables.ts +91 -0
- package/src/modules/web-push/index.test.ts +824 -0
- package/src/modules/web-push/index.ts +1027 -0
- package/src/mutation/jobs.test.ts +25 -0
- package/src/mutation/jobs.ts +41 -0
- package/src/mutation/mutation-id.test.ts +37 -0
- package/src/mutation/mutation-id.ts +35 -0
- package/src/mutation/pending-writes.integration.test.ts +175 -0
- package/src/mutation/push.saga.test.ts +285 -0
- package/src/mutation/push.saga.ts +217 -0
- package/src/mutation/rows.test.ts +231 -0
- package/src/mutation/rows.ts +427 -0
- package/src/mutation/tray.saga.test.ts +63 -0
- package/src/mutation/tray.saga.ts +48 -0
- package/src/mutation/write.saga.test.ts +316 -0
- package/src/mutation/write.saga.ts +268 -0
- package/src/otel/index.ts +10 -7
- package/src/pure.ts +66 -0
- package/src/push/types.ts +174 -0
- package/src/query/anon-query-isolation.integration.test.ts +337 -0
- package/src/query/env.test.ts +23 -0
- package/src/query/env.ts +56 -0
- package/src/query/fetch.saga.test.ts +199 -0
- package/src/query/fetch.saga.ts +120 -0
- package/src/query/hash.test.ts +14 -0
- package/src/query/hash.ts +18 -0
- package/src/query/id-set-plan.test.ts +132 -0
- package/src/query/lifecycle.saga.test.ts +274 -0
- package/src/query/lifecycle.saga.ts +159 -0
- package/src/query/list-ref-live.integration.test.ts +203 -0
- package/src/query/materialize.saga.test.ts +115 -0
- package/src/query/materialize.saga.ts +84 -0
- package/src/query/materialize.test.ts +58 -0
- package/src/query/materialize.ts +57 -0
- package/src/query/membership.saga.test.ts +350 -0
- package/src/query/membership.saga.ts +278 -0
- package/src/query/membership.test.ts +118 -0
- package/src/query/membership.ts +201 -0
- package/src/query/register.saga.test.ts +311 -0
- package/src/query/register.saga.ts +229 -0
- package/src/query/render-set.test.ts +47 -0
- package/src/query/render-set.ts +71 -0
- package/src/query/sql.test.ts +112 -0
- package/src/query/sql.ts +149 -0
- package/src/query/window-query.test.ts +70 -0
- package/src/query/window-query.ts +195 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +199 -0
- package/src/services/database/connection-supervisor.test.ts +318 -0
- package/src/services/database/connection-supervisor.ts +422 -0
- package/src/services/database/database.query-timeout.test.ts +140 -0
- package/src/services/database/database.responses.test.ts +43 -0
- package/src/services/database/database.ts +146 -61
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/errors.ts +34 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/landed-body-clear.integration.test.ts +236 -0
- package/src/services/database/local-migrator.integration.test.ts +110 -0
- package/src/services/database/local-migrator.ts +59 -16
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +487 -63
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +484 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +273 -6
- package/src/services/database/sqlite-cache-engine.test.ts +617 -0
- package/src/services/database/sqlite-cache-engine.timeout.test.ts +61 -0
- package/src/services/database/sqlite-cache-engine.ts +1372 -0
- package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
- package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
- package/src/services/database/sqlite-lock-verify.test.ts +33 -0
- package/src/services/database/sqlite-lock-verify.ts +45 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +145 -0
- package/src/services/database/sqlite-plan-sql.ts +182 -0
- package/src/services/database/sqlite-projection.test.ts +121 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +261 -0
- package/src/services/database/sqlite-select.ts +131 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +224 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +416 -0
- package/src/services/database/surreal-cache-engine.ts +161 -0
- package/src/services/logger/index.test.ts +132 -0
- package/src/services/logger/index.ts +189 -7
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +10 -3
- package/src/services/persistence/surrealdb.ts +4 -4
- package/src/services/stream-processor/index.ts +783 -68
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +186 -0
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +226 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +58 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +327 -0
- package/src/services/tabs/coordinator.test.ts +386 -0
- package/src/services/tabs/coordinator.ts +638 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +262 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +640 -0
- package/src/sp00ky.ts +5 -395
- package/src/state/client-state.ts +200 -0
- package/src/state/lifecycle.test.ts +141 -0
- package/src/state/lifecycle.ts +99 -0
- package/src/state/reducers.test.ts +304 -0
- package/src/state/reducers.ts +526 -0
- package/src/state/selectors.test.ts +175 -0
- package/src/state/selectors.ts +222 -0
- package/src/sw/index.test.ts +582 -0
- package/src/sw/index.ts +607 -0
- package/src/sw/store.ts +96 -0
- package/src/sync/connection.saga.test.ts +97 -0
- package/src/sync/connection.saga.ts +89 -0
- package/src/sync/live.saga.test.ts +178 -0
- package/src/sync/live.saga.ts +107 -0
- package/src/sync/policy.health.test.ts +56 -0
- package/src/sync/policy.test.ts +663 -0
- package/src/sync/policy.ts +515 -0
- package/src/sync/poll.saga.test.ts +51 -0
- package/src/sync/poll.saga.ts +36 -0
- package/src/sync/tabs.saga.test.ts +78 -0
- package/src/sync/tabs.saga.ts +96 -0
- package/src/testing/adapters.ts +133 -0
- package/src/testing/build.ts +63 -0
- package/src/testing/fake-services.ts +48 -0
- package/src/testing/run-pure.test.ts +92 -0
- package/src/testing/run-pure.ts +128 -0
- package/src/testing/services.ts +17 -0
- package/src/types.ts +624 -10
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/edges.ts +39 -0
- package/src/utils/error-classification.test.ts +54 -0
- package/src/utils/error-classification.ts +25 -0
- package/src/utils/index.ts +78 -12
- package/src/utils/parser.test.ts +67 -102
- package/src/utils/parser.ts +64 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/sha256.ts +15 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/utils.test.ts +3 -2
- package/tsdown.config.ts +117 -3
- package/vitest.config.ts +48 -0
- package/src/modules/cache/index.ts +0 -241
- package/src/modules/cache/types.ts +0 -19
- package/src/modules/data/data.test.ts +0 -58
- package/src/modules/data/index.ts +0 -879
- package/src/modules/sync/engine.ts +0 -173
- package/src/modules/sync/events/index.ts +0 -77
- package/src/modules/sync/index.ts +0 -3
- package/src/modules/sync/queue/index.ts +0 -2
- package/src/modules/sync/queue/queue-down.ts +0 -89
- package/src/modules/sync/queue/queue-up.ts +0 -223
- package/src/modules/sync/scheduler.ts +0 -84
- package/src/modules/sync/sync.ts +0 -421
- package/src/modules/sync/utils.test.ts +0 -311
- package/src/modules/sync/utils.ts +0 -167
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { RecordId } from 'surrealdb';
|
|
3
|
+
import {
|
|
4
|
+
diffRecordVersionArray,
|
|
5
|
+
applyRecordVersionDiff,
|
|
6
|
+
createDiffFromDbOp,
|
|
7
|
+
ArraySyncer,
|
|
8
|
+
resolveListRefPollInterval,
|
|
9
|
+
DEFAULT_LIST_REF_POLL_INTERVAL_MS,
|
|
10
|
+
buildListRefSelect,
|
|
11
|
+
buildSubqueryListRefSelect,
|
|
12
|
+
buildListRefBatchSelect,
|
|
13
|
+
buildQueryRowCountBatchSelect,
|
|
14
|
+
planListRefPollChunks,
|
|
15
|
+
nextPollDelayMs,
|
|
16
|
+
listRefPollDelayMs,
|
|
17
|
+
LIST_REF_POLL_MAX_INTERVAL_MS,
|
|
18
|
+
recordVersionArraysEqual,
|
|
19
|
+
} from './policy';
|
|
20
|
+
import type { RecordVersionArray, RecordVersionDiff } from '../types';
|
|
21
|
+
import { encodeRecordId } from '../utils/index';
|
|
22
|
+
|
|
23
|
+
describe('ArraySyncer.update', () => {
|
|
24
|
+
it('planListRefPollChunks keeps duplicate hashes with equal ages stable', () => {
|
|
25
|
+
const now = 1000;
|
|
26
|
+
const chunks = planListRefPollChunks(
|
|
27
|
+
[
|
|
28
|
+
{ hash: 'x', rows: 1, lastPolledAt: 0 },
|
|
29
|
+
{ hash: 'x', rows: 1, lastPolledAt: 0 },
|
|
30
|
+
{ hash: 'a', rows: 1, lastPolledAt: 0 },
|
|
31
|
+
],
|
|
32
|
+
{ now }
|
|
33
|
+
);
|
|
34
|
+
expect(chunks).toEqual([['a', 'x', 'x']]);
|
|
35
|
+
});
|
|
36
|
+
it('nextPollDelayMs treats a future live event as no event', () => {
|
|
37
|
+
expect(nextPollDelayMs({ now: 100, lastLiveEventAt: 200, baseIntervalMs: 500 })).toBe(500);
|
|
38
|
+
});
|
|
39
|
+
it('bumps only the matching record and re-sorts on the next diff', () => {
|
|
40
|
+
const syncer = new ArraySyncer([['t:b', 1], ['t:a', 1]], [['t:a', 2], ['t:b', 1]]);
|
|
41
|
+
syncer.update('t:a', 2);
|
|
42
|
+
expect(syncer.nextSet()).toEqual({ added: [], updated: [], removed: [] });
|
|
43
|
+
syncer.insert('t:c', 1);
|
|
44
|
+
syncer.delete('t:b');
|
|
45
|
+
const diff = syncer.nextSet();
|
|
46
|
+
expect(diff!.added.map((a) => encodeRecordId(a.id))).toEqual(['t:b']);
|
|
47
|
+
expect(diff!.removed.map((r) => encodeRecordId(r))).toEqual(['t:c']);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function rid(table: string, id: string): RecordId<string> {
|
|
52
|
+
return new RecordId(table, id);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe('diffRecordVersionArray', () => {
|
|
56
|
+
it('detects added records (in remote, not local)', () => {
|
|
57
|
+
const local: RecordVersionArray = [['user:1', 1]];
|
|
58
|
+
const remote: RecordVersionArray = [
|
|
59
|
+
['user:1', 1],
|
|
60
|
+
['user:2', 1],
|
|
61
|
+
];
|
|
62
|
+
const diff = diffRecordVersionArray(local, remote);
|
|
63
|
+
|
|
64
|
+
expect(diff.added).toHaveLength(1);
|
|
65
|
+
expect(encodeRecordId(diff.added[0].id)).toBe('user:2');
|
|
66
|
+
expect(diff.added[0].version).toBe(1);
|
|
67
|
+
expect(diff.updated).toHaveLength(0);
|
|
68
|
+
expect(diff.removed).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('detects updated records (remote version > local version)', () => {
|
|
72
|
+
const local: RecordVersionArray = [['user:1', 1]];
|
|
73
|
+
const remote: RecordVersionArray = [['user:1', 3]];
|
|
74
|
+
const diff = diffRecordVersionArray(local, remote);
|
|
75
|
+
|
|
76
|
+
expect(diff.updated).toHaveLength(1);
|
|
77
|
+
expect(encodeRecordId(diff.updated[0].id)).toBe('user:1');
|
|
78
|
+
expect(diff.updated[0].version).toBe(3);
|
|
79
|
+
expect(diff.added).toHaveLength(0);
|
|
80
|
+
expect(diff.removed).toHaveLength(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('detects removed records (in local, not remote)', () => {
|
|
84
|
+
const local: RecordVersionArray = [
|
|
85
|
+
['user:1', 1],
|
|
86
|
+
['user:2', 1],
|
|
87
|
+
];
|
|
88
|
+
const remote: RecordVersionArray = [['user:1', 1]];
|
|
89
|
+
const diff = diffRecordVersionArray(local, remote);
|
|
90
|
+
|
|
91
|
+
expect(diff.removed).toHaveLength(1);
|
|
92
|
+
expect(encodeRecordId(diff.removed[0])).toBe('user:2');
|
|
93
|
+
expect(diff.added).toHaveLength(0);
|
|
94
|
+
expect(diff.updated).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('handles null arrays', () => {
|
|
98
|
+
const diff = diffRecordVersionArray(null, null);
|
|
99
|
+
expect(diff.added).toHaveLength(0);
|
|
100
|
+
expect(diff.updated).toHaveLength(0);
|
|
101
|
+
expect(diff.removed).toHaveLength(0);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handles empty arrays', () => {
|
|
105
|
+
const diff = diffRecordVersionArray([], []);
|
|
106
|
+
expect(diff.added).toHaveLength(0);
|
|
107
|
+
expect(diff.updated).toHaveLength(0);
|
|
108
|
+
expect(diff.removed).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('no diff when arrays match', () => {
|
|
112
|
+
const arr: RecordVersionArray = [
|
|
113
|
+
['user:1', 1],
|
|
114
|
+
['user:2', 2],
|
|
115
|
+
];
|
|
116
|
+
const diff = diffRecordVersionArray(arr, arr);
|
|
117
|
+
expect(diff.added).toHaveLength(0);
|
|
118
|
+
expect(diff.updated).toHaveLength(0);
|
|
119
|
+
expect(diff.removed).toHaveLength(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('handles mixed adds/updates/removes', () => {
|
|
123
|
+
const local: RecordVersionArray = [
|
|
124
|
+
['user:1', 1],
|
|
125
|
+
['user:2', 1],
|
|
126
|
+
['user:3', 1],
|
|
127
|
+
];
|
|
128
|
+
const remote: RecordVersionArray = [
|
|
129
|
+
['user:1', 1], // same
|
|
130
|
+
['user:2', 3], // updated
|
|
131
|
+
// user:3 removed
|
|
132
|
+
['user:4', 1], // added
|
|
133
|
+
];
|
|
134
|
+
const diff = diffRecordVersionArray(local, remote);
|
|
135
|
+
|
|
136
|
+
expect(diff.added).toHaveLength(1);
|
|
137
|
+
expect(encodeRecordId(diff.added[0].id)).toBe('user:4');
|
|
138
|
+
expect(diff.updated).toHaveLength(1);
|
|
139
|
+
expect(encodeRecordId(diff.updated[0].id)).toBe('user:2');
|
|
140
|
+
expect(diff.removed).toHaveLength(1);
|
|
141
|
+
expect(encodeRecordId(diff.removed[0])).toBe('user:3');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('applyRecordVersionDiff', () => {
|
|
146
|
+
it('applies additions', () => {
|
|
147
|
+
const current: RecordVersionArray = [['user:1', 1]];
|
|
148
|
+
const diff: RecordVersionDiff = {
|
|
149
|
+
added: [{ id: rid('user', '2'), version: 1 }],
|
|
150
|
+
updated: [],
|
|
151
|
+
removed: [],
|
|
152
|
+
};
|
|
153
|
+
const result = applyRecordVersionDiff(current, diff);
|
|
154
|
+
expect(result).toEqual([
|
|
155
|
+
['user:1', 1],
|
|
156
|
+
['user:2', 1],
|
|
157
|
+
]);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('applies updates', () => {
|
|
161
|
+
const current: RecordVersionArray = [['user:1', 1]];
|
|
162
|
+
const diff: RecordVersionDiff = {
|
|
163
|
+
added: [],
|
|
164
|
+
updated: [{ id: rid('user', '1'), version: 5 }],
|
|
165
|
+
removed: [],
|
|
166
|
+
};
|
|
167
|
+
const result = applyRecordVersionDiff(current, diff);
|
|
168
|
+
expect(result).toEqual([['user:1', 5]]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('applies removals', () => {
|
|
172
|
+
const current: RecordVersionArray = [
|
|
173
|
+
['user:1', 1],
|
|
174
|
+
['user:2', 2],
|
|
175
|
+
];
|
|
176
|
+
const diff: RecordVersionDiff = {
|
|
177
|
+
added: [],
|
|
178
|
+
updated: [],
|
|
179
|
+
removed: [rid('user', '1')],
|
|
180
|
+
};
|
|
181
|
+
const result = applyRecordVersionDiff(current, diff);
|
|
182
|
+
expect(result).toEqual([['user:2', 2]]);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('result is sorted by record ID', () => {
|
|
186
|
+
const current: RecordVersionArray = [['user:c', 1]];
|
|
187
|
+
const diff: RecordVersionDiff = {
|
|
188
|
+
added: [
|
|
189
|
+
{ id: rid('user', 'a'), version: 1 },
|
|
190
|
+
{ id: rid('user', 'z'), version: 1 },
|
|
191
|
+
],
|
|
192
|
+
updated: [],
|
|
193
|
+
removed: [],
|
|
194
|
+
};
|
|
195
|
+
const result = applyRecordVersionDiff(current, diff);
|
|
196
|
+
expect(result).toEqual([
|
|
197
|
+
['user:a', 1],
|
|
198
|
+
['user:c', 1],
|
|
199
|
+
['user:z', 1],
|
|
200
|
+
]);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('empty diff returns original (sorted)', () => {
|
|
204
|
+
const current: RecordVersionArray = [
|
|
205
|
+
['user:b', 2],
|
|
206
|
+
['user:a', 1],
|
|
207
|
+
];
|
|
208
|
+
const diff: RecordVersionDiff = { added: [], updated: [], removed: [] };
|
|
209
|
+
const result = applyRecordVersionDiff(current, diff);
|
|
210
|
+
expect(result).toEqual([
|
|
211
|
+
['user:a', 1],
|
|
212
|
+
['user:b', 2],
|
|
213
|
+
]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('createDiffFromDbOp', () => {
|
|
218
|
+
it('CREATE populates added array', () => {
|
|
219
|
+
const recordId = rid('user', '1');
|
|
220
|
+
const diff = createDiffFromDbOp('CREATE', recordId, 1);
|
|
221
|
+
expect(diff.added).toHaveLength(1);
|
|
222
|
+
expect(diff.added[0].id).toBe(recordId);
|
|
223
|
+
expect(diff.added[0].version).toBe(1);
|
|
224
|
+
expect(diff.updated).toHaveLength(0);
|
|
225
|
+
expect(diff.removed).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('UPDATE populates updated array', () => {
|
|
229
|
+
const recordId = rid('user', '1');
|
|
230
|
+
const diff = createDiffFromDbOp('UPDATE', recordId, 2);
|
|
231
|
+
expect(diff.updated).toHaveLength(1);
|
|
232
|
+
expect(diff.updated[0].id).toBe(recordId);
|
|
233
|
+
expect(diff.updated[0].version).toBe(2);
|
|
234
|
+
expect(diff.added).toHaveLength(0);
|
|
235
|
+
expect(diff.removed).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('DELETE populates removed array', () => {
|
|
239
|
+
const recordId = rid('user', '1');
|
|
240
|
+
const diff = createDiffFromDbOp('DELETE', recordId, 1);
|
|
241
|
+
expect(diff.removed).toHaveLength(1);
|
|
242
|
+
expect(diff.removed[0]).toBe(recordId);
|
|
243
|
+
expect(diff.added).toHaveLength(0);
|
|
244
|
+
expect(diff.updated).toHaveLength(0);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('skips if existing version >= new version', () => {
|
|
248
|
+
const recordId = rid('user', '1');
|
|
249
|
+
const versions: RecordVersionArray = [['user:1', 5]];
|
|
250
|
+
const diff = createDiffFromDbOp('UPDATE', recordId, 3, versions);
|
|
251
|
+
expect(diff.added).toHaveLength(0);
|
|
252
|
+
expect(diff.updated).toHaveLength(0);
|
|
253
|
+
expect(diff.removed).toHaveLength(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('applies if existing version < new version', () => {
|
|
257
|
+
const recordId = rid('user', '1');
|
|
258
|
+
const versions: RecordVersionArray = [['user:1', 2]];
|
|
259
|
+
const diff = createDiffFromDbOp('UPDATE', recordId, 5, versions);
|
|
260
|
+
expect(diff.updated).toHaveLength(1);
|
|
261
|
+
expect(diff.updated[0].version).toBe(5);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('ArraySyncer', () => {
|
|
266
|
+
it('insert adds to local array', () => {
|
|
267
|
+
const syncer = new ArraySyncer(
|
|
268
|
+
[['user:1', 1]],
|
|
269
|
+
[['user:1', 1]]
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
syncer.insert('user:2', 1);
|
|
273
|
+
const diff = syncer.nextSet();
|
|
274
|
+
expect(diff).not.toBeNull();
|
|
275
|
+
// local now has user:2 which remote does not → user:2 is in local removed from remote perspective
|
|
276
|
+
// Actually: local=[user:1, user:2], remote=[user:1] → user:2 is "removed" (in local, not remote)
|
|
277
|
+
expect(diff!.removed).toHaveLength(1);
|
|
278
|
+
expect(encodeRecordId(diff!.removed[0])).toBe('user:2');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('update modifies version in local array', () => {
|
|
282
|
+
const syncer = new ArraySyncer(
|
|
283
|
+
[['user:1', 1]],
|
|
284
|
+
[['user:1', 1]]
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
syncer.update('user:1', 5);
|
|
288
|
+
const diff = syncer.nextSet();
|
|
289
|
+
// local version (5) > remote version (1), so no "updated" from diff perspective
|
|
290
|
+
// diff finds remote additions/updates relative to local; local version higher means no remote update
|
|
291
|
+
expect(diff).not.toBeNull();
|
|
292
|
+
expect(diff!.added).toHaveLength(0);
|
|
293
|
+
expect(diff!.updated).toHaveLength(0);
|
|
294
|
+
expect(diff!.removed).toHaveLength(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('delete removes from local array', () => {
|
|
298
|
+
const syncer = new ArraySyncer(
|
|
299
|
+
[
|
|
300
|
+
['user:1', 1],
|
|
301
|
+
['user:2', 1],
|
|
302
|
+
],
|
|
303
|
+
[
|
|
304
|
+
['user:1', 1],
|
|
305
|
+
['user:2', 1],
|
|
306
|
+
]
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
syncer.delete('user:2');
|
|
310
|
+
const diff = syncer.nextSet();
|
|
311
|
+
expect(diff).not.toBeNull();
|
|
312
|
+
// remote has user:2, local does not → added from remote perspective
|
|
313
|
+
expect(diff!.added).toHaveLength(1);
|
|
314
|
+
expect(encodeRecordId(diff!.added[0].id)).toBe('user:2');
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('nextSet returns correct diff against remote', () => {
|
|
318
|
+
const syncer = new ArraySyncer(
|
|
319
|
+
[['user:1', 1]],
|
|
320
|
+
[
|
|
321
|
+
['user:1', 1],
|
|
322
|
+
['user:2', 1],
|
|
323
|
+
]
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const diff = syncer.nextSet();
|
|
327
|
+
expect(diff).not.toBeNull();
|
|
328
|
+
expect(diff!.added).toHaveLength(1);
|
|
329
|
+
expect(encodeRecordId(diff!.added[0].id)).toBe('user:2');
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('maintains sorting after mutations', () => {
|
|
333
|
+
const syncer = new ArraySyncer(
|
|
334
|
+
[['user:c', 1]],
|
|
335
|
+
[]
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
syncer.insert('user:a', 1);
|
|
339
|
+
syncer.insert('user:z', 1);
|
|
340
|
+
|
|
341
|
+
// nextSet triggers sort
|
|
342
|
+
const diff = syncer.nextSet();
|
|
343
|
+
// All 3 are in local but not remote → 3 removed items
|
|
344
|
+
expect(diff!.removed).toHaveLength(3);
|
|
345
|
+
// Check they come in sorted order
|
|
346
|
+
const removedIds = diff!.removed.map((r) => encodeRecordId(r));
|
|
347
|
+
const sorted = [...removedIds].toSorted();
|
|
348
|
+
expect(removedIds).toEqual(sorted);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('resolveListRefPollInterval', () => {
|
|
353
|
+
it('returns the default when no option is provided', () => {
|
|
354
|
+
expect(resolveListRefPollInterval()).toBe(DEFAULT_LIST_REF_POLL_INTERVAL_MS);
|
|
355
|
+
expect(resolveListRefPollInterval(undefined)).toBe(
|
|
356
|
+
DEFAULT_LIST_REF_POLL_INTERVAL_MS
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('respects a positive override', () => {
|
|
361
|
+
expect(resolveListRefPollInterval(1000)).toBe(1000);
|
|
362
|
+
expect(resolveListRefPollInterval(5000)).toBe(5000);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('falls back to default for zero, negative, NaN, or Infinity', () => {
|
|
366
|
+
// Accepting these would either silently disable polling or busy-loop
|
|
367
|
+
// the event loop, so they fall back to the default.
|
|
368
|
+
expect(resolveListRefPollInterval(0)).toBe(DEFAULT_LIST_REF_POLL_INTERVAL_MS);
|
|
369
|
+
expect(resolveListRefPollInterval(-500)).toBe(
|
|
370
|
+
DEFAULT_LIST_REF_POLL_INTERVAL_MS
|
|
371
|
+
);
|
|
372
|
+
expect(resolveListRefPollInterval(NaN)).toBe(
|
|
373
|
+
DEFAULT_LIST_REF_POLL_INTERVAL_MS
|
|
374
|
+
);
|
|
375
|
+
expect(resolveListRefPollInterval(Infinity)).toBe(
|
|
376
|
+
DEFAULT_LIST_REF_POLL_INTERVAL_MS
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('locks the default at 500ms so accidental tuning trips this test', () => {
|
|
381
|
+
expect(DEFAULT_LIST_REF_POLL_INTERVAL_MS).toBe(500);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
describe('buildListRefSelect', () => {
|
|
386
|
+
it('substitutes the table name', () => {
|
|
387
|
+
expect(buildListRefSelect('_00_list_ref')).toContain('->_00_list_ref ');
|
|
388
|
+
expect(buildListRefSelect('_00_list_ref_user_abc')).toContain(
|
|
389
|
+
'->_00_list_ref_user_abc '
|
|
390
|
+
);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('traverses from the bound query id instead of scanning the table', () => {
|
|
394
|
+
// `_00_list_ref*` has no index on `in`: `FROM table WHERE in = $in` is a
|
|
395
|
+
// full scan, which on the shared anon table is every visitor's edges.
|
|
396
|
+
const sql = buildListRefSelect('_00_list_ref');
|
|
397
|
+
expect(sql).toContain('FROM $in->_00_list_ref ');
|
|
398
|
+
expect(sql).not.toContain('in = $in');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('excludes subquery rows via parent IS NONE', () => {
|
|
402
|
+
// Without this predicate the poll would surface child records (the
|
|
403
|
+
// subquery-projection rows the SSP also writes into list_ref) as
|
|
404
|
+
// spurious "added" diffs against `localArray` every tick.
|
|
405
|
+
expect(buildListRefSelect('_00_list_ref')).toContain('parent IS NONE');
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('selects only `out` and `version`', () => {
|
|
409
|
+
// The diff only needs the record id and its version; pulling more
|
|
410
|
+
// fields would bloat the per-tick query traffic.
|
|
411
|
+
const sql = buildListRefSelect('_00_list_ref');
|
|
412
|
+
expect(sql.startsWith('SELECT out, version FROM ')).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
describe('buildSubqueryListRefSelect', () => {
|
|
417
|
+
it('substitutes the table name', () => {
|
|
418
|
+
expect(buildSubqueryListRefSelect('_00_list_ref')).toContain('->_00_list_ref ');
|
|
419
|
+
expect(buildSubqueryListRefSelect('_00_list_ref_user_abc')).toContain(
|
|
420
|
+
'->_00_list_ref_user_abc '
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('traverses from the bound query id', () => {
|
|
425
|
+
expect(buildSubqueryListRefSelect('_00_list_ref')).toContain('FROM $in->_00_list_ref ');
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it('selects ONLY subquery child rows via parent IS NOT NONE', () => {
|
|
429
|
+
// The mirror of `buildListRefSelect`'s `parent IS NONE`: the SSP writes
|
|
430
|
+
// `.related()` child rows into list_ref tagged with `parent`/`parent_rel`;
|
|
431
|
+
// this select pulls exactly those so their bodies can be synced into the
|
|
432
|
+
// local cache (separately from the primary window) and the correlated
|
|
433
|
+
// surql re-materializes with related data on a cold reload.
|
|
434
|
+
const sql = buildSubqueryListRefSelect('_00_list_ref');
|
|
435
|
+
expect(sql).toContain('parent IS NOT NONE');
|
|
436
|
+
expect(sql).not.toContain('parent IS NONE ');
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('selects only `out` and `version`', () => {
|
|
440
|
+
const sql = buildSubqueryListRefSelect('_00_list_ref');
|
|
441
|
+
expect(sql.startsWith('SELECT out, version FROM ')).toBe(true);
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('nextPollDelayMs', () => {
|
|
446
|
+
it('returns the base interval when LIVE has never delivered', () => {
|
|
447
|
+
expect(
|
|
448
|
+
nextPollDelayMs({
|
|
449
|
+
now: 10_000,
|
|
450
|
+
lastLiveEventAt: null,
|
|
451
|
+
baseIntervalMs: 500,
|
|
452
|
+
})
|
|
453
|
+
).toBe(500);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
it('returns the healthy interval when a LIVE event fired within the cooldown', () => {
|
|
457
|
+
expect(
|
|
458
|
+
nextPollDelayMs({
|
|
459
|
+
now: 10_000,
|
|
460
|
+
lastLiveEventAt: 8_000, // 2s ago, well within 5s cooldown
|
|
461
|
+
baseIntervalMs: 500,
|
|
462
|
+
})
|
|
463
|
+
).toBe(5_000);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('snaps back to the base interval after the cooldown elapses', () => {
|
|
467
|
+
expect(
|
|
468
|
+
nextPollDelayMs({
|
|
469
|
+
now: 20_000,
|
|
470
|
+
lastLiveEventAt: 10_000, // 10s ago, well past cooldown
|
|
471
|
+
baseIntervalMs: 500,
|
|
472
|
+
})
|
|
473
|
+
).toBe(500);
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('treats the cooldown boundary as expired (>= cooldownMs returns base)', () => {
|
|
477
|
+
// Exactly at the boundary, the LIVE feed is considered cold so we
|
|
478
|
+
// resume the aggressive cadence. Picks the same direction as
|
|
479
|
+
// `if (sinceLive >= cooldownMs)`.
|
|
480
|
+
expect(
|
|
481
|
+
nextPollDelayMs({
|
|
482
|
+
now: 15_000,
|
|
483
|
+
lastLiveEventAt: 10_000, // exactly 5s ago
|
|
484
|
+
baseIntervalMs: 500,
|
|
485
|
+
cooldownMs: 5_000,
|
|
486
|
+
})
|
|
487
|
+
).toBe(500);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('clamps a clock-skew negative sinceLive to the base interval', () => {
|
|
491
|
+
// If a stale timestamp puts the LIVE event in the future, we
|
|
492
|
+
// should not interpret that as "LIVE healthy" — treat it as
|
|
493
|
+
// unknown and use the conservative aggressive cadence.
|
|
494
|
+
expect(
|
|
495
|
+
nextPollDelayMs({
|
|
496
|
+
now: 10_000,
|
|
497
|
+
lastLiveEventAt: 20_000, // future timestamp
|
|
498
|
+
baseIntervalMs: 500,
|
|
499
|
+
})
|
|
500
|
+
).toBe(500);
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
it('never widens below an aggressively-configured base interval', () => {
|
|
504
|
+
// If the caller picked a faster base (e.g. 100ms) on purpose,
|
|
505
|
+
// the healthy path should not silently slow things back to 5s.
|
|
506
|
+
// The healthy interval is clamped to at least base.
|
|
507
|
+
expect(
|
|
508
|
+
nextPollDelayMs({
|
|
509
|
+
now: 10_000,
|
|
510
|
+
lastLiveEventAt: 9_500,
|
|
511
|
+
baseIntervalMs: 8_000,
|
|
512
|
+
})
|
|
513
|
+
).toBe(8_000);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('respects custom cooldown and healthy-interval values', () => {
|
|
517
|
+
expect(
|
|
518
|
+
nextPollDelayMs({
|
|
519
|
+
now: 10_000,
|
|
520
|
+
lastLiveEventAt: 9_500, // 0.5s ago
|
|
521
|
+
baseIntervalMs: 250,
|
|
522
|
+
cooldownMs: 1_000,
|
|
523
|
+
healthyIntervalMs: 2_000,
|
|
524
|
+
})
|
|
525
|
+
).toBe(2_000);
|
|
526
|
+
expect(
|
|
527
|
+
nextPollDelayMs({
|
|
528
|
+
now: 10_000,
|
|
529
|
+
lastLiveEventAt: 8_500, // 1.5s ago > cooldown
|
|
530
|
+
baseIntervalMs: 250,
|
|
531
|
+
cooldownMs: 1_000,
|
|
532
|
+
healthyIntervalMs: 2_000,
|
|
533
|
+
})
|
|
534
|
+
).toBe(250);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
describe('listRefPollDelayMs', () => {
|
|
539
|
+
it('returns the base interval at idle streak 0 (something just happened)', () => {
|
|
540
|
+
expect(listRefPollDelayMs({ idleStreak: 0, baseIntervalMs: 500 })).toBe(500);
|
|
541
|
+
// Negative streak is defensively treated as "active" too.
|
|
542
|
+
expect(listRefPollDelayMs({ idleStreak: -3, baseIntervalMs: 500 })).toBe(500);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('doubles per idle streak (exponential backoff)', () => {
|
|
546
|
+
expect(listRefPollDelayMs({ idleStreak: 1, baseIntervalMs: 500 })).toBe(1_000);
|
|
547
|
+
expect(listRefPollDelayMs({ idleStreak: 2, baseIntervalMs: 500 })).toBe(2_000);
|
|
548
|
+
expect(listRefPollDelayMs({ idleStreak: 3, baseIntervalMs: 500 })).toBe(4_000);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('caps at LIST_REF_POLL_MAX_INTERVAL_MS (5s) once the doubling exceeds it', () => {
|
|
552
|
+
// streak 4 would be 8000ms uncapped → clamped to 5000.
|
|
553
|
+
expect(listRefPollDelayMs({ idleStreak: 4, baseIntervalMs: 500 })).toBe(5_000);
|
|
554
|
+
expect(listRefPollDelayMs({ idleStreak: 50, baseIntervalMs: 500 })).toBe(5_000);
|
|
555
|
+
expect(LIST_REF_POLL_MAX_INTERVAL_MS).toBe(5_000);
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('never returns below the configured base, even when base exceeds the cap', () => {
|
|
559
|
+
// An aggressively-large base must not be implicitly shrunk by the cap.
|
|
560
|
+
expect(listRefPollDelayMs({ idleStreak: 0, baseIntervalMs: 8_000 })).toBe(8_000);
|
|
561
|
+
expect(listRefPollDelayMs({ idleStreak: 5, baseIntervalMs: 8_000 })).toBe(8_000);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it('respects a custom max interval', () => {
|
|
565
|
+
expect(
|
|
566
|
+
listRefPollDelayMs({ idleStreak: 10, baseIntervalMs: 500, maxIntervalMs: 3_000 })
|
|
567
|
+
).toBe(3_000);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('does not overflow for a very long idle streak', () => {
|
|
571
|
+
// 2^1000 would be Infinity; the exponent clamp keeps it finite and capped.
|
|
572
|
+
expect(listRefPollDelayMs({ idleStreak: 1_000, baseIntervalMs: 500 })).toBe(5_000);
|
|
573
|
+
});
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
describe('recordVersionArraysEqual', () => {
|
|
577
|
+
it('treats the same reference and identical contents as equal', () => {
|
|
578
|
+
const a: RecordVersionArray = [['game:1', 1], ['game:2', 3]];
|
|
579
|
+
expect(recordVersionArraysEqual(a, a)).toBe(true);
|
|
580
|
+
expect(recordVersionArraysEqual(a, [['game:1', 1], ['game:2', 3]])).toBe(true);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
it('is order-insensitive (the list_ref SELECT has no ORDER BY)', () => {
|
|
584
|
+
expect(
|
|
585
|
+
recordVersionArraysEqual(
|
|
586
|
+
[['game:1', 1], ['game:2', 3]],
|
|
587
|
+
[['game:2', 3], ['game:1', 1]]
|
|
588
|
+
)
|
|
589
|
+
).toBe(true);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('returns false when a version differs for the same id', () => {
|
|
593
|
+
expect(
|
|
594
|
+
recordVersionArraysEqual([['game:1', 1]], [['game:1', 2]])
|
|
595
|
+
).toBe(false);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('returns false on length mismatch', () => {
|
|
599
|
+
expect(
|
|
600
|
+
recordVersionArraysEqual([['game:1', 1]], [['game:1', 1], ['game:2', 1]])
|
|
601
|
+
).toBe(false);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it('returns false when an id is present in one but not the other', () => {
|
|
605
|
+
expect(
|
|
606
|
+
recordVersionArraysEqual([['game:1', 1]], [['game:2', 1]])
|
|
607
|
+
).toBe(false);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it('treats two empty arrays as equal', () => {
|
|
611
|
+
expect(recordVersionArraysEqual([], [])).toBe(true);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe('list_ref poll batching', () => {
|
|
616
|
+
it('reads many queries in one statement and their rowCounts keyed by id', () => {
|
|
617
|
+
expect(buildListRefBatchSelect('_00_list_ref_user_abc')).toBe(
|
|
618
|
+
'SELECT in, out, version, parent FROM $ins->_00_list_ref_user_abc'
|
|
619
|
+
);
|
|
620
|
+
expect(buildQueryRowCountBatchSelect()).toBe(
|
|
621
|
+
'SELECT VALUE { id: id, rowCount: rowCount, state: state } FROM $ins'
|
|
622
|
+
);
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it('packs due queries oldest-first under the row budget', () => {
|
|
626
|
+
const chunks = planListRefPollChunks(
|
|
627
|
+
[
|
|
628
|
+
{ hash: 'a', rows: 600, lastPolledAt: 300 },
|
|
629
|
+
{ hash: 'b', rows: 600, lastPolledAt: 100 },
|
|
630
|
+
{ hash: 'c', rows: 0, lastPolledAt: 0 },
|
|
631
|
+
{ hash: 'd', rows: 900, lastPolledAt: 200 },
|
|
632
|
+
],
|
|
633
|
+
{ now: 1_000, rowBudget: 1_400 }
|
|
634
|
+
);
|
|
635
|
+
// c (never polled) first, then b, d, a by age; d does not fit next to b.
|
|
636
|
+
expect(chunks).toEqual([['c', 'b'], ['d'], ['a']]);
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('never splits below one query and always refreshes a lone huge view', () => {
|
|
640
|
+
expect(
|
|
641
|
+
planListRefPollChunks([{ hash: 'big', rows: 10_000, lastPolledAt: 0 }], {
|
|
642
|
+
now: 100_000,
|
|
643
|
+
rowBudget: 100,
|
|
644
|
+
})
|
|
645
|
+
).toEqual([['big']]);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('defers large views until their minimum age has passed', () => {
|
|
649
|
+
const large = { hash: 'large', rows: 5_000, lastPolledAt: 990_000 };
|
|
650
|
+
const small = { hash: 'small', rows: 10, lastPolledAt: 990_000 };
|
|
651
|
+
expect(
|
|
652
|
+
planListRefPollChunks([large, small], { now: 1_000_000, largeViewRows: 1_000, largeViewMinAgeMs: 15_000 })
|
|
653
|
+
).toEqual([['small']]);
|
|
654
|
+
expect(
|
|
655
|
+
planListRefPollChunks([large, small], { now: 1_006_000, largeViewRows: 1_000, largeViewMinAgeMs: 15_000 })
|
|
656
|
+
// Same age: hash order breaks the tie, and the large view fills its own chunk.
|
|
657
|
+
).toEqual([['large'], ['small']]);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('returns no chunks when nothing is due', () => {
|
|
661
|
+
expect(planListRefPollChunks([], { now: 1 })).toEqual([]);
|
|
662
|
+
});
|
|
663
|
+
});
|