@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 +5 -5
- package/dist/sqlite-open.js +31 -10
- package/dist/sqlite-worker.js +1 -1
- package/package.json +3 -3
- package/src/services/database/relation-resolver.test.ts +17 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/sqlite-plan-sql.test.ts +6 -0
- package/src/services/database/sqlite-plan-sql.ts +8 -5
- package/src/services/database/sqlite-select.test.ts +20 -5
- package/src/services/stream-processor/index.ts +4 -1
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
|
|
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.
|
|
2182
|
-
const WASM_VERSION = "0.0.1-canary.
|
|
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.
|
|
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,
|
package/dist/sqlite-open.js
CHANGED
|
@@ -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, (
|
|
196
|
+
return JSON.stringify(row, function(k, v) {
|
|
177
197
|
if (v instanceof Uint8Array) return { __u8: toBase64(v) };
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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,
|
|
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 };
|
package/dist/sqlite-worker.js
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
79
|
-
"@spooky-sync/ssp-wasm": "0.0.1-canary.
|
|
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
|
/**
|
|
Binary file
|
|
@@ -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, (
|
|
63
|
+
return JSON.stringify(row, function (this: Record<string, unknown>, k, v) {
|
|
64
64
|
if (v instanceof Uint8Array) return { __u8: toBase64(v) };
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
193
|
-
//
|
|
194
|
-
expect(sel.payload.plan.ids).toEqual([
|
|
195
|
-
expect(
|
|
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
|
-
|
|
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'
|