@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.
@@ -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),