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

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 {
@@ -1563,7 +1563,7 @@ function pureWriteOpResult(op) {
1563
1563
  }
1564
1564
  /**
1565
1565
  * Local cache backend on official SQLite-WASM in a dedicated Worker (see
1566
- * `sqlite-worker.ts`), with OPFS SAHPool persistence. Storage model: one table
1566
+ * `sqlite-worker.js`), with OPFS SAHPool persistence. Storage model: one table
1567
1567
  * per schema table, `id TEXT PRIMARY KEY, data TEXT` where `data` is the row as
1568
1568
  * JSON. Filtering/ordering use `json_extract`. Relations are decomposed by the
1569
1569
  * shared {@link resolveRelations} — identical to the SurrealDB backend.
@@ -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.119";
5104
+ const WASM_VERSION = "0.0.1-canary.119";
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.119",
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.119",
63
+ "@spooky-sync/ssp-wasm": "0.0.1-canary.119",
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,7 +106,13 @@ 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.
109
+ // Source references the `.ts` so the monorepo's src-bundling consumers
110
+ // (e.g. the example app, which aliases `@spooky-sync/core` to `src`) resolve
111
+ // it — Vite handles `.ts` workers. For the published package, the tsdown
112
+ // build rewrites this to `./sqlite-worker.js` (the top-level emitted entry;
113
+ // see tsdown.config.ts), which the flat `dist/index.js` resolves. The worker
114
+ // (+ `@sqlite.org/sqlite-wasm`) still loads lazily — only when `localEngine:
115
+ // 'sqlite'` is used.
110
116
  const worker = new Worker(new URL('./sqlite-worker.ts', import.meta.url), { type: 'module' });
111
117
  worker.onmessage = (ev: MessageEvent) => {
112
118
  const { id, ok, error, ...rest } = ev.data ?? {};
package/tsdown.config.ts CHANGED
@@ -53,6 +53,19 @@ const versionDefinePlugin = {
53
53
  },
54
54
  };
55
55
 
56
+ // The SQLite worker URL is written as `./sqlite-worker.ts` in source so Vite's
57
+ // src-bundling consumers (the example app aliases core to `src`) resolve it.
58
+ // In the published build the worker is emitted at `dist/sqlite-worker.js`, so
59
+ // rewrite the reference to `.js` here — otherwise the flat `dist/index.js`
60
+ // carries a dangling `.ts` URL that a consumer's bundler can't resolve.
61
+ const sqliteWorkerUrlPlugin = {
62
+ name: 'sp00ky-sqlite-worker-url',
63
+ transform(code: string) {
64
+ if (!code.includes('sqlite-worker.ts')) return null;
65
+ return code.split('sqlite-worker.ts').join('sqlite-worker.js');
66
+ },
67
+ };
68
+
56
69
  export default defineConfig({
57
70
  // `sqlite-worker` is emitted at `dist/sqlite-worker.js` (top level, NOT under
58
71
  // services/database) so the `new URL('./sqlite-worker.js', import.meta.url)`
@@ -68,5 +81,5 @@ export default defineConfig({
68
81
  dts: true,
69
82
  clean: true,
70
83
  hash: false,
71
- plugins: [versionDefinePlugin],
84
+ plugins: [versionDefinePlugin, sqliteWorkerUrlPlugin],
72
85
  });