@spooky-sync/core 0.0.1-canary.20 → 0.0.1-canary.201
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 +57 -0
- package/dist/index.d.ts +2184 -54
- package/dist/index.js +11515 -2399
- package/dist/otel/index.d.ts +2 -2
- package/dist/otel/index.js +6 -6
- package/dist/sqlite-open.js +276 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +421 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +434 -0
- package/dist/types.d.ts +688 -11
- package/package.json +11 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/skills/{spooky-core → sp00ky-core}/SKILL.md +12 -12
- package/skills/{spooky-core → sp00ky-core}/references/auth.md +1 -1
- package/skills/{spooky-core → sp00ky-core}/references/config.md +2 -2
- package/src/bucket-blurhash.test.ts +148 -0
- package/src/build-globals.d.ts +12 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +35 -2
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +201 -0
- package/src/modules/auth/events/index.ts +2 -1
- package/src/modules/auth/index.ts +59 -20
- package/src/modules/cache/index.ts +112 -32
- package/src/modules/cache/types.ts +2 -2
- 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/data/data.hydration.test.ts +142 -0
- package/src/modules/data/data.membership.test.ts +462 -0
- package/src/modules/data/data.rebind.test.ts +147 -0
- package/src/modules/data/data.run.test.ts +113 -0
- package/src/modules/data/data.settled-writes.test.ts +206 -0
- package/src/modules/data/data.status.test.ts +249 -0
- package/src/modules/data/id-set-plan.test.ts +122 -0
- package/src/modules/data/index.ts +1580 -130
- package/src/modules/data/mutation-id.test.ts +25 -0
- package/src/modules/data/mutation-id.ts +35 -0
- package/src/modules/data/window-query.test.ts +52 -0
- package/src/modules/data/window-query.ts +194 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +386 -37
- package/src/modules/devtools/notify-throttle.test.ts +149 -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 +251 -0
- package/src/modules/feature-flag/index.ts +308 -0
- package/src/modules/ref-tables.test.ts +91 -0
- package/src/modules/ref-tables.ts +88 -0
- package/src/modules/sync/engine.ts +101 -37
- package/src/modules/sync/events/index.ts +9 -2
- package/src/modules/sync/queue/queue-down.test.ts +107 -0
- package/src/modules/sync/queue/queue-down.ts +35 -6
- package/src/modules/sync/queue/queue-up.forwarded.test.ts +164 -0
- package/src/modules/sync/queue/queue-up.ts +241 -57
- package/src/modules/sync/scheduler.pause.test.ts +109 -0
- package/src/modules/sync/scheduler.retry.test.ts +156 -0
- package/src/modules/sync/scheduler.ts +158 -11
- package/src/modules/sync/sync.cleanup.test.ts +116 -0
- package/src/modules/sync/sync.health.test.ts +149 -0
- package/src/modules/sync/sync.heartbeat.test.ts +80 -0
- package/src/modules/sync/sync.live-removal.test.ts +134 -0
- package/src/modules/sync/sync.reconnect.test.ts +145 -0
- package/src/modules/sync/sync.subquery.test.ts +82 -0
- package/src/modules/sync/sync.ts +1558 -99
- package/src/modules/sync/utils.test.ts +269 -2
- package/src/modules/sync/utils.ts +201 -17
- package/src/otel/index.ts +13 -10
- 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 +160 -0
- package/src/services/database/connection-supervisor.test.ts +289 -0
- package/src/services/database/connection-supervisor.ts +415 -0
- package/src/services/database/database.query-timeout.test.ts +83 -0
- package/src/services/database/database.ts +32 -12
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/local-migrator.ts +30 -27
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +478 -67
- 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 +413 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +110 -14
- package/src/services/database/sqlite-cache-engine.test.ts +558 -0
- package/src/services/database/sqlite-cache-engine.ts +1257 -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-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +104 -0
- package/src/services/database/sqlite-plan-sql.ts +106 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +246 -0
- package/src/services/database/sqlite-select.ts +121 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +221 -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 +141 -0
- package/src/services/logger/index.ts +3 -2
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +11 -4
- package/src/services/persistence/surrealdb.ts +10 -10
- package/src/services/stream-processor/index.ts +444 -52
- 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 +136 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +216 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +23 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +278 -0
- package/src/services/tabs/coordinator.test.ts +244 -0
- package/src/services/tabs/coordinator.ts +576 -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 +242 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +586 -0
- package/src/sp00ky.auth-order.test.ts +92 -0
- package/src/sp00ky.init-query.test.ts +183 -0
- package/src/sp00ky.ts +1543 -0
- package/src/types.ts +496 -13
- package/src/utils/blurhash.ts +90 -0
- package/src/utils/error-classification.test.ts +44 -0
- package/src/utils/error-classification.ts +7 -0
- package/src/utils/index.ts +73 -13
- package/src/utils/parser.ts +3 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/withRetry.test.ts +1 -1
- package/tsdown.config.ts +86 -1
- package/src/spooky.ts +0 -395
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { Sp00kyClient } from './sp00ky';
|
|
5
|
+
|
|
6
|
+
// Guards for the local-first paint contract of `Sp00kyClient.initQuery`:
|
|
7
|
+
// the hash must resolve (and the hook paint from local cache) without any
|
|
8
|
+
// network on the awaited path, while the opt-in instant-hydrate + the
|
|
9
|
+
// `register` down-event run in a background chain that (a) keeps hydrate
|
|
10
|
+
// strictly before the enqueue, (b) hydrates only when `instantHydrate:
|
|
11
|
+
// true` (default off — the register lifecycle is the single freshness
|
|
12
|
+
// path), (c) never rejects, and (d) is shared by concurrent mounts of the
|
|
13
|
+
// same query.
|
|
14
|
+
//
|
|
15
|
+
// Structural half: like sp00ky.auth-order.test.ts, a regex over the source
|
|
16
|
+
// catches the ordering regressions a runtime mock can't cheaply cover
|
|
17
|
+
// (Sp00kyClient's constructor drags in SurrealDB + the WASM SSP).
|
|
18
|
+
// Behavioral half: `finishQueryInit`/`initQuery` only touch a handful of
|
|
19
|
+
// injected fields, so a bare `Object.create(Sp00kyClient.prototype)` with
|
|
20
|
+
// stubs exercises the real method bodies without the constructor.
|
|
21
|
+
|
|
22
|
+
describe('Sp00kyClient.initQuery structural invariants', () => {
|
|
23
|
+
const source = readFileSync(resolve(__dirname, 'sp00ky.ts'), 'utf-8');
|
|
24
|
+
|
|
25
|
+
const methodBody = (name: string): string => {
|
|
26
|
+
const match = source.match(new RegExp(`private async ${name}[\\s\\S]*?\\n \\}`));
|
|
27
|
+
expect(match, `expected a "private async ${name}" method in sp00ky.ts`).not.toBeNull();
|
|
28
|
+
// Strip line comments so prose mentioning awaits/calls can't trip the checks.
|
|
29
|
+
return match![0]
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map((line) => line.replace(/\/\/.*$/, ''))
|
|
32
|
+
.join('\n');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
it('initQuery returns the hash with no network on the awaited path', () => {
|
|
36
|
+
const body = methodBody('initQuery');
|
|
37
|
+
expect(body).toContain('return hash');
|
|
38
|
+
expect(body, 'initQuery must not await the remote (paint path is network-free)').not.toMatch(
|
|
39
|
+
/await this\.remote\./
|
|
40
|
+
);
|
|
41
|
+
expect(body, 'the register enqueue belongs to the background chain').not.toMatch(
|
|
42
|
+
/await this\.sync\.enqueueDownEvent/
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('finishQueryInit hydrates strictly before enqueuing register', () => {
|
|
47
|
+
const body = methodBody('finishQueryInit');
|
|
48
|
+
const fetchIdx = body.indexOf('this.remote.query');
|
|
49
|
+
const enqueueIdx = body.indexOf('this.sync.enqueueDownEvent');
|
|
50
|
+
expect(fetchIdx).toBeGreaterThanOrEqual(0);
|
|
51
|
+
expect(enqueueIdx).toBeGreaterThanOrEqual(0);
|
|
52
|
+
expect(
|
|
53
|
+
fetchIdx,
|
|
54
|
+
'hydrate must settle before the register enqueue so a stale one-shot snapshot can never land after the authoritative _00_list_ref overwrite'
|
|
55
|
+
).toBeLessThan(enqueueIdx);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('finishQueryInit captures the bucket epoch before the remote fetch', () => {
|
|
59
|
+
const body = methodBody('finishQueryInit');
|
|
60
|
+
const epochIdx = body.indexOf('this.local.epoch');
|
|
61
|
+
const fetchIdx = body.indexOf('this.remote.query');
|
|
62
|
+
expect(epochIdx).toBeGreaterThanOrEqual(0);
|
|
63
|
+
expect(epochIdx, 'epoch must be read before the fetch to fence bucket switches').toBeLessThan(
|
|
64
|
+
fetchIdx
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('Sp00kyClient.finishQueryInit behavior', () => {
|
|
70
|
+
const hash = 'sha-hash';
|
|
71
|
+
const q: any = { hash: 42, selectQuery: { query: 'SELECT * FROM user', vars: {} } };
|
|
72
|
+
|
|
73
|
+
let client: any;
|
|
74
|
+
let calls: string[];
|
|
75
|
+
let enqueued: any[];
|
|
76
|
+
let epoch: number;
|
|
77
|
+
let cold: boolean;
|
|
78
|
+
let remoteImpl: () => Promise<any>;
|
|
79
|
+
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
calls = [];
|
|
82
|
+
enqueued = [];
|
|
83
|
+
epoch = 1;
|
|
84
|
+
cold = true;
|
|
85
|
+
remoteImpl = async () => {
|
|
86
|
+
calls.push('fetch');
|
|
87
|
+
return [[{ id: 'user:a' }]];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const noop = () => {};
|
|
91
|
+
const logger: any = { debug: noop, info: noop, warn: noop, error: noop, trace: noop };
|
|
92
|
+
logger.child = () => logger;
|
|
93
|
+
|
|
94
|
+
client = Object.create(Sp00kyClient.prototype);
|
|
95
|
+
Object.assign(client, {
|
|
96
|
+
config: { instantHydrate: true, schema: { tables: [{ name: 'user', columns: {} }] } },
|
|
97
|
+
logger,
|
|
98
|
+
preloadedHashes: new Set<number>(),
|
|
99
|
+
pendingQueryInits: new Map<string, Promise<void>>(),
|
|
100
|
+
local: {
|
|
101
|
+
get epoch() {
|
|
102
|
+
return epoch;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
remote: { query: (...args: any[]) => remoteImpl() },
|
|
106
|
+
sync: {
|
|
107
|
+
enqueueDownEvent: (event: any) => {
|
|
108
|
+
calls.push('enqueue');
|
|
109
|
+
enqueued.push(event);
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
dataModule: {
|
|
113
|
+
isCold: () => cold,
|
|
114
|
+
applyHydration: async () => {
|
|
115
|
+
calls.push('hydrate');
|
|
116
|
+
},
|
|
117
|
+
query: async () => hash,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('cold path with hydrate enabled: fetch → hydrate → enqueue, in order', async () => {
|
|
123
|
+
await client.finishQueryInit(hash, q, {});
|
|
124
|
+
expect(calls).toEqual(['fetch', 'hydrate', 'enqueue']);
|
|
125
|
+
expect(enqueued).toEqual([{ type: 'register', payload: { hash } }]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('a preloaded query still hydrates when enabled — cache-first never depends on WHY rows are cached', async () => {
|
|
129
|
+
client.preloadedHashes.add(q.hash);
|
|
130
|
+
await client.finishQueryInit(hash, q, {});
|
|
131
|
+
expect(calls).toEqual(['fetch', 'hydrate', 'enqueue']);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('warm query (not cold) goes straight to enqueue', async () => {
|
|
135
|
+
cold = false;
|
|
136
|
+
await client.finishQueryInit(hash, q, {});
|
|
137
|
+
expect(calls).toEqual(['enqueue']);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('a rejected hydrate fetch still enqueues register and never rejects', async () => {
|
|
141
|
+
remoteImpl = async () => {
|
|
142
|
+
calls.push('fetch');
|
|
143
|
+
throw new Error('offline');
|
|
144
|
+
};
|
|
145
|
+
await expect(client.finishQueryInit(hash, q, {})).resolves.toBeUndefined();
|
|
146
|
+
expect(calls).toEqual(['fetch', 'enqueue']);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('a bucket switch during the fetch skips applyHydration', async () => {
|
|
150
|
+
remoteImpl = async () => {
|
|
151
|
+
calls.push('fetch');
|
|
152
|
+
epoch = 2; // switch lands while the fetch is in flight
|
|
153
|
+
return [[{ id: 'user:a' }]];
|
|
154
|
+
};
|
|
155
|
+
await client.finishQueryInit(hash, q, {});
|
|
156
|
+
expect(calls).toEqual(['fetch', 'enqueue']);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('default (instantHydrate unset) does not hydrate — register lifecycle is the only freshness path', async () => {
|
|
160
|
+
delete client.config.instantHydrate;
|
|
161
|
+
await client.finishQueryInit(hash, q, {});
|
|
162
|
+
expect(calls).toEqual(['enqueue']);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('instantHydrate: false does not hydrate', async () => {
|
|
166
|
+
client.config.instantHydrate = false;
|
|
167
|
+
await client.finishQueryInit(hash, q, {});
|
|
168
|
+
expect(calls).toEqual(['enqueue']);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('concurrent initQuery calls for the same hash share one background chain', async () => {
|
|
172
|
+
const [h1, h2] = await Promise.all([
|
|
173
|
+
client.initQuery('user', q, '10m'),
|
|
174
|
+
client.initQuery('user', q, '10m'),
|
|
175
|
+
]);
|
|
176
|
+
expect(h1).toBe(hash);
|
|
177
|
+
expect(h2).toBe(hash);
|
|
178
|
+
// Let the shared chain drain.
|
|
179
|
+
await Promise.all([...client.pendingQueryInits.values()]);
|
|
180
|
+
expect(calls).toEqual(['fetch', 'hydrate', 'enqueue']);
|
|
181
|
+
expect(client.pendingQueryInits.size).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
});
|