@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.
Files changed (148) hide show
  1. package/AGENTS.md +57 -0
  2. package/dist/index.d.ts +2184 -54
  3. package/dist/index.js +11515 -2399
  4. package/dist/otel/index.d.ts +2 -2
  5. package/dist/otel/index.js +6 -6
  6. package/dist/sqlite-open.js +276 -0
  7. package/dist/sqlite-worker.d.ts +1 -0
  8. package/dist/sqlite-worker.js +421 -0
  9. package/dist/tabs-broker-worker.d.ts +8 -0
  10. package/dist/tabs-broker-worker.js +434 -0
  11. package/dist/types.d.ts +688 -11
  12. package/package.json +11 -7
  13. package/scripts/check-broker-bundle.mjs +33 -0
  14. package/skills/{spooky-core → sp00ky-core}/SKILL.md +12 -12
  15. package/skills/{spooky-core → sp00ky-core}/references/auth.md +1 -1
  16. package/skills/{spooky-core → sp00ky-core}/references/config.md +2 -2
  17. package/src/bucket-blurhash.test.ts +148 -0
  18. package/src/build-globals.d.ts +12 -0
  19. package/src/events/events.test.ts +2 -1
  20. package/src/events/index.ts +3 -0
  21. package/src/index.ts +35 -2
  22. package/src/modules/app-release/index.test.ts +125 -0
  23. package/src/modules/app-release/index.ts +201 -0
  24. package/src/modules/auth/events/index.ts +2 -1
  25. package/src/modules/auth/index.ts +59 -20
  26. package/src/modules/cache/index.ts +112 -32
  27. package/src/modules/cache/types.ts +2 -2
  28. package/src/modules/crdt/crdt-field.ts +294 -0
  29. package/src/modules/crdt/crdt-hydration.test.ts +210 -0
  30. package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
  31. package/src/modules/crdt/index.ts +463 -0
  32. package/src/modules/crdt/loro-loader.ts +25 -0
  33. package/src/modules/data/data.hydration.test.ts +142 -0
  34. package/src/modules/data/data.membership.test.ts +462 -0
  35. package/src/modules/data/data.rebind.test.ts +147 -0
  36. package/src/modules/data/data.run.test.ts +113 -0
  37. package/src/modules/data/data.settled-writes.test.ts +206 -0
  38. package/src/modules/data/data.status.test.ts +249 -0
  39. package/src/modules/data/id-set-plan.test.ts +122 -0
  40. package/src/modules/data/index.ts +1580 -130
  41. package/src/modules/data/mutation-id.test.ts +25 -0
  42. package/src/modules/data/mutation-id.ts +35 -0
  43. package/src/modules/data/window-query.test.ts +52 -0
  44. package/src/modules/data/window-query.ts +194 -0
  45. package/src/modules/devtools/flags.ts +349 -0
  46. package/src/modules/devtools/index.ts +386 -37
  47. package/src/modules/devtools/notify-throttle.test.ts +149 -0
  48. package/src/modules/devtools/storage-info.test.ts +79 -0
  49. package/src/modules/devtools/storage-info.ts +168 -0
  50. package/src/modules/devtools/versions.test.ts +74 -0
  51. package/src/modules/devtools/versions.ts +110 -0
  52. package/src/modules/feature-flag/index.test.ts +251 -0
  53. package/src/modules/feature-flag/index.ts +308 -0
  54. package/src/modules/ref-tables.test.ts +91 -0
  55. package/src/modules/ref-tables.ts +88 -0
  56. package/src/modules/sync/engine.ts +101 -37
  57. package/src/modules/sync/events/index.ts +9 -2
  58. package/src/modules/sync/queue/queue-down.test.ts +107 -0
  59. package/src/modules/sync/queue/queue-down.ts +35 -6
  60. package/src/modules/sync/queue/queue-up.forwarded.test.ts +164 -0
  61. package/src/modules/sync/queue/queue-up.ts +241 -57
  62. package/src/modules/sync/scheduler.pause.test.ts +109 -0
  63. package/src/modules/sync/scheduler.retry.test.ts +156 -0
  64. package/src/modules/sync/scheduler.ts +158 -11
  65. package/src/modules/sync/sync.cleanup.test.ts +116 -0
  66. package/src/modules/sync/sync.health.test.ts +149 -0
  67. package/src/modules/sync/sync.heartbeat.test.ts +80 -0
  68. package/src/modules/sync/sync.live-removal.test.ts +134 -0
  69. package/src/modules/sync/sync.reconnect.test.ts +145 -0
  70. package/src/modules/sync/sync.subquery.test.ts +82 -0
  71. package/src/modules/sync/sync.ts +1558 -99
  72. package/src/modules/sync/utils.test.ts +269 -2
  73. package/src/modules/sync/utils.ts +201 -17
  74. package/src/otel/index.ts +13 -10
  75. package/src/services/blobs/blob-cache.test.ts +359 -0
  76. package/src/services/blobs/blob-cache.ts +603 -0
  77. package/src/services/blobs/blob-manifest.ts +227 -0
  78. package/src/services/blobs/blob-store.test.ts +77 -0
  79. package/src/services/blobs/blob-store.ts +359 -0
  80. package/src/services/blobs/blob.fixture.ts +90 -0
  81. package/src/services/blobs/index.ts +70 -0
  82. package/src/services/database/cache-engine.ts +160 -0
  83. package/src/services/database/connection-supervisor.test.ts +289 -0
  84. package/src/services/database/connection-supervisor.ts +415 -0
  85. package/src/services/database/database.query-timeout.test.ts +83 -0
  86. package/src/services/database/database.ts +32 -12
  87. package/src/services/database/engine-factory.ts +33 -0
  88. package/src/services/database/events/index.ts +2 -1
  89. package/src/services/database/index.ts +7 -0
  90. package/src/services/database/local-migrator.ts +30 -27
  91. package/src/services/database/local.test.ts +64 -0
  92. package/src/services/database/local.ts +478 -67
  93. package/src/services/database/plan-render.test.ts +159 -0
  94. package/src/services/database/plan-render.ts +108 -0
  95. package/src/services/database/relation-resolver.test.ts +413 -0
  96. package/src/services/database/relation-resolver.ts +0 -0
  97. package/src/services/database/remote.ts +110 -14
  98. package/src/services/database/sqlite-cache-engine.test.ts +558 -0
  99. package/src/services/database/sqlite-cache-engine.ts +1257 -0
  100. package/src/services/database/sqlite-devtools-queries.integration.test.ts +143 -0
  101. package/src/services/database/sqlite-devtools-queries.test.ts +154 -0
  102. package/src/services/database/sqlite-open.test.ts +150 -0
  103. package/src/services/database/sqlite-open.ts +164 -0
  104. package/src/services/database/sqlite-plan-sql.test.ts +104 -0
  105. package/src/services/database/sqlite-plan-sql.ts +106 -0
  106. package/src/services/database/sqlite-select.integration.test.ts +185 -0
  107. package/src/services/database/sqlite-select.test.ts +246 -0
  108. package/src/services/database/sqlite-select.ts +121 -0
  109. package/src/services/database/sqlite-transport.fixture.ts +30 -0
  110. package/src/services/database/sqlite-transport.ts +221 -0
  111. package/src/services/database/sqlite-worker.ts +437 -0
  112. package/src/services/database/surql-translate.ts +416 -0
  113. package/src/services/database/surreal-cache-engine.ts +141 -0
  114. package/src/services/logger/index.ts +3 -2
  115. package/src/services/persistence/localstorage.ts +2 -2
  116. package/src/services/persistence/resilient.ts +11 -4
  117. package/src/services/persistence/surrealdb.ts +10 -10
  118. package/src/services/stream-processor/index.ts +444 -52
  119. package/src/services/stream-processor/permissions.test.ts +47 -0
  120. package/src/services/stream-processor/permissions.ts +53 -0
  121. package/src/services/stream-processor/stream-processor.batch.test.ts +136 -0
  122. package/src/services/stream-processor/stream-processor.reset.test.ts +216 -0
  123. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  124. package/src/services/stream-processor/wasm-types.ts +23 -2
  125. package/src/services/tabs/broker-client.ts +283 -0
  126. package/src/services/tabs/broker.test.ts +278 -0
  127. package/src/services/tabs/coordinator.test.ts +244 -0
  128. package/src/services/tabs/coordinator.ts +576 -0
  129. package/src/services/tabs/fake-ports.fixture.ts +112 -0
  130. package/src/services/tabs/leader-locks.ts +75 -0
  131. package/src/services/tabs/protocol.ts +242 -0
  132. package/src/services/tabs/support.ts +36 -0
  133. package/src/services/tabs/tabs-broker-worker.ts +586 -0
  134. package/src/sp00ky.auth-order.test.ts +92 -0
  135. package/src/sp00ky.init-query.test.ts +183 -0
  136. package/src/sp00ky.ts +1543 -0
  137. package/src/types.ts +496 -13
  138. package/src/utils/blurhash.ts +90 -0
  139. package/src/utils/error-classification.test.ts +44 -0
  140. package/src/utils/error-classification.ts +7 -0
  141. package/src/utils/index.ts +73 -13
  142. package/src/utils/parser.ts +3 -2
  143. package/src/utils/semver.test.ts +32 -0
  144. package/src/utils/semver.ts +30 -0
  145. package/src/utils/surql.ts +30 -18
  146. package/src/utils/withRetry.test.ts +1 -1
  147. package/tsdown.config.ts +86 -1
  148. package/src/spooky.ts +0 -395
