@spooky-sync/core 0.0.1-canary.135 → 0.0.1-canary.137
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/dist/index.js
CHANGED
|
@@ -1440,6 +1440,20 @@ function setPath(obj, path, value) {
|
|
|
1440
1440
|
* full merged row is only materialized for a LET-wrapped upsert (see the 'let'
|
|
1441
1441
|
* case). delete/deleteAll yield `[]`; noop yields `null`.
|
|
1442
1442
|
*/
|
|
1443
|
+
/**
|
|
1444
|
+
* The `_00_*` internal tables the client relies on. The LocalMigrator DEFINEs
|
|
1445
|
+
* them, but every DEFINE lowers to a noop on the SQLite engine, so they must be
|
|
1446
|
+
* created physically at open (see `openInternal`) or a read-before-first-write
|
|
1447
|
+
* on a fresh bucket throws "no such table". Keep in sync with the systemSchema
|
|
1448
|
+
* block in `local-migrator.ts`.
|
|
1449
|
+
*/
|
|
1450
|
+
const SYSTEM_TABLES = [
|
|
1451
|
+
"_00_stream_processor_state",
|
|
1452
|
+
"_00_query",
|
|
1453
|
+
"_00_preload",
|
|
1454
|
+
"_00_schema",
|
|
1455
|
+
"_00_pending_mutations"
|
|
1456
|
+
];
|
|
1443
1457
|
function pureWriteOpResult(op) {
|
|
1444
1458
|
switch (op.kind) {
|
|
1445
1459
|
case "upsert": return {
|
|
@@ -1594,9 +1608,11 @@ var SqliteCacheEngine = class {
|
|
|
1594
1608
|
this.worker = this.spawnWorker();
|
|
1595
1609
|
const { persisted } = await this.rawCall("open", {
|
|
1596
1610
|
dbName: bucketId,
|
|
1597
|
-
useOpfs: this.useOpfs
|
|
1611
|
+
useOpfs: this.useOpfs,
|
|
1612
|
+
systemTables: SYSTEM_TABLES
|
|
1598
1613
|
});
|
|
1599
1614
|
this.knownTables.clear();
|
|
1615
|
+
for (const t of SYSTEM_TABLES) this.knownTables.add(t);
|
|
1600
1616
|
this.bucketId = bucketId;
|
|
1601
1617
|
this.logger.info({
|
|
1602
1618
|
bucketId,
|
|
@@ -5073,8 +5089,8 @@ function parseBackendInfo(raw) {
|
|
|
5073
5089
|
|
|
5074
5090
|
//#endregion
|
|
5075
5091
|
//#region src/modules/devtools/index.ts
|
|
5076
|
-
const CORE_VERSION = "0.0.1-canary.
|
|
5077
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
5092
|
+
const CORE_VERSION = "0.0.1-canary.137";
|
|
5093
|
+
const WASM_VERSION = "0.0.1-canary.137";
|
|
5078
5094
|
const SURREAL_VERSION = "3.0.3";
|
|
5079
5095
|
var DevToolsService = class {
|
|
5080
5096
|
eventsHistory = [];
|
package/dist/sqlite-worker.js
CHANGED
|
@@ -96,7 +96,7 @@ async function executeSelect(plan, params, db) {
|
|
|
96
96
|
* hot path; the per-statement ops remain for the write/shim paths.
|
|
97
97
|
*/
|
|
98
98
|
let db = null;
|
|
99
|
-
async function open(dbName, useOpfs) {
|
|
99
|
+
async function open(dbName, useOpfs, systemTables = []) {
|
|
100
100
|
const sqlite3 = await sqlite3InitModule();
|
|
101
101
|
let persisted = false;
|
|
102
102
|
if (useOpfs && sqlite3.installOpfsSAHPoolVfs) try {
|
|
@@ -104,6 +104,7 @@ async function open(dbName, useOpfs) {
|
|
|
104
104
|
persisted = true;
|
|
105
105
|
} catch {}
|
|
106
106
|
if (!db) db = new sqlite3.oo1.DB(":memory:", "c");
|
|
107
|
+
for (const t of systemTables) db.exec({ sql: `CREATE TABLE IF NOT EXISTS "${t}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)` });
|
|
107
108
|
try {
|
|
108
109
|
db.exec({ sql: "PRAGMA busy_timeout = 5000; PRAGMA cache_size = -32000;" });
|
|
109
110
|
} catch {}
|
|
@@ -157,7 +158,8 @@ self.onmessage = async (ev) => {
|
|
|
157
158
|
switch (type) {
|
|
158
159
|
case "open":
|
|
159
160
|
selectDb.knownTables.clear();
|
|
160
|
-
result = await open(payload.dbName, payload.useOpfs);
|
|
161
|
+
result = await open(payload.dbName, payload.useOpfs, payload.systemTables);
|
|
162
|
+
for (const t of payload.systemTables ?? []) selectDb.knownTables.add(t);
|
|
161
163
|
break;
|
|
162
164
|
case "select":
|
|
163
165
|
result = await executeSelect(payload.plan, payload.params ?? {}, selectDb);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spooky-sync/core",
|
|
3
|
-
"version": "0.0.1-canary.
|
|
3
|
+
"version": "0.0.1-canary.137",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
|
-
"@spooky-sync/query-builder": "0.0.1-canary.
|
|
63
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
62
|
+
"@spooky-sync/query-builder": "0.0.1-canary.137",
|
|
63
|
+
"@spooky-sync/ssp-wasm": "0.0.1-canary.137",
|
|
64
64
|
"@sqlite.org/sqlite-wasm": "3.53.0-build1",
|
|
65
65
|
"@surrealdb/wasm": "^3.0.3",
|
|
66
66
|
"fast-json-patch": "^3.1.1",
|
|
@@ -96,6 +96,45 @@ describe('SqliteCacheEngine.switchBucket serialization', () => {
|
|
|
96
96
|
});
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
// Regression: DEFINE is a noop on this engine, so the `_00_*` internal tables
|
|
100
|
+
// the migrator DEFINEs are never physically created — a fresh bucket that READS
|
|
101
|
+
// one before any write (the sync layer selects `_00_query` at startup) threw
|
|
102
|
+
// "no such table: _00_query" and wedged the client on "Loading database". The
|
|
103
|
+
// fix seeds them inside `open`; assert the open message carries them.
|
|
104
|
+
describe('SqliteCacheEngine system-table seeding', () => {
|
|
105
|
+
it('passes the _00_* system tables to the worker open (fresh-bucket safe)', async () => {
|
|
106
|
+
const msgs: any[] = [];
|
|
107
|
+
const engine = new SqliteCacheEngine({ namespace: 'n', database: 'd' } as any, makeLogger());
|
|
108
|
+
(engine as any).spawnWorker = () => {
|
|
109
|
+
const w: any = { onmessage: null, onerror: null, onmessageerror: null, terminate() {} };
|
|
110
|
+
w.postMessage = (msg: any) => {
|
|
111
|
+
msgs.push(msg);
|
|
112
|
+
Promise.resolve().then(() => {
|
|
113
|
+
const rest = msg.type === 'open' ? { persisted: true } : {};
|
|
114
|
+
w.onmessage?.({ data: { id: msg.id, ok: true, ...rest } });
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
// Wire pending resolution like the real spawnWorker.
|
|
118
|
+
const inner = w.onmessage;
|
|
119
|
+
w.onmessage = (ev: any) => {
|
|
120
|
+
const { id, ok, error, ...rest } = ev.data ?? {};
|
|
121
|
+
const p = (engine as any).pending.get(id);
|
|
122
|
+
if (!p) return;
|
|
123
|
+
(engine as any).pending.delete(id);
|
|
124
|
+
if (ok) p.resolve(rest);
|
|
125
|
+
else p.reject(new Error(error));
|
|
126
|
+
void inner;
|
|
127
|
+
};
|
|
128
|
+
return w as unknown as Worker;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
await engine.connect('user:fresh');
|
|
132
|
+
const open = msgs.find((m) => m.type === 'open');
|
|
133
|
+
expect(open?.payload?.systemTables).toContain('_00_query');
|
|
134
|
+
expect(open?.payload?.systemTables).toContain('_00_pending_mutations');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
99
138
|
// `pureWriteOpResult` is the single source of truth for what a pure-write op
|
|
100
139
|
// contributes to a query's per-statement results. The batched fast path in
|
|
101
140
|
// `query()` and the per-op `execOp` path BOTH route through it, so a caller that
|
|
@@ -34,6 +34,21 @@ import type { EngineTx, Id, LocalStore, OrderBy, RelationFetch, Row } from './ca
|
|
|
34
34
|
* full merged row is only materialized for a LET-wrapped upsert (see the 'let'
|
|
35
35
|
* case). delete/deleteAll yield `[]`; noop yields `null`.
|
|
36
36
|
*/
|
|
37
|
+
/**
|
|
38
|
+
* The `_00_*` internal tables the client relies on. The LocalMigrator DEFINEs
|
|
39
|
+
* them, but every DEFINE lowers to a noop on the SQLite engine, so they must be
|
|
40
|
+
* created physically at open (see `openInternal`) or a read-before-first-write
|
|
41
|
+
* on a fresh bucket throws "no such table". Keep in sync with the systemSchema
|
|
42
|
+
* block in `local-migrator.ts`.
|
|
43
|
+
*/
|
|
44
|
+
const SYSTEM_TABLES = [
|
|
45
|
+
'_00_stream_processor_state',
|
|
46
|
+
'_00_query',
|
|
47
|
+
'_00_preload',
|
|
48
|
+
'_00_schema',
|
|
49
|
+
'_00_pending_mutations',
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
37
52
|
export function pureWriteOpResult(op: SqlOp): unknown {
|
|
38
53
|
switch (op.kind) {
|
|
39
54
|
case 'upsert':
|
|
@@ -217,11 +232,22 @@ export class SqliteCacheEngine implements LocalStore {
|
|
|
217
232
|
*/
|
|
218
233
|
private async openInternal(bucketId: string): Promise<void> {
|
|
219
234
|
this.worker = this.spawnWorker();
|
|
235
|
+
// Seed the `_00_*` system tables as part of `open` (worker-side, one round
|
|
236
|
+
// trip). The LocalMigrator DEFINEs them, but `translateSurql` lowers every
|
|
237
|
+
// DEFINE to a noop on this engine (SQLite has no DDL vocabulary), so
|
|
238
|
+
// provisioning never actually creates them — they were only made lazily on
|
|
239
|
+
// first WRITE. A fresh bucket (e.g. right after signup) that READS one first
|
|
240
|
+
// (the sync layer selects `_00_query` before any row lands) hit
|
|
241
|
+
// "no such table: _00_query" and the client wedged on "Loading database".
|
|
242
|
+
// Creating them inside `open` guarantees any access order is safe without
|
|
243
|
+
// adding ops to the engine's queue.
|
|
220
244
|
const { persisted } = await this.rawCall<{ persisted: boolean }>('open', {
|
|
221
245
|
dbName: bucketId,
|
|
222
246
|
useOpfs: this.useOpfs,
|
|
247
|
+
systemTables: SYSTEM_TABLES,
|
|
223
248
|
});
|
|
224
249
|
this.knownTables.clear();
|
|
250
|
+
for (const t of SYSTEM_TABLES) this.knownTables.add(t);
|
|
225
251
|
this.bucketId = bucketId;
|
|
226
252
|
this.logger.info(
|
|
227
253
|
{ bucketId, persisted, Category: 'sp00ky-client::SqliteCacheEngine::connect' },
|
|
@@ -34,7 +34,11 @@ let db: {
|
|
|
34
34
|
close: () => void;
|
|
35
35
|
} | null = null;
|
|
36
36
|
|
|
37
|
-
async function open(
|
|
37
|
+
async function open(
|
|
38
|
+
dbName: string,
|
|
39
|
+
useOpfs: boolean,
|
|
40
|
+
systemTables: readonly string[] = []
|
|
41
|
+
): Promise<{ persisted: boolean }> {
|
|
38
42
|
const sqlite3: any = await sqlite3InitModule();
|
|
39
43
|
let persisted = false;
|
|
40
44
|
if (useOpfs && sqlite3.installOpfsSAHPoolVfs) {
|
|
@@ -47,6 +51,12 @@ async function open(dbName: string, useOpfs: boolean): Promise<{ persisted: bool
|
|
|
47
51
|
}
|
|
48
52
|
}
|
|
49
53
|
if (!db) db = new sqlite3.oo1.DB(':memory:', 'c');
|
|
54
|
+
// Physically create the internal `_00_*` tables the client reads before any
|
|
55
|
+
// write (DEFINE is a noop on this engine, so the migrator can't). Prevents
|
|
56
|
+
// "no such table: _00_query" on a fresh bucket right after signup.
|
|
57
|
+
for (const t of systemTables) {
|
|
58
|
+
db!.exec({ sql: `CREATE TABLE IF NOT EXISTS "${t}" (id TEXT PRIMARY KEY, data TEXT NOT NULL)` });
|
|
59
|
+
}
|
|
50
60
|
// Retry (rather than instantly failing with SQLITE_BUSY=5) if a lock is held.
|
|
51
61
|
// Combined with the engine's single-flight op queue, overlap is avoided.
|
|
52
62
|
// `cache_size` is negated → KiB (here 32 MiB) so SQLite's page cache can't
|
|
@@ -106,7 +116,10 @@ self.onmessage = async (ev: MessageEvent) => {
|
|
|
106
116
|
switch (type) {
|
|
107
117
|
case 'open':
|
|
108
118
|
selectDb.knownTables.clear();
|
|
109
|
-
result = await open(payload.dbName, payload.useOpfs);
|
|
119
|
+
result = await open(payload.dbName, payload.useOpfs, payload.systemTables);
|
|
120
|
+
// The freshly seeded system tables exist — record them so the select
|
|
121
|
+
// path doesn't redundantly re-issue CREATE TABLE for each.
|
|
122
|
+
for (const t of (payload.systemTables ?? []) as string[]) selectDb.knownTables.add(t);
|
|
110
123
|
break;
|
|
111
124
|
case 'select':
|
|
112
125
|
result = await executeSelect(payload.plan, payload.params ?? {}, selectDb);
|