@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.
|
|
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.
|
|
5104
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
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.
|
|
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.
|
|
63
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
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);
|