@spooky-sync/core 0.0.1-canary.314 → 0.0.1-canary.315

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
@@ -1,4 +1,4 @@
1
- import { a as renderOrderSql, c as serializeMergeRow, d as stableKey, i as projectedDataSql, l as serializeRow, o as renderWhereSql, r as project, s as reviveRow, t as PROMOTION_OPEN_OPTIONS, u as resolveRelations } from "./sqlite-open.js";
1
+ import { a as renderOrderSql, c as serializeMergeRow, d as resolveRelations, f as stableKey, i as projectedDataSql, l as serializeRow, o as renderWhereSql, r as project, s as reviveRow, t as PROMOTION_OPEN_OPTIONS, u as recordIdKey } from "./sqlite-open.js";
2
2
  import { QueryBuilder, RecordId, baseFieldOfParam, cyrb53 } from "@spooky-sync/query-builder";
3
3
  import { DateTime, Duration, RecordId as RecordId$1, Surreal, Uuid, applyDiagnostics, createRemoteEngines } from "surrealdb";
4
4
  import pino from "pino";
@@ -2178,8 +2178,8 @@ function trimHistory(history, cap) {
2178
2178
 
2179
2179
  //#endregion
2180
2180
  //#region src/modules/devtools/index.ts
2181
- const CORE_VERSION = "0.0.1-canary.314";
2182
- const WASM_VERSION = "0.0.1-canary.314";
2181
+ const CORE_VERSION = "0.0.1-canary.315";
2182
+ const WASM_VERSION = "0.0.1-canary.315";
2183
2183
  const SURREAL_VERSION = "3.0.3";
2184
2184
  var DevToolsService = class DevToolsService {
2185
2185
  version = CORE_VERSION;
@@ -11212,7 +11212,7 @@ var StreamProcessorService = class {
11212
11212
  const hasToString = typeof value.toString === "function";
11213
11213
  const isNotPlainObject = value.constructor !== Object;
11214
11214
  if (hasTable && hasId && hasToString && isNotPlainObject) {
11215
- const result = value.toString();
11215
+ const result = recordIdKey(value) ?? value.toString();
11216
11216
  this.logger.trace({
11217
11217
  result,
11218
11218
  Category: "sp00ky-client::StreamProcessorService::normalizeValue"
@@ -14603,7 +14603,7 @@ function createTabsCoordinator(config, s, host) {
14603
14603
  return new TabsCoordinator({
14604
14604
  tabId: s.tabId,
14605
14605
  fingerprint: computeTabsFingerprint({
14606
- coreVersion: "0.0.1-canary.314",
14606
+ coreVersion: "0.0.1-canary.315",
14607
14607
  schemaHash: hash53(config.schemaSurql),
14608
14608
  endpoint: config.database.endpoint ?? "",
14609
14609
  namespace: config.database.namespace,
@@ -88,13 +88,32 @@ async function resolveOne(parents, relation, fetcher, depth, path) {
88
88
  function looksLikeRecordId(value) {
89
89
  if (value == null) return false;
90
90
  if (typeof value === "string") return /^[A-Za-z_][A-Za-z0-9_]*:.+/.test(value);
91
- if (typeof value === "object") {
92
- const rid = value;
93
- return rid.tb !== void 0 && rid.id !== void 0;
94
- }
91
+ if (typeof value === "object") return recordIdKey(value) !== null;
95
92
  return false;
96
93
  }
97
94
  /**
95
+ * The raw `table:id` key of a RecordId, duck-typed: the SDK 1.x shape
96
+ * `{ tb, id }` or the SDK 2.x one, whose `table` is a `Table` (both behind
97
+ * getters). `null` for anything else.
98
+ *
99
+ * Never the SDK's `toString()`: that is SurrealQL, escaped. SDK 2.1.0 escapes
100
+ * every key that starts with a digit (`game:⟨476g…⟩`), while the store and
101
+ * server membership spell the same id `game:476g…`, so a key built from
102
+ * `toString()` silently missed about a quarter of random ids. A complex
103
+ * (array/object) key has no raw spelling and returns `null`.
104
+ */
105
+ function recordIdKey(value) {
106
+ const rid = value;
107
+ if (rid.id === void 0) return null;
108
+ if (rid.tb !== void 0) return `${String(rid.tb)}:${stableKey(rid.id)}`;
109
+ const proto = Object.getPrototypeOf(value);
110
+ if (proto === Object.prototype || proto === null) return null;
111
+ const name = rid.table?.name;
112
+ if (typeof rid.table !== "object" || typeof name !== "string") return null;
113
+ if (typeof rid.id === "string" || typeof rid.id === "number" || typeof rid.id === "bigint") return `${name}:${String(rid.id)}`;
114
+ return null;
115
+ }
116
+ /**
98
117
  * Stable grouping key for a correlation value. A `RecordId` (duck-typed to
99
118
  * avoid a hard surrealdb dependency here) and its `table:id` string form must
100
119
  * collapse to the same key so a parent FK stored either way matches the child.
@@ -104,8 +123,9 @@ function stableKey(value) {
104
123
  if (typeof value === "string") return value;
105
124
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
106
125
  if (typeof value === "object") {
126
+ const key = recordIdKey(value);
127
+ if (key !== null) return key;
107
128
  const rid = value;
108
- if (rid.tb !== void 0 && rid.id !== void 0) return `${String(rid.tb)}:${stableKey(rid.id)}`;
109
129
  if (typeof rid.toString === "function") {
110
130
  const s = rid.toString();
111
131
  if (s !== "[object Object]") return s;
@@ -173,11 +193,12 @@ function scalar(value) {
173
193
  return value;
174
194
  }
175
195
  function serializeRow(row) {
176
- return JSON.stringify(row, (_k, v) => {
196
+ return JSON.stringify(row, function(k, v) {
177
197
  if (v instanceof Uint8Array) return { __u8: toBase64(v) };
178
- if (v && typeof v === "object") {
179
- const rid = v;
180
- if (rid.tb !== void 0 && rid.id !== void 0) return stableKey(v);
198
+ const raw = this[k];
199
+ if (raw && typeof raw === "object") {
200
+ const key = recordIdKey(raw);
201
+ if (key !== null) return key;
181
202
  }
182
203
  return v;
183
204
  });
@@ -355,4 +376,4 @@ async function openDb(sqlite3, dbName, useOpfs, opts = {}) {
355
376
  }
356
377
 
357
378
  //#endregion
358
- export { renderOrderSql as a, serializeMergeRow as c, stableKey as d, projectedDataSql as i, serializeRow as l, openDb as n, renderWhereSql as o, project as r, reviveRow as s, PROMOTION_OPEN_OPTIONS as t, resolveRelations as u };
379
+ export { renderOrderSql as a, serializeMergeRow as c, resolveRelations as d, stableKey as f, projectedDataSql as i, serializeRow as l, openDb as n, renderWhereSql as o, project as r, reviveRow as s, PROMOTION_OPEN_OPTIONS as t, recordIdKey as u };
@@ -1,4 +1,4 @@
1
- import { a as renderOrderSql, d as stableKey, i as projectedDataSql, n as openDb, o as renderWhereSql, r as project, s as reviveRow, u as resolveRelations } from "./sqlite-open.js";
1
+ import { a as renderOrderSql, d as resolveRelations, f as stableKey, i as projectedDataSql, n as openDb, o as renderWhereSql, r as project, s as reviveRow } from "./sqlite-open.js";
2
2
  import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
3
3
 
4
4
  //#region src/services/database/sqlite-lock-verify.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.314",
3
+ "version": "0.0.1-canary.315",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -75,8 +75,8 @@
75
75
  }
76
76
  },
77
77
  "dependencies": {
78
- "@spooky-sync/query-builder": "0.0.1-canary.314",
79
- "@spooky-sync/ssp-wasm": "0.0.1-canary.314",
78
+ "@spooky-sync/query-builder": "0.0.1-canary.315",
79
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.315",
80
80
  "@sqlite.org/sqlite-wasm": "3.53.0-build1",
81
81
  "@surrealdb/wasm": "^3.0.3",
82
82
  "blurhash": "^2.0.5",
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
+ import { RecordId } from 'surrealdb';
2
3
  import type { RelationPlan, WhereNode } from '@spooky-sync/query-builder';
3
4
  import { looksLikeRecordId, resolveRelations, sortRows, stableKey } from './relation-resolver';
4
5
  import {
@@ -306,6 +307,22 @@ describe('stableKey', () => {
306
307
  expect(stableKey({ tb: 'user', id: '1' })).toBe('user:1');
307
308
  expect(stableKey('user:1')).toBe('user:1');
308
309
  });
310
+
311
+ it('spells an SDK RecordId raw, never as escaped SurrealQL', () => {
312
+ // SDK 2.1.0 `toString()`s these as `game:⟨476g…⟩`, `t:⟨123⟩`, `t:⟨a-b⟩`.
313
+ expect(stableKey(new RecordId('game', '476gsdlvro7cme8nwied'))).toBe('game:476gsdlvro7cme8nwied');
314
+ expect(stableKey(new RecordId('t', '123'))).toBe('t:123');
315
+ expect(stableKey(new RecordId('t', 'a-b'))).toBe('t:a-b');
316
+ expect(stableKey(new RecordId('t', 7))).toBe('t:7');
317
+ expect(looksLikeRecordId(new RecordId('game', '476gsdlvro7cme8nwied'))).toBe(true);
318
+ });
319
+
320
+ it('leaves a complex key to toString and a plain row with table/id columns alone', () => {
321
+ const complex = new RecordId('t', [1, 'x']);
322
+ expect(stableKey(complex)).toBe(complex.toString());
323
+ const row = { table: { name: 'x' }, id: 'y' };
324
+ expect(stableKey(row)).toBe(JSON.stringify(row));
325
+ });
309
326
  });
310
327
 
311
328
  /**
@@ -1,4 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
+ import { RecordId } from 'surrealdb';
2
3
  import type { WhereNode } from '@spooky-sync/query-builder';
3
4
  import {
4
5
  comparisonSql,
@@ -96,6 +97,11 @@ describe('serializeRow / reviveRow round-trip', () => {
96
97
  const json = serializeRow({ id: 't:1', author: { tb: 'user', id: 'u1' }, n: 2 });
97
98
  expect(reviveRow(json)).toEqual({ id: 't:1', author: 'user:u1', n: 2 });
98
99
  });
100
+
101
+ it('stores an SDK RecordId link raw, not as its escaped toJSON', () => {
102
+ const json = serializeRow({ id: 'game:1', white: new RecordId('player_name', '3abc'), tags: [new RecordId('t', '9z')] });
103
+ expect(reviveRow(json)).toEqual({ id: 'game:1', white: 'player_name:3abc', tags: ['t:9z'] });
104
+ });
99
105
  });
100
106
 
101
107
  describe('project', () => {
@@ -1,5 +1,5 @@
1
1
  import type { WhereComparison, WhereNode } from '@spooky-sync/query-builder';
2
- import { stableKey } from './relation-resolver';
2
+ import { recordIdKey, stableKey } from './relation-resolver';
3
3
  import type { OrderBy, Row } from './cache-engine';
4
4
 
5
5
  /**
@@ -60,11 +60,14 @@ export function scalar(value: unknown): unknown {
60
60
  }
61
61
 
62
62
  export function serializeRow(row: Row): string {
63
- return JSON.stringify(row, (_k, v) => {
63
+ return JSON.stringify(row, function (this: Record<string, unknown>, k, v) {
64
64
  if (v instanceof Uint8Array) return { __u8: toBase64(v) };
65
- if (v && typeof v === 'object') {
66
- const rid = v as { tb?: unknown; id?: unknown };
67
- if (rid.tb !== undefined && rid.id !== undefined) return stableKey(v);
65
+ // `v` has already been through `toJSON()`, and a RecordId's is escaped
66
+ // SurrealQL (`game:⟨476g…⟩`), so key the original value instead.
67
+ const raw = this[k];
68
+ if (raw && typeof raw === 'object') {
69
+ const key = recordIdKey(raw);
70
+ if (key !== null) return key;
68
71
  }
69
72
  return v;
70
73
  });
@@ -45,6 +45,10 @@ const FIXTURE: Record<string, Row[]> = {
45
45
  { id: 'comment:2', thread: 'thread:B', body: 'second' },
46
46
  { id: 'comment:3', thread: 'thread:A', body: 'other thread' },
47
47
  ],
48
+ game: [
49
+ { id: 'game:476gsdlvro7cme8nwied', white: 'player_name:a' },
50
+ { id: 'game:stgyqp7fm3sc4tmyc8fm', white: 'player_name:b' },
51
+ ],
48
52
  };
49
53
 
50
54
  /** The `.one()` detail-view plan shape: id baked as a literal AND slaved to
@@ -187,12 +191,23 @@ describe('SqliteCacheEngine.select via worker (one hop)', () => {
187
191
  const rid = new RecordId('comment', '2');
188
192
  const { engine, calls } = makeEngine({ workerSelect: true, answerSelect: true });
189
193
  await engine.connect('anon');
190
- await engine.select({ table: 'comment', ids: [rid, 'comment:1'] }, {});
194
+ const rows = await engine.select({ table: 'comment', ids: [rid, 'comment:1'] }, {});
191
195
  const sel = calls.find((c) => c.type === 'select')!;
192
- // stableKey output (surrealdb may escape the id part, e.g. comment:⟨2⟩) —
193
- // identical to what the legacy `selectByIds` binds, so stored rows match.
194
- expect(sel.payload.plan.ids).toEqual([stableKey(rid), 'comment:1']);
195
- expect(sel.payload.plan.ids.every((i: unknown) => typeof i === 'string')).toBe(true);
196
+ // The raw spelling the rows are stored under, never the SDK's escaped
197
+ // `comment:⟨2⟩`: that one matched no stored row.
198
+ expect(sel.payload.plan.ids).toEqual(['comment:2', 'comment:1']);
199
+ expect(rows.map((r) => r.id)).toEqual(['comment:2', 'comment:1']);
200
+ });
201
+
202
+ it('finds a window row whose id starts with a digit (SDK 2.1.0 escapes it)', async () => {
203
+ // `materializeEffect` parses every window id into a RecordId, and SDK
204
+ // 2.1.0's `toString()` spells this one `game:⟨476g…⟩`. Keyed that way the
205
+ // row went missing, the window came back short and read as the list end.
206
+ const { engine } = makeEngine({ workerSelect: true, answerSelect: true });
207
+ await engine.connect('anon');
208
+ const ids = [new RecordId('game', '476gsdlvro7cme8nwied'), new RecordId('game', 'stgyqp7fm3sc4tmyc8fm')];
209
+ const rows = await engine.select({ table: 'game', ids }, {});
210
+ expect(rows.map((r) => r.id)).toEqual(['game:476gsdlvro7cme8nwied', 'game:stgyqp7fm3sc4tmyc8fm']);
196
211
  });
197
212
 
198
213
  it('normalizes RecordId values baked inside the plan\'s where tree', async () => {
@@ -7,6 +7,7 @@ import type { WasmProcessor, WasmStreamUpdate } from './wasm-types';
7
7
  import type { Duration } from 'surrealdb';
8
8
  import type { QueryTimeToLive, RecordVersionArray } from '../../types';
9
9
  import { encodeRecordId } from '../../utils/index';
10
+ import { recordIdKey } from '../database/relation-resolver';
10
11
 
11
12
  // Simple interface for query plan registration (replaces Incantation class)
12
13
  interface QueryPlanConfig {
@@ -1044,7 +1045,9 @@ export class StreamProcessorService {
1044
1045
  const isNotPlainObject = value.constructor !== Object;
1045
1046
 
1046
1047
  if (hasTable && hasId && hasToString && isNotPlainObject) {
1047
- const result = value.toString();
1048
+ // The raw `table:id`, as the store spells it. `toString()` is escaped
1049
+ // SurrealQL (SDK 2.1.0: `game:⟨476g…⟩`); only a complex key uses it.
1050
+ const result = recordIdKey(value) ?? value.toString();
1048
1051
  this.logger.trace(
1049
1052
  { result, Category: 'sp00ky-client::StreamProcessorService::normalizeValue' },
1050
1053
  'RecordId detected'