@spooky-sync/core 0.0.1-canary.204 → 0.0.1-canary.205
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.d.ts +148 -40
- package/dist/index.js +601 -173
- package/dist/tabs-broker-worker.js +51 -13
- package/dist/types.d.ts +52 -13
- package/package.json +3 -3
- package/src/modules/cache/index.ts +21 -4
- package/src/modules/devtools/index.ts +3 -1
- package/src/modules/sync/engine.ts +76 -58
- package/src/modules/sync/sync.ts +65 -8
- package/src/services/database/cache-engine.ts +33 -0
- package/src/services/database/sqlite-cache-engine.test.ts +58 -0
- package/src/services/database/sqlite-cache-engine.ts +71 -1
- package/src/services/database/surreal-cache-engine.ts +20 -0
- package/src/services/stream-processor/index.ts +471 -225
- package/src/services/stream-processor/stream-processor.batch.test.ts +3 -13
- package/src/services/stream-processor/stream-processor.prime.test.ts +198 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +77 -67
- package/src/services/stream-processor/wasm-types.ts +32 -1
- package/src/services/tabs/broker.test.ts +49 -0
- package/src/services/tabs/coordinator.test.ts +6 -1
- package/src/services/tabs/coordinator.ts +35 -3
- package/src/services/tabs/tabs-broker-worker.ts +66 -12
- package/src/sp00ky.ts +54 -8
- package/src/types.ts +21 -13
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from './sqlite-plan-sql';
|
|
11
11
|
import type { Logger } from '../logger/index';
|
|
12
12
|
import type { Sp00kyConfig, StorageHealth } from '../../types';
|
|
13
|
+
import type { SnapshotMeta, StoredSnapshot } from './cache-engine';
|
|
13
14
|
import type { SealedQuery } from '../../utils/surql';
|
|
14
15
|
import { resolveRelations, stableKey } from './relation-resolver';
|
|
15
16
|
import {
|
|
@@ -50,8 +51,13 @@ import type { EngineStorageDiagnostics } from '../../modules/devtools/storage-in
|
|
|
50
51
|
* on a fresh bucket throws "no such table". Keep in sync with the systemSchema
|
|
51
52
|
* block in `local-migrator.ts`.
|
|
52
53
|
*/
|
|
54
|
+
const SNAPSHOT_TABLE = '_00_circuit_snapshot';
|
|
55
|
+
|
|
53
56
|
const SYSTEM_TABLES = [
|
|
54
57
|
'_00_stream_processor_state',
|
|
58
|
+
// In-browser circuit snapshot: one BLOB row (`circuit`) + a JSON meta row.
|
|
59
|
+
// Excluded from DevTools' table listing, since its data column is not JSON.
|
|
60
|
+
'_00_circuit_snapshot',
|
|
55
61
|
'_00_query',
|
|
56
62
|
'_00_preload',
|
|
57
63
|
'_00_window',
|
|
@@ -197,7 +203,7 @@ export class SqliteCacheEngine implements LocalStore {
|
|
|
197
203
|
const { rows: tables } = await this.call<{ rows: { name: string }[] }>('exec', {
|
|
198
204
|
sql: "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
|
|
199
205
|
});
|
|
200
|
-
const names = (tables ?? []).map((r) => r.name);
|
|
206
|
+
const names = (tables ?? []).map((r) => r.name).filter((n) => n !== SNAPSHOT_TABLE);
|
|
201
207
|
if (names.length) {
|
|
202
208
|
// Names come from sqlite_master itself; double-quoting is enough.
|
|
203
209
|
const sql = names
|
|
@@ -837,6 +843,70 @@ export class SqliteCacheEngine implements LocalStore {
|
|
|
837
843
|
return rows[0] ?? null;
|
|
838
844
|
}
|
|
839
845
|
|
|
846
|
+
/**
|
|
847
|
+
* `(id, _00_rv)` of every row per table, straight off the worker: no body
|
|
848
|
+
* parse, one round trip per table. A table that does not exist yet reads as
|
|
849
|
+
* empty rather than failing the whole scan.
|
|
850
|
+
*/
|
|
851
|
+
async scanVersions(tables: string[]): Promise<Record<string, [string, number][]>> {
|
|
852
|
+
const out: Record<string, [string, number][]> = {};
|
|
853
|
+
for (const table of tables) {
|
|
854
|
+
try {
|
|
855
|
+
const { rows } = await this.call<{ rows: { id: string; rv: unknown }[] }>('exec', {
|
|
856
|
+
sql: `SELECT id, json_extract(data, '$._00_rv') AS rv FROM "${table.replace(/"/g, '""')}"`,
|
|
857
|
+
});
|
|
858
|
+
out[table] = (rows ?? []).map((r) => [r.id, Number(r.rv) || 0]);
|
|
859
|
+
} catch {
|
|
860
|
+
out[table] = [];
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return out;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async getSnapshot(key: string): Promise<StoredSnapshot | null> {
|
|
867
|
+
const { rows } = await this.call<{ rows: { id: string; data: unknown }[] }>('exec', {
|
|
868
|
+
sql: `SELECT id, data FROM "${SNAPSHOT_TABLE}" WHERE id IN (?, ?)`,
|
|
869
|
+
bind: [key, `${key}:meta`],
|
|
870
|
+
});
|
|
871
|
+
let bytes: Uint8Array | null = null;
|
|
872
|
+
let meta: SnapshotMeta | null = null;
|
|
873
|
+
for (const r of rows ?? []) {
|
|
874
|
+
if (r.id === key) {
|
|
875
|
+
if (r.data instanceof Uint8Array) bytes = r.data;
|
|
876
|
+
else if (r.data instanceof ArrayBuffer) bytes = new Uint8Array(r.data);
|
|
877
|
+
else if (typeof r.data === 'string') bytes = new TextEncoder().encode(r.data);
|
|
878
|
+
} else if (typeof r.data === 'string') {
|
|
879
|
+
try {
|
|
880
|
+
meta = JSON.parse(r.data) as SnapshotMeta;
|
|
881
|
+
} catch {
|
|
882
|
+
meta = null;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return bytes && meta ? { bytes, meta } : null;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async putSnapshot(key: string, bytes: Uint8Array, meta: SnapshotMeta): Promise<void> {
|
|
890
|
+
// One transaction, so a reader never sees new bytes with old meta.
|
|
891
|
+
await this.call('batch', [
|
|
892
|
+
{
|
|
893
|
+
sql: `INSERT OR REPLACE INTO "${SNAPSHOT_TABLE}" (id, data) VALUES (?, ?)`,
|
|
894
|
+
bind: [key, bytes],
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
sql: `INSERT OR REPLACE INTO "${SNAPSHOT_TABLE}" (id, data) VALUES (?, ?)`,
|
|
898
|
+
bind: [`${key}:meta`, JSON.stringify(meta)],
|
|
899
|
+
},
|
|
900
|
+
]);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
async deleteSnapshot(key: string): Promise<void> {
|
|
904
|
+
await this.call('run', {
|
|
905
|
+
sql: `DELETE FROM "${SNAPSHOT_TABLE}" WHERE id IN (?, ?)`,
|
|
906
|
+
bind: [key, `${key}:meta`],
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
840
910
|
// ---- writes --------------------------------------------------------------
|
|
841
911
|
|
|
842
912
|
async upsert(table: string, id: Id, data: Row, mode: 'replace' | 'merge'): Promise<void> {
|
|
@@ -99,6 +99,26 @@ export class SurrealCacheEngine extends LocalDatabaseService implements LocalCac
|
|
|
99
99
|
return new RecordId(table, raw);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/**
|
|
103
|
+
* `(id, _00_rv)` of every row per table. Runs on the main thread against the
|
|
104
|
+
* embedded engine (no worker here), which is why the circuit prime caps how
|
|
105
|
+
* much it is willing to read through this path.
|
|
106
|
+
*/
|
|
107
|
+
async scanVersions(tables: string[]): Promise<Record<string, [string, number][]>> {
|
|
108
|
+
const out: Record<string, [string, number][]> = {};
|
|
109
|
+
for (const table of tables) {
|
|
110
|
+
try {
|
|
111
|
+
const [rows] = await this.query<[{ id: Id; _00_rv?: unknown }[]]>(
|
|
112
|
+
`SELECT id, _00_rv FROM ${table};`
|
|
113
|
+
);
|
|
114
|
+
out[table] = (rows ?? []).map((r) => [stableKey(r.id), Number(r._00_rv) || 0]);
|
|
115
|
+
} catch {
|
|
116
|
+
out[table] = [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
102
122
|
async getById(table: string, id: Id): Promise<Row | null> {
|
|
103
123
|
const [row] = await this.query<[Row | null]>('SELECT * FROM ONLY $__id;', {
|
|
104
124
|
__id: this.toRecordId(table, id),
|