@spooky-sync/core 0.0.1-canary.117 → 0.0.1-canary.118

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
@@ -1222,7 +1222,7 @@ function renderRelationFetchSurql(req) {
1222
1222
  vars: { __keys: req.keys },
1223
1223
  n: 0
1224
1224
  };
1225
- let sql = `SELECT ${req.select && req.select.length > 0 ? ["id", ...req.select].join(", ") : "*"} FROM ${req.table} WHERE ${req.matchField} IN $__keys`;
1225
+ let sql = `SELECT ${req.select && req.select.length > 0 ? ["id", ...req.select].join(", ") : "*"} FROM ${req.table} WHERE ${req.matchField} IN $__keys.map(|$__k| type::record(<string> $__k))`;
1226
1226
  if (req.where && req.where.length > 0) sql += ` AND ${renderWhereSurql(req.where, ctx)}`;
1227
1227
  if (req.orderBy && req.orderBy.length > 0) sql += renderOrderBy(req.orderBy);
1228
1228
  return {
@@ -1616,7 +1616,7 @@ var SqliteCacheEngine = class {
1616
1616
  return () => {};
1617
1617
  }
1618
1618
  spawnWorker() {
1619
- const worker = new Worker(new URL("./sqlite-worker.ts", import.meta.url), { type: "module" });
1619
+ const worker = new Worker(new URL("./sqlite-worker.js", import.meta.url), { type: "module" });
1620
1620
  worker.onmessage = (ev) => {
1621
1621
  const { id, ok, error, ...rest } = ev.data ?? {};
1622
1622
  const p = this.pending.get(id);
@@ -5100,8 +5100,8 @@ function parseBackendInfo(raw) {
5100
5100
 
5101
5101
  //#endregion
5102
5102
  //#region src/modules/devtools/index.ts
5103
- const CORE_VERSION = "0.0.1-canary.117";
5104
- const WASM_VERSION = "0.0.1-canary.117";
5103
+ const CORE_VERSION = "0.0.1-canary.118";
5104
+ const WASM_VERSION = "0.0.1-canary.118";
5105
5105
  const SURREAL_VERSION = "3.0.3";
5106
5106
  var DevToolsService = class {
5107
5107
  eventsHistory = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.117",
3
+ "version": "0.0.1-canary.118",
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.117",
63
- "@spooky-sync/ssp-wasm": "0.0.1-canary.117",
62
+ "@spooky-sync/query-builder": "0.0.1-canary.118",
63
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.118",
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",
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { RecordId } from 'surrealdb';
3
+ import { Sp00kySync } from './sync';
4
+ import { buildSubqueryListRefSelect } from './utils';
5
+ import { encodeRecordId } from '../../utils/index';
6
+
7
+ // Guards the CLIENT half of `.related()` (author/comments) delivery: given
8
+ // subquery-child edges on the server (`_00_list_ref…` rows with
9
+ // `parent IS NOT NONE`), `syncSubqueryChildren` MUST pull the child bodies
10
+ // through the sync engine so a later re-materialization attaches them to the
11
+ // parent. If this stops forwarding the child ids, related fields come back
12
+ // empty (threads render with an "Anonymous" author, comments vanish) even
13
+ // though the server wrote the edges correctly.
14
+
15
+ function makeSync() {
16
+ const logger: any = {
17
+ child: () => logger,
18
+ debug: () => {}, info: () => {}, warn: () => {}, error: () => {}, trace: () => {},
19
+ };
20
+ const remote: any = { query: vi.fn() };
21
+ const queryId = new RecordId('_00_query', 'h1');
22
+ const queryState: any = { config: { id: queryId, subqueryRemoteArray: [] } };
23
+ const dataModule: any = { getQueryByHash: vi.fn().mockReturnValue(queryState) };
24
+
25
+ const sync = new Sp00kySync(
26
+ {} as any, remote, {} as any, dataModule, {} as any, logger,
27
+ );
28
+
29
+ // syncEngine is constructed internally; replace it with a spy.
30
+ const syncRecords = vi.fn().mockResolvedValue(undefined);
31
+ (sync as any).syncEngine = { syncRecords };
32
+ // Pin the resolved per-user list_ref table (auth routing is out of scope here).
33
+ (sync as any).listRefTable = () => '_00_list_ref_user_x';
34
+
35
+ const run = (hash: string) => (sync as any).syncSubqueryChildren(hash) as Promise<void>;
36
+ return { remote, syncRecords, queryState, queryId, run };
37
+ }
38
+
39
+ describe('syncSubqueryChildren — related-child delivery', () => {
40
+ beforeEach(() => vi.clearAllMocks());
41
+
42
+ it('reads the subquery edges (parent IS NOT NONE) scoped to the query id', async () => {
43
+ const { remote, run, queryId } = makeSync();
44
+ remote.query.mockResolvedValue([[]]);
45
+
46
+ await run('h1');
47
+
48
+ expect(remote.query).toHaveBeenCalledWith(
49
+ buildSubqueryListRefSelect('_00_list_ref_user_x'),
50
+ { in: queryId }
51
+ );
52
+ });
53
+
54
+ it('forwards each subquery child id to the sync engine so its body is cached', async () => {
55
+ const { remote, syncRecords, run } = makeSync();
56
+ // Server has one author child edge for this query.
57
+ remote.query.mockResolvedValue([[
58
+ { out: new RecordId('user', 'u'), version: 1 },
59
+ ]]);
60
+
61
+ await run('h1');
62
+
63
+ expect(syncRecords).toHaveBeenCalledTimes(1);
64
+ const arg = syncRecords.mock.calls[0][0];
65
+ expect(arg.added).toHaveLength(1);
66
+ expect(encodeRecordId(arg.added[0].id)).toBe('user:u');
67
+ expect(arg.added[0].version).toBe(1);
68
+ expect(arg.removed).toEqual([]); // shared child bodies are never deleted here
69
+ });
70
+
71
+ it('is idempotent: unchanged edges do not refetch bodies', async () => {
72
+ const { remote, syncRecords, run, queryState } = makeSync();
73
+ queryState.config.subqueryRemoteArray = [['user:u', 1]];
74
+ remote.query.mockResolvedValue([[
75
+ { out: new RecordId('user', 'u'), version: 1 },
76
+ ]]);
77
+
78
+ await run('h1');
79
+
80
+ expect(syncRecords).not.toHaveBeenCalled();
81
+ });
82
+ });
@@ -51,7 +51,7 @@ describe('renderRelationFetchSurql', () => {
51
51
  where: [{ field: 'hidden', op: '=', value: false }],
52
52
  orderBy: [['rank', 'asc']],
53
53
  });
54
- expect(sql).toBe('SELECT * FROM comment WHERE post IN $__keys AND hidden = $__p0 ORDER BY rank asc;');
54
+ expect(sql).toBe('SELECT * FROM comment WHERE post IN $__keys.map(|$__k| type::record(<string> $__k)) AND hidden = $__p0 ORDER BY rank asc;');
55
55
  expect(vars).toEqual({ __keys: ['post:1', 'post:2'], __p0: false });
56
56
  });
57
57
  });
@@ -77,7 +77,12 @@ export function renderRelationFetchSurql(req: RelationFetch): RenderedQuery {
77
77
  const ctx: RenderCtx = { vars: { __keys: req.keys }, n: 0 };
78
78
  const projection =
79
79
  req.select && req.select.length > 0 ? ['id', ...req.select].join(', ') : '*';
80
- let sql = `SELECT ${projection} FROM ${req.table} WHERE ${req.matchField} IN $__keys`;
80
+ // The correlation keys arrive as record-id STRINGS (`"user:abc"`), but the
81
+ // matched column (`id`, or a `record<…>` foreign key) is a RecordId. In
82
+ // SurrealDB `id IN ["user:abc"]` never matches (string ≠ record), so every
83
+ // `.related()` field would resolve empty. Coerce each key to a record id with
84
+ // `type::record(<string> …)` (idempotent if a key is already a RecordId).
85
+ let sql = `SELECT ${projection} FROM ${req.table} WHERE ${req.matchField} IN $__keys.map(|$__k| type::record(<string> $__k))`;
81
86
  if (req.where && req.where.length > 0) {
82
87
  sql += ` AND ${renderWhereSurql(req.where, ctx)}`;
83
88
  }
@@ -106,8 +106,11 @@ export class SqliteCacheEngine implements LocalStore {
106
106
  // ---- worker plumbing -----------------------------------------------------
107
107
 
108
108
  private spawnWorker(): Worker {
109
- // The worker is a separate module entry; the bundler rewrites this URL.
110
- const worker = new Worker(new URL('./sqlite-worker.ts', import.meta.url), { type: 'module' });
109
+ // The worker is a separate tsdown entry emitted at `dist/sqlite-worker.js`
110
+ // (see tsdown.config.ts). This URL must point at the emitted `.js` so it
111
+ // resolves after bundling into the flat `dist/index.js`; a `.ts` here ships
112
+ // a dangling reference the consumer's bundler can't resolve.
113
+ const worker = new Worker(new URL('./sqlite-worker.js', import.meta.url), { type: 'module' });
111
114
  worker.onmessage = (ev: MessageEvent) => {
112
115
  const { id, ok, error, ...rest } = ev.data ?? {};
113
116
  const p = this.pending.get(id);