@@ -0,0 +1,421 @@
1
+ import { a as renderWhereSql, c as resolveRelations, i as renderOrderSql, l as stableKey, n as openDb, o as reviveRow, r as project } from "./sqlite-open.js";
2
+ import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
3
+
4
+ //#region src/services/database/sqlite-select.ts
5
+ function ensureTable(db, table) {
6
+ if (db.knownTables.has(table)) return;
7
+ db.run(`CREATE TABLE IF NOT EXISTS "${table}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)`);
8
+ db.knownTables.add(table);
9
+ }
10
+ function execRows(db, sql, bind) {
11
+ return db.exec(sql, bind).map((r) => reviveRow(r.data));
12
+ }
13
+ /** Mirrors the engine's `selectByIds`: fetch by primary id, preserving `ids`
14
+ * order unless an ORDER BY overrides it. */
15
+ function selectByIds(db, table, ids, opts) {
16
+ if (ids.length === 0) return [];
17
+ ensureTable(db, table);
18
+ const keys = ids.map(stableKey);
19
+ let sql = `SELECT data FROM "${table}" WHERE id IN (${keys.map(() => "?").join(", ")})`;
20
+ if (opts?.orderBy && opts.orderBy.length > 0) sql += renderOrderSql(opts.orderBy);
21
+ let rows = execRows(db, sql, keys);
22
+ if (!opts?.orderBy || opts.orderBy.length === 0) {
23
+ const pos = new Map(keys.map((k, i) => [k, i]));
24
+ rows = rows.sort((a, b) => (pos.get(stableKey(a.id)) ?? 0) - (pos.get(stableKey(b.id)) ?? 0));
25
+ }
26
+ return opts?.select ? rows.map((r) => project(r, opts.select)) : rows;
27
+ }
28
+ /** Mirrors the engine's `fetchRelation` SQL exactly. */
29
+ function fetchRelation(db, req) {
30
+ ensureTable(db, req.table);
31
+ const keys = req.keys.map(stableKey);
32
+ const placeholders = keys.map(() => "?").join(", ");
33
+ const bind = [...keys];
34
+ const lhs = req.matchField === "id" ? "id" : `json_extract(data, '$.${req.matchField}')`;
35
+ let sql = `SELECT data FROM "${req.table}" WHERE ${lhs} IN (${placeholders})`;
36
+ if (req.where && req.where.length > 0) sql += ` AND ${renderWhereSql(req.where, bind, {})}`;
37
+ if (req.orderBy && req.orderBy.length > 0) sql += renderOrderSql(req.orderBy);
38
+ const rows = execRows(db, sql, bind);
39
+ return req.select ? rows.map((r) => project(r, req.select)) : rows;
40
+ }
41
+ async function executeSelect(plan, params, db) {
42
+ const counter = { n: 0 };
43
+ const fetcher = { fetchRelation: (req) => {
44
+ counter.n++;
45
+ return Promise.resolve(fetchRelation(db, req));
46
+ } };
47
+ if (plan.ids) {
48
+ const rows = selectByIds(db, plan.table, plan.ids, {
49
+ select: plan.select,
50
+ orderBy: plan.orderBy
51
+ });
52
+ await resolveRelations(rows, plan.relations, fetcher);
53
+ return {
54
+ rows,
55
+ relationFetches: counter.n
56
+ };
57
+ }
58
+ ensureTable(db, plan.table);
59
+ const bind = [];
60
+ let sql = `SELECT data FROM "${plan.table}"`;
61
+ if (plan.where && plan.where.length > 0) sql += ` WHERE ${renderWhereSql(plan.where, bind, params)}`;
62
+ if (plan.orderBy && plan.orderBy.length > 0) sql += renderOrderSql(plan.orderBy);
63
+ else sql += ` ORDER BY id`;
64
+ if (plan.limit !== void 0) sql += ` LIMIT ${Number(plan.limit)}`;
65
+ if (plan.offset !== void 0) sql += ` OFFSET ${Number(plan.offset)}`;
66
+ const rows = execRows(db, sql, bind);
67
+ const projected = plan.select ? rows.map((r) => project(r, plan.select)) : rows;
68
+ await resolveRelations(projected, plan.relations, fetcher);
69
+ return {
70
+ rows: projected,
71
+ relationFetches: counter.n
72
+ };
73
+ }
74
+
75
+ //#endregion
76
+ //#region src/services/database/sqlite-worker.ts
77
+ /**
78
+ * Dedicated Web Worker that owns the SQLite-WASM handle. All DB access is
79
+ * funnelled through here (the main thread never touches the wasm module), which
80
+ * is also what the OPFS VFS requires — file access must happen off the main
81
+ * thread. Persistence uses the **OPFS SAHPool VFS**: durable, and (unlike the
82
+ * classic OPFS VFS) it does NOT require COOP/COEP cross-origin isolation
83
+ * headers, so host apps embedding the client need no server changes. When OPFS
84
+ * is unavailable it retries, then falls back to an in-memory DB and REPORTS the
85
+ * loss of durability (see `sqlite-open.ts`) instead of degrading silently.
86
+ *
87
+ * Message protocol (request/response keyed by `id`; replies go to the channel
88
+ * the request arrived on):
89
+ * { id, type: 'open', payload: { dbName, useOpfs, workerLockName? } }
90
+ * -> { id, ok, persisted, opfsError? }
91
+ * { id, type: 'exec', payload: { sql, bind } } -> { id, ok, rows }
92
+ * { id, type: 'run', payload: { sql, bind } } -> { id, ok }
93
+ * { id, type: 'batch', payload: [{ sql, bind }] } (atomic BEGIN/COMMIT)
94
+ * { id, type: 'select', payload: { plan, params } } -> { id, ok, rows, relationFetches }
95
+ * { id, type: 'close' }
96
+ * { id, type: 'shutdown' } (owner only: close + pauseVfs + self.close)
97
+ * { id, type: 'relock', payload: { workerLockName } } (owner only)
98
+ * { id, type: 'add-client', payload: { clientId } } + ev.ports[0] (owner only)
99
+ * { id, type: 'remove-client', payload: { clientId } } (owner only)
100
+ *
101
+ * Multi-client (shared-tabs mode): the OWNER (the leader tab's engine) speaks
102
+ * on the worker's own channel and controls the lifecycle. Follower tabs get a
103
+ * MessagePort each (`add-client`) and may only issue data ops
104
+ * (exec/run/batch/select). Every op, from any channel, runs through ONE op
105
+ * chain so an async `select` from one client can never interleave with another
106
+ * client's `batch` at the VFS layer.
107
+ *
108
+ * Leader fencing (shared-tabs mode): `open` may carry `workerLockName`, a Web
109
+ * Lock this worker acquires BEFORE opening the pool and holds for the DB's
110
+ * lifetime. The name is unique per leadership, so the lock being gone has
111
+ * exactly one meaning: the broker stole it because this tab was presumed dead
112
+ * (usually frozen). When that happens the worker FENCES itself: closes the DB,
113
+ * releases the OPFS sync access handles (pauseVfs), tells the owner, and
114
+ * refuses every further op. A frozen-then-thawed ex-leader therefore can never
115
+ * write the durable file after a new leader took over. The thaw gate below
116
+ * closes the wake-up ordering race where a queued write could run before the
117
+ * lock-loss callback fires.
118
+ *
119
+ * `select` executes a whole QueryPlan — table creation, base select, the full
120
+ * `.related()` tree (shared `resolveRelations`), and JSON row parsing — in ONE
121
+ * round-trip, returning structured-clone row objects. This is the first-load
122
+ * hot path; the per-statement ops remain for the write/shim paths.
123
+ */
124
+ let db = null;
125
+ let pool = null;
126
+ /** Once fenced, every op is refused and the DB stays closed. Terminal. */
127
+ let fenced = false;
128
+ /** The per-leadership lock currently held (null = no fencing active). A
129
+ * relock (same-leader re-promotion after a broker restart) briefly holds two
130
+ * locks: the new one is installed here first, then the old one's own release
131
+ * is called, so there is no window where neither is held. */
132
+ let currentLock = null;
133
+ function workerLocks() {
134
+ const nav = globalThis.navigator;
135
+ return nav?.locks && typeof nav.locks.request === "function" ? nav.locks : null;
136
+ }
137
+ /**
138
+ * Acquire `name` exclusively without waiting. Resolves with a handle when
139
+ * granted, null when someone else holds it. If the request settles while the
140
+ * holder still believes it owns the lock, the broker stole it: fence.
141
+ */
142
+ function acquireWorkerLock(name) {
143
+ const locks = workerLocks();
144
+ if (!locks) return Promise.resolve({
145
+ name,
146
+ release() {}
147
+ });
148
+ return new Promise((resolve) => {
149
+ let granted = false;
150
+ let intentional = false;
151
+ let releaseGrant = null;
152
+ locks.request(name, {
153
+ mode: "exclusive",
154
+ ifAvailable: true
155
+ }, (lock) => {
156
+ if (!lock) {
157
+ resolve(null);
158
+ return;
159
+ }
160
+ granted = true;
161
+ resolve({
162
+ name,
163
+ release() {
164
+ intentional = true;
165
+ releaseGrant?.();
166
+ }
167
+ });
168
+ return new Promise((release) => {
169
+ releaseGrant = release;
170
+ });
171
+ }).then(() => {
172
+ if (granted && !intentional) fence("worker lock stolen");
173
+ }, () => {
174
+ if (granted && !intentional) fence("worker lock request failed");
175
+ });
176
+ });
177
+ }
178
+ function releaseHeldLock() {
179
+ currentLock?.release();
180
+ currentLock = null;
181
+ }
182
+ /**
183
+ * Terminal teardown after leadership loss. Closing the DB first makes
184
+ * pauseVfs legal (it throws while files are open); pausing releases the sync
185
+ * access handles so the NEW leader's open succeeds without waiting for this
186
+ * worker to be garbage collected.
187
+ */
188
+ async function fence(reason) {
189
+ if (fenced) return;
190
+ fenced = true;
191
+ console.error(`[sp00ky] sqlite worker fenced (${reason}): leadership was taken over, closing the database and refusing further ops.`);
192
+ try {
193
+ db?.close();
194
+ } catch {}
195
+ db = null;
196
+ try {
197
+ pool?.pauseVfs?.();
198
+ } catch {}
199
+ pool = null;
200
+ try {
201
+ self.postMessage({
202
+ type: "lock-lost",
203
+ reason
204
+ });
205
+ } catch {}
206
+ releaseHeldLock();
207
+ self.close();
208
+ }
209
+ /**
210
+ * A frozen tab's worker resumes with its message queue intact, and a queued
211
+ * write could run BEFORE the lock-steal callback fires. Both the 1s interval
212
+ * and every op call `noteTick()`; whichever runs first after a long gap kicks
213
+ * off a lock verification, and ops await it before touching the DB. The gap
214
+ * threshold must stay BELOW the broker's pong timeout: a freeze shorter than
215
+ * the threshold cannot have triggered a steal yet, so skipping the check for
216
+ * small gaps is safe.
217
+ */
218
+ const FREEZE_SUSPECT_MS = 1e4;
219
+ let lastTickAt = performance.now();
220
+ let thawVerification = null;
221
+ function noteTick() {
222
+ const now = performance.now();
223
+ const gap = now - lastTickAt;
224
+ lastTickAt = now;
225
+ if (gap > FREEZE_SUSPECT_MS && currentLock && !fenced && !thawVerification) thawVerification = verifyLockStillHeld().finally(() => {
226
+ thawVerification = null;
227
+ });
228
+ }
229
+ setInterval(noteTick, 1e3);
230
+ async function verifyLockStillHeld() {
231
+ const locks = workerLocks();
232
+ const name = currentLock?.name;
233
+ if (!locks || !name || typeof locks.query !== "function") return;
234
+ try {
235
+ if (!((await locks.query()).held ?? []).some((l) => l.name === name)) await fence("lock missing after suspected freeze");
236
+ } catch {}
237
+ }
238
+ async function open(dbName, useOpfs, systemTables = [], workerLockName, openOptions) {
239
+ if (workerLockName) {
240
+ const handle = await acquireWorkerLock(workerLockName);
241
+ if (!handle) throw new Error("worker-lock-unavailable");
242
+ releaseHeldLock();
243
+ currentLock = handle;
244
+ }
245
+ const result = await openDb(await sqlite3InitModule(), dbName, useOpfs, openOptions ?? {});
246
+ db = result.db;
247
+ pool = result.pool ?? null;
248
+ for (const t of systemTables) db.exec({ sql: `CREATE TABLE IF NOT EXISTS "${t}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)` });
249
+ try {
250
+ db.exec({ sql: "PRAGMA busy_timeout = 5000; PRAGMA cache_size = -32000;" });
251
+ } catch {}
252
+ return {
253
+ persisted: result.persisted,
254
+ opfsError: result.opfsError
255
+ };
256
+ }
257
+ function closeDb() {
258
+ db?.close();
259
+ db = null;
260
+ pool = null;
261
+ selectDb.knownTables.clear();
262
+ releaseHeldLock();
263
+ }
264
+ function exec(sql, bind) {
265
+ if (!db) throw new Error("sqlite: DB not open");
266
+ return db.exec({
267
+ sql,
268
+ bind,
269
+ rowMode: "object",
270
+ returnValue: "resultRows"
271
+ });
272
+ }
273
+ function run(sql, bind) {
274
+ if (!db) throw new Error("sqlite: DB not open");
275
+ db.exec({
276
+ sql,
277
+ bind
278
+ });
279
+ }
280
+ function batch(stmts) {
281
+ if (!db) throw new Error("sqlite: DB not open");
282
+ db.exec({ sql: "BEGIN" });
283
+ try {
284
+ for (const s of stmts) db.exec({
285
+ sql: s.sql,
286
+ bind: s.bind
287
+ });
288
+ db.exec({ sql: "COMMIT" });
289
+ } catch (e) {
290
+ try {
291
+ db.exec({ sql: "ROLLBACK" });
292
+ } catch {}
293
+ throw e;
294
+ }
295
+ }
296
+ /** DB handle for `executeSelect` (see `sqlite-select.ts`, the unit-testable
297
+ * plan executor). `knownTables` is cleared on open/close, which covers both
298
+ * the solo fresh-worker path and the shared-mode same-worker bucket switch. */
299
+ const selectDb = {
300
+ exec: (sql, bind) => exec(sql, bind),
301
+ run,
302
+ knownTables: /* @__PURE__ */ new Set()
303
+ };
304
+ /** Follower client ports, keyed by the clientId the owner assigned. */
305
+ const clients = /* @__PURE__ */ new Map();
306
+ /** Data ops a follower client port may issue; lifecycle stays owner-only. */
307
+ const CLIENT_OPS = new Set([
308
+ "exec",
309
+ "run",
310
+ "batch",
311
+ "select"
312
+ ]);
313
+ /**
314
+ * ONE chain for every op from every channel. The handlers for `open` and
315
+ * `select` are async; without this, a follower's select could interleave with
316
+ * the owner's batch mid-transaction at the VFS layer. (Solo mode kept this
317
+ * invariant on the main thread via the engine's opQueue; with multiple client
318
+ * ports only the worker can.)
319
+ */
320
+ let opChain = Promise.resolve();
321
+ async function handle(type, payload, source) {
322
+ if (fenced) throw new Error("sqlite: fenced, leadership was taken over");
323
+ if (source === "client" && !CLIENT_OPS.has(type)) throw new Error(`sqlite worker: op ${type} is not allowed on a client port`);
324
+ switch (type) {
325
+ case "open":
326
+ selectDb.knownTables.clear();
327
+ {
328
+ const result = await open(payload.dbName, payload.useOpfs, payload.systemTables, payload.workerLockName, payload.openOptions);
329
+ for (const t of payload.systemTables ?? []) selectDb.knownTables.add(t);
330
+ return result;
331
+ }
332
+ case "relock": {
333
+ const handle = await acquireWorkerLock(payload.workerLockName);
334
+ if (!handle) throw new Error("worker-lock-unavailable");
335
+ releaseHeldLock();
336
+ currentLock = handle;
337
+ return {};
338
+ }
339
+ case "select": return executeSelect(payload.plan, payload.params ?? {}, selectDb);
340
+ case "exec": return { rows: exec(payload.sql, payload.bind) };
341
+ case "run":
342
+ run(payload.sql, payload.bind);
343
+ return {};
344
+ case "batch":
345
+ batch(payload);
346
+ return {};
347
+ case "close":
348
+ closeDb();
349
+ return {};
350
+ case "shutdown":
351
+ try {
352
+ db?.close();
353
+ } catch {}
354
+ db = null;
355
+ try {
356
+ pool?.pauseVfs?.();
357
+ } catch {}
358
+ pool = null;
359
+ releaseHeldLock();
360
+ queueMicrotask(() => self.close());
361
+ return {};
362
+ case "remove-client":
363
+ clients.get(payload.clientId)?.close();
364
+ clients.delete(payload.clientId);
365
+ return {};
366
+ default: throw new Error(`sqlite worker: unknown message ${type}`);
367
+ }
368
+ }
369
+ function dispatch(target, data, source) {
370
+ const { id, type, payload } = data ?? {};
371
+ noteTick();
372
+ const t0 = performance.now();
373
+ const runOp = async () => {
374
+ try {
375
+ if (thawVerification) await thawVerification;
376
+ const result = await handle(type, payload, source);
377
+ target.postMessage({
378
+ id,
379
+ ok: true,
380
+ wt: performance.now() - t0,
381
+ ...result
382
+ });
383
+ } catch (err) {
384
+ target.postMessage({
385
+ id,
386
+ ok: false,
387
+ error: err instanceof Error ? err.message : String(err)
388
+ });
389
+ }
390
+ };
391
+ opChain = opChain.then(runOp, runOp);
392
+ }
393
+ self.onmessage = (ev) => {
394
+ const data = ev.data ?? {};
395
+ if (data.type === "add-client") {
396
+ const port = ev.ports?.[0];
397
+ const clientId = data.payload?.clientId;
398
+ if (!port || !clientId) {
399
+ self.postMessage({
400
+ id: data.id,
401
+ ok: false,
402
+ error: "sqlite worker: add-client needs a clientId and a transferred port"
403
+ });
404
+ return;
405
+ }
406
+ clients.get(clientId)?.close();
407
+ clients.set(clientId, port);
408
+ port.onmessage = (pe) => dispatch(port, pe.data, "client");
409
+ port.start?.();
410
+ self.postMessage({
411
+ id: data.id,
412
+ ok: true,
413
+ wt: 0
414
+ });
415
+ return;
416
+ }
417
+ dispatch(self, data, "owner");
418
+ };
419
+
420
+ //#endregion
421
+ export { };
@@ -0,0 +1,8 @@
1
+ //#region src/services/tabs/tabs-broker-worker.d.ts
2
+ /** Attach a connecting tab's port. Exported for the node test harness, which
3
+ * drives the broker with fake MessagePort pairs instead of a SharedWorker. */
4
+ declare function handleConnect(port: MessagePort): void;
5
+ /** Test-only: wipe module state so each test starts with a fresh broker. */
6
+ declare function __resetBrokerForTests(): void;
7
+ //#endregion
8
+ export { __resetBrokerForTests, handleConnect };