@spooky-sync/core 0.0.1-canary.17 → 0.0.1-canary.170
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/AGENTS.md +57 -0
- package/dist/index.d.ts +2046 -365
- package/dist/index.js +11016 -2476
- package/dist/otel/index.d.ts +21 -0
- package/dist/otel/index.js +86 -0
- package/dist/sqlite-open.js +276 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +420 -0
- package/dist/tabs-broker-worker.d.ts +8 -0
- package/dist/tabs-broker-worker.js +434 -0
- package/dist/types.d.ts +997 -0
- package/package.json +14 -7
- package/scripts/check-broker-bundle.mjs +33 -0
- package/skills/{spooky-core → sp00ky-core}/SKILL.md +94 -18
- package/skills/{spooky-core → sp00ky-core}/references/auth.md +1 -1
- package/skills/{spooky-core → sp00ky-core}/references/config.md +4 -4
- package/src/build-globals.d.ts +12 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +24 -2
- package/src/modules/app-release/index.test.ts +125 -0
- package/src/modules/app-release/index.ts +201 -0
- package/src/modules/auth/events/index.ts +2 -1
- package/src/modules/auth/index.ts +59 -20
- package/src/modules/cache/index.ts +112 -32
- package/src/modules/cache/types.ts +2 -2
- package/src/modules/crdt/crdt-field.ts +294 -0
- package/src/modules/crdt/crdt-hydration.test.ts +210 -0
- package/src/modules/crdt/crdt-reconnect.test.ts +195 -0
- package/src/modules/crdt/index.ts +463 -0
- package/src/modules/crdt/loro-loader.ts +25 -0
- package/src/modules/data/data.hydration.test.ts +142 -0
- package/src/modules/data/data.membership.test.ts +297 -0
- package/src/modules/data/data.rebind.test.ts +147 -0
- package/src/modules/data/data.run.test.ts +113 -0
- package/src/modules/data/data.status.test.ts +249 -0
- package/src/modules/data/id-set-plan.test.ts +122 -0
- package/src/modules/data/index.ts +1403 -129
- package/src/modules/data/mutation-id.test.ts +25 -0
- package/src/modules/data/mutation-id.ts +35 -0
- package/src/modules/data/window-query.test.ts +52 -0
- package/src/modules/data/window-query.ts +194 -0
- package/src/modules/devtools/flags.ts +349 -0
- package/src/modules/devtools/index.ts +382 -37
- package/src/modules/devtools/notify-throttle.test.ts +149 -0
- package/src/modules/devtools/storage-info.test.ts +79 -0
- package/src/modules/devtools/storage-info.ts +168 -0
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +81 -0
- package/src/modules/feature-flag/index.test.ts +251 -0
- package/src/modules/feature-flag/index.ts +308 -0
- package/src/modules/ref-tables.test.ts +91 -0
- package/src/modules/ref-tables.ts +88 -0
- package/src/modules/sync/engine.ts +101 -37
- package/src/modules/sync/events/index.ts +9 -2
- package/src/modules/sync/queue/queue-down.ts +12 -5
- package/src/modules/sync/queue/queue-up.forwarded.test.ts +164 -0
- package/src/modules/sync/queue/queue-up.ts +216 -56
- package/src/modules/sync/scheduler.pause.test.ts +109 -0
- package/src/modules/sync/scheduler.ts +77 -9
- package/src/modules/sync/sync.health.test.ts +149 -0
- package/src/modules/sync/sync.live-removal.test.ts +134 -0
- package/src/modules/sync/sync.reconnect.test.ts +145 -0
- package/src/modules/sync/sync.subquery.test.ts +82 -0
- package/src/modules/sync/sync.ts +1456 -92
- package/src/modules/sync/utils.test.ts +269 -2
- package/src/modules/sync/utils.ts +182 -17
- package/src/otel/index.ts +127 -0
- package/src/services/blobs/blob-cache.test.ts +359 -0
- package/src/services/blobs/blob-cache.ts +603 -0
- package/src/services/blobs/blob-manifest.ts +227 -0
- package/src/services/blobs/blob-store.test.ts +77 -0
- package/src/services/blobs/blob-store.ts +359 -0
- package/src/services/blobs/blob.fixture.ts +90 -0
- package/src/services/blobs/index.ts +70 -0
- package/src/services/database/cache-engine.ts +160 -0
- package/src/services/database/connection-supervisor.test.ts +289 -0
- package/src/services/database/connection-supervisor.ts +415 -0
- package/src/services/database/database.query-timeout.test.ts +83 -0
- package/src/services/database/database.ts +32 -12
- package/src/services/database/engine-factory.ts +33 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +7 -0
- package/src/services/database/local-migrator.ts +30 -27
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +478 -67
- package/src/services/database/plan-render.test.ts +159 -0
- package/src/services/database/plan-render.ts +108 -0
- package/src/services/database/relation-resolver.test.ts +413 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +110 -14
- package/src/services/database/sqlite-cache-engine.test.ts +464 -0
- package/src/services/database/sqlite-cache-engine.ts +1159 -0
- package/src/services/database/sqlite-open.test.ts +150 -0
- package/src/services/database/sqlite-open.ts +164 -0
- package/src/services/database/sqlite-plan-sql.test.ts +104 -0
- package/src/services/database/sqlite-plan-sql.ts +106 -0
- package/src/services/database/sqlite-select.integration.test.ts +185 -0
- package/src/services/database/sqlite-select.test.ts +246 -0
- package/src/services/database/sqlite-select.ts +113 -0
- package/src/services/database/sqlite-transport.fixture.ts +30 -0
- package/src/services/database/sqlite-transport.ts +219 -0
- package/src/services/database/sqlite-worker.ts +437 -0
- package/src/services/database/surql-translate.ts +291 -0
- package/src/services/database/surreal-cache-engine.ts +141 -0
- package/src/services/logger/index.ts +6 -109
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +11 -4
- package/src/services/persistence/surrealdb.ts +10 -10
- package/src/services/stream-processor/index.ts +444 -52
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +136 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +216 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +23 -2
- package/src/services/tabs/broker-client.ts +283 -0
- package/src/services/tabs/broker.test.ts +278 -0
- package/src/services/tabs/coordinator.test.ts +244 -0
- package/src/services/tabs/coordinator.ts +576 -0
- package/src/services/tabs/fake-ports.fixture.ts +112 -0
- package/src/services/tabs/leader-locks.ts +75 -0
- package/src/services/tabs/protocol.ts +242 -0
- package/src/services/tabs/support.ts +36 -0
- package/src/services/tabs/tabs-broker-worker.ts +586 -0
- package/src/sp00ky.auth-order.test.ts +92 -0
- package/src/sp00ky.init-query.test.ts +183 -0
- package/src/sp00ky.ts +1424 -0
- package/src/types.ts +467 -15
- package/src/utils/error-classification.test.ts +44 -0
- package/src/utils/error-classification.ts +7 -0
- package/src/utils/index.ts +73 -13
- package/src/utils/parser.ts +3 -2
- package/src/utils/semver.test.ts +32 -0
- package/src/utils/semver.ts +30 -0
- package/src/utils/surql.ts +30 -18
- package/src/utils/withRetry.test.ts +1 -1
- package/tsdown.config.ts +86 -1
- package/src/spooky.ts +0 -395
package/src/utils/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { GetTable, SchemaStructure, TableModel, TableNames } from '@spooky-sync/query-builder';
|
|
1
|
+
import type { GetTable, SchemaStructure, TableModel, TableNames } from '@spooky-sync/query-builder';
|
|
2
2
|
import { Uuid, RecordId, Duration } from 'surrealdb';
|
|
3
|
-
import { Logger } from '../services/logger/index';
|
|
4
|
-
import { QueryTimeToLive } from '../types';
|
|
3
|
+
import type { Logger } from '../services/logger/index';
|
|
4
|
+
import type { QueryTimeToLive } from '../types';
|
|
5
5
|
|
|
6
6
|
export * from './surql';
|
|
7
7
|
export * from './parser';
|
|
@@ -59,7 +59,7 @@ export function generateNewTableId<S extends SchemaStructure, T extends TableNam
|
|
|
59
59
|
|
|
60
60
|
// ==================== SCHEMA ENCODING/DECODING ====================
|
|
61
61
|
|
|
62
|
-
export function
|
|
62
|
+
export function decodeFromSp00ky<S extends SchemaStructure, T extends TableNames<S>>(
|
|
63
63
|
schema: S,
|
|
64
64
|
tableName: T,
|
|
65
65
|
record: TableModel<GetTable<S, T>>
|
|
@@ -74,19 +74,19 @@ export function decodeFromSpooky<S extends SchemaStructure, T extends TableNames
|
|
|
74
74
|
for (const field of Object.keys(table.columns)) {
|
|
75
75
|
const column = table.columns[field] as any;
|
|
76
76
|
const relation = schema.relationships.find((r) => r.from === tableName && r.field === field);
|
|
77
|
-
if ((column.recordId || relation) && encoded[field]
|
|
77
|
+
if ((column.recordId || relation) && encoded[field] !== null && encoded[field] !== undefined) {
|
|
78
78
|
if (encoded[field] instanceof RecordId) {
|
|
79
79
|
encoded[field] = `${encoded[field].table.toString()}:${encoded[field].id}`;
|
|
80
80
|
} else if (
|
|
81
81
|
relation &&
|
|
82
|
-
(encoded[field] instanceof Object || encoded[field]
|
|
82
|
+
(encoded[field] instanceof Object || Array.isArray(encoded[field]))
|
|
83
83
|
) {
|
|
84
84
|
if (Array.isArray(encoded[field])) {
|
|
85
85
|
encoded[field] = encoded[field].map((item) =>
|
|
86
|
-
|
|
86
|
+
decodeFromSp00ky(schema, relation.to, item)
|
|
87
87
|
);
|
|
88
88
|
} else {
|
|
89
|
-
encoded[field] =
|
|
89
|
+
encoded[field] = decodeFromSp00ky(schema, relation.to, encoded[field]);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -97,15 +97,25 @@ export function decodeFromSpooky<S extends SchemaStructure, T extends TableNames
|
|
|
97
97
|
|
|
98
98
|
// ==================== TIME/DURATION UTILITIES ====================
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Read the millisecond count off a surrealdb `Duration`. Different `surrealdb`
|
|
102
|
+
* releases expose it under `milliseconds` (current) or the older private
|
|
103
|
+
* `_milliseconds` field, so read whichever is set; returns 0 when neither is.
|
|
104
|
+
*/
|
|
105
|
+
function durationMillis(duration: Duration): number {
|
|
106
|
+
const d = duration as { milliseconds?: number | bigint; _milliseconds?: number | bigint };
|
|
107
|
+
return Number(d.milliseconds || d._milliseconds || 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
100
110
|
/**
|
|
101
111
|
* Parse duration string or Duration object to milliseconds
|
|
102
112
|
*/
|
|
103
113
|
export function parseDuration(duration: QueryTimeToLive | Duration): number {
|
|
104
114
|
if (duration instanceof Duration) {
|
|
105
|
-
const ms = (duration
|
|
106
|
-
if (ms) return
|
|
115
|
+
const ms = durationMillis(duration);
|
|
116
|
+
if (ms) return ms;
|
|
107
117
|
const str = duration.toString();
|
|
108
|
-
if (str !== '[object Object]') return parseDuration(str as
|
|
118
|
+
if (str !== '[object Object]') return parseDuration(str as QueryTimeToLive);
|
|
109
119
|
return 600000;
|
|
110
120
|
}
|
|
111
121
|
|
|
@@ -117,7 +127,7 @@ export function parseDuration(duration: QueryTimeToLive | Duration): number {
|
|
|
117
127
|
|
|
118
128
|
const match = duration.match(/^(\d+)([smh])$/);
|
|
119
129
|
if (!match) return 600000;
|
|
120
|
-
const val = parseInt(match[1], 10);
|
|
130
|
+
const val = Number.parseInt(match[1], 10);
|
|
121
131
|
const unit = match[2];
|
|
122
132
|
switch (unit) {
|
|
123
133
|
case 's':
|
|
@@ -137,6 +147,18 @@ export async function fileToUint8Array(file: File | Blob): Promise<Uint8Array> {
|
|
|
137
147
|
return new Uint8Array(buffer);
|
|
138
148
|
}
|
|
139
149
|
|
|
150
|
+
// ==================== TEXT UTILITIES ====================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Convert plain text to simple HTML paragraphs.
|
|
154
|
+
* Useful for seeding a rich-text editor (e.g. TipTap/ProseMirror) with fallback content.
|
|
155
|
+
*/
|
|
156
|
+
export function textToHtml(text: string): string {
|
|
157
|
+
return text
|
|
158
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
159
|
+
.split('\n').map((l) => `<p>${l || '<br>'}</p>`).join('');
|
|
160
|
+
}
|
|
161
|
+
|
|
140
162
|
// ==================== DATABASE UTILITIES ====================
|
|
141
163
|
|
|
142
164
|
/**
|
|
@@ -165,7 +187,7 @@ export async function withRetry<T>(
|
|
|
165
187
|
attempt: i + 1,
|
|
166
188
|
retries,
|
|
167
189
|
error: msg,
|
|
168
|
-
Category: '
|
|
190
|
+
Category: 'sp00ky-client::utils::withRetry',
|
|
169
191
|
},
|
|
170
192
|
'Retrying DB operation'
|
|
171
193
|
);
|
|
@@ -177,3 +199,41 @@ export async function withRetry<T>(
|
|
|
177
199
|
}
|
|
178
200
|
throw lastError;
|
|
179
201
|
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Reject after `timeoutMs` if `promise` hasn't settled.
|
|
205
|
+
*
|
|
206
|
+
* Exists because a WebSocket RPC has no deadline of its own: the SurrealDB SDK
|
|
207
|
+
* parks each call in a pending map and only rejects it when the socket reports
|
|
208
|
+
* a close. On a half-open socket (peer gone, no `close` event, `readyState`
|
|
209
|
+
* still OPEN) the call never settles at all, which wedges anything serialized
|
|
210
|
+
* behind it.
|
|
211
|
+
*
|
|
212
|
+
* `message` MUST contain "timed out" so `classifySyncError` treats the
|
|
213
|
+
* rejection as `network` — that's what makes the sync queues retry the
|
|
214
|
+
* operation instead of rolling the mutation back as an application error.
|
|
215
|
+
*
|
|
216
|
+
* Non-positive `timeoutMs` returns `promise` unchanged. The underlying promise
|
|
217
|
+
* is NOT cancelled (nothing can cancel an in-flight RPC); its eventual
|
|
218
|
+
* settlement is absorbed, so attach no expectations to it after a timeout.
|
|
219
|
+
*/
|
|
220
|
+
export function withTimeout<T>(
|
|
221
|
+
promise: Promise<T>,
|
|
222
|
+
timeoutMs: number,
|
|
223
|
+
message: string
|
|
224
|
+
): Promise<T> {
|
|
225
|
+
if (!(timeoutMs > 0)) return promise;
|
|
226
|
+
return new Promise<T>((resolve, reject) => {
|
|
227
|
+
const timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
228
|
+
promise.then(
|
|
229
|
+
(value) => {
|
|
230
|
+
clearTimeout(timer);
|
|
231
|
+
resolve(value);
|
|
232
|
+
},
|
|
233
|
+
(err) => {
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
reject(err);
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
});
|
|
239
|
+
}
|
package/src/utils/parser.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ColumnSchema
|
|
1
|
+
import type { ColumnSchema} from '@spooky-sync/query-builder';
|
|
2
|
+
import { RecordId } from '@spooky-sync/query-builder';
|
|
2
3
|
import { parseRecordIdString } from './index';
|
|
3
4
|
import { DateTime } from 'surrealdb';
|
|
4
5
|
|
|
@@ -8,7 +9,7 @@ export function cleanRecord(
|
|
|
8
9
|
): Record<string, any> {
|
|
9
10
|
const cleaned: Record<string, any> = {};
|
|
10
11
|
for (const [key, value] of Object.entries(record)) {
|
|
11
|
-
if (key === 'id' || key in tableSchema) {
|
|
12
|
+
if (key === 'id' || key.startsWith('_00_') || key in tableSchema) {
|
|
12
13
|
cleaned[key] = value;
|
|
13
14
|
}
|
|
14
15
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { semverGt } from './semver';
|
|
3
|
+
|
|
4
|
+
describe('semverGt', () => {
|
|
5
|
+
it('compares strictly greater', () => {
|
|
6
|
+
expect(semverGt('1.0.1', '1.0.0')).toBe(true);
|
|
7
|
+
expect(semverGt('1.1.0', '1.0.9')).toBe(true);
|
|
8
|
+
expect(semverGt('2.0.0', '1.9.9')).toBe(true);
|
|
9
|
+
expect(semverGt('1.10.0', '1.9.0')).toBe(true); // numeric, not lexicographic
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('equal and lower are not greater', () => {
|
|
13
|
+
expect(semverGt('1.0.0', '1.0.0')).toBe(false);
|
|
14
|
+
expect(semverGt('1.0.0', '1.0.1')).toBe(false);
|
|
15
|
+
expect(semverGt('0.9.9', '1.0.0')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('missing parts default to zero', () => {
|
|
19
|
+
expect(semverGt('1.2', '1.2.0')).toBe(false);
|
|
20
|
+
expect(semverGt('1.2.1', '1.2')).toBe(true);
|
|
21
|
+
expect(semverGt('2', '1.9.9')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('junk never compares greater', () => {
|
|
25
|
+
expect(semverGt('abc', '1.0.0')).toBe(false);
|
|
26
|
+
expect(semverGt('1.0.0', 'abc')).toBe(false);
|
|
27
|
+
expect(semverGt('', '0.0.0')).toBe(false);
|
|
28
|
+
expect(semverGt(null, undefined)).toBe(false);
|
|
29
|
+
expect(semverGt('1.0.0-beta', '0.9.0')).toBe(false); // prerelease unsupported
|
|
30
|
+
expect(semverGt('1.2.3.4', '1.0.0')).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Minimal semver comparison for app-release version checks. "X.Y.Z" numeric
|
|
2
|
+
// with missing parts read as 0 ("1.2" == "1.2.0"); any malformed input never
|
|
3
|
+
// compares greater, so a bad release row can never nag (or force-reload)
|
|
4
|
+
// every client.
|
|
5
|
+
|
|
6
|
+
function parse(v: unknown): [number, number, number] | null {
|
|
7
|
+
const parts = String(v ?? '')
|
|
8
|
+
.trim()
|
|
9
|
+
.split('.');
|
|
10
|
+
if (parts.length === 0 || parts.length > 3 || parts[0] === '') return null;
|
|
11
|
+
const nums: number[] = [];
|
|
12
|
+
for (let i = 0; i < 3; i++) {
|
|
13
|
+
const raw = parts[i] ?? '0';
|
|
14
|
+
if (!/^\d+$/.test(raw)) return null;
|
|
15
|
+
nums.push(parseInt(raw, 10));
|
|
16
|
+
}
|
|
17
|
+
return nums as [number, number, number];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** True when `a` is a valid version strictly greater than valid version `b`. */
|
|
21
|
+
export function semverGt(a: unknown, b: unknown): boolean {
|
|
22
|
+
const pa = parse(a);
|
|
23
|
+
const pb = parse(b);
|
|
24
|
+
if (!pa || !pb) return false;
|
|
25
|
+
for (let i = 0; i < 3; i++) {
|
|
26
|
+
if (pa[i] > pb[i]) return true;
|
|
27
|
+
if (pa[i] < pb[i]) return false;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
package/src/utils/surql.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MutationEventType } from '../types';
|
|
1
|
+
import type { MutationEventType } from '../types';
|
|
2
2
|
|
|
3
3
|
// ==================== TYPES ====================
|
|
4
4
|
|
|
@@ -34,6 +34,7 @@ export interface SurqlHelper {
|
|
|
34
34
|
keyDataVars: ({ key: string; variable: string } | { statement: string } | string)[]
|
|
35
35
|
): string;
|
|
36
36
|
upsert(idVar: string, dataVar: string): string;
|
|
37
|
+
upsertMerge(idVar: string, dataVar: string): string;
|
|
37
38
|
updateMerge(idVar: string, dataVar: string): string;
|
|
38
39
|
updateSet(
|
|
39
40
|
idVar: string,
|
|
@@ -90,17 +91,9 @@ export const surql: SurqlHelper = {
|
|
|
90
91
|
returnValues: ({ field: string; alias: string } | string)[]
|
|
91
92
|
) {
|
|
92
93
|
return `SELECT ${returnValues
|
|
93
|
-
.map((
|
|
94
|
-
typeof returnValues === 'string'
|
|
95
|
-
? returnValues
|
|
96
|
-
: `${returnValues.field} as ${returnValues.alias}`
|
|
97
|
-
)
|
|
94
|
+
.map((rv) => (typeof rv === 'string' ? rv : `${rv.field} as ${rv.alias}`))
|
|
98
95
|
.join(',')} FROM ${table} WHERE ${whereVar
|
|
99
|
-
.map((
|
|
100
|
-
typeof whereVar === 'string'
|
|
101
|
-
? `${whereVar} = $${whereVar}`
|
|
102
|
-
: `${whereVar.field} = $${whereVar.variable}`
|
|
103
|
-
)
|
|
96
|
+
.map((wv) => (typeof wv === 'string' ? `${wv} = $${wv}` : `${wv.field} = $${wv.variable}`))
|
|
104
97
|
.join(' AND ')}`;
|
|
105
98
|
},
|
|
106
99
|
|
|
@@ -127,6 +120,14 @@ export const surql: SurqlHelper = {
|
|
|
127
120
|
return `UPSERT ONLY $${idVar} REPLACE $${dataVar}`;
|
|
128
121
|
},
|
|
129
122
|
|
|
123
|
+
// Use this for sync-down ingestion: the remote payload omits local-only
|
|
124
|
+
// fields injected by the CLI's local-schema setup (`_00_crdt`,
|
|
125
|
+
// `_00_cursor`), and REPLACE would wipe them on every round-trip and
|
|
126
|
+
// break offline reload of CRDT state.
|
|
127
|
+
upsertMerge(idVar: string, dataVar: string) {
|
|
128
|
+
return `UPSERT ONLY $${idVar} MERGE $${dataVar}`;
|
|
129
|
+
},
|
|
130
|
+
|
|
130
131
|
updateMerge(idVar: string, dataVar: string) {
|
|
131
132
|
return `UPDATE ONLY $${idVar} MERGE $${dataVar}`;
|
|
132
133
|
},
|
|
@@ -136,12 +137,12 @@ export const surql: SurqlHelper = {
|
|
|
136
137
|
keyDataVar: ({ key: string; variable: string } | { statement: string } | string)[]
|
|
137
138
|
) {
|
|
138
139
|
return `UPDATE $${idVar} SET ${keyDataVar
|
|
139
|
-
.map((
|
|
140
|
-
typeof
|
|
141
|
-
? `${
|
|
142
|
-
: 'statement' in
|
|
143
|
-
?
|
|
144
|
-
: `${
|
|
140
|
+
.map((kdv) =>
|
|
141
|
+
typeof kdv === 'string'
|
|
142
|
+
? `${kdv} = $${kdv}`
|
|
143
|
+
: 'statement' in kdv
|
|
144
|
+
? kdv.statement
|
|
145
|
+
: `${kdv.key} = $${kdv.variable}`
|
|
145
146
|
)
|
|
146
147
|
.join(', ')}`;
|
|
147
148
|
},
|
|
@@ -163,7 +164,18 @@ export const surql: SurqlHelper = {
|
|
|
163
164
|
) {
|
|
164
165
|
switch (t) {
|
|
165
166
|
case 'create':
|
|
166
|
-
|
|
167
|
+
// `data` is REQUIRED here, not decorative. The outbox row is the only
|
|
168
|
+
// copy of a pending create once the in-memory UpEvent is gone (reload,
|
|
169
|
+
// or a shared-tabs follower whose row is replayed by the leader). This
|
|
170
|
+
// used to drop `dataVar` on the floor, so every replayed create arrived
|
|
171
|
+
// with `data: undefined` and `processUpEvent` threw on
|
|
172
|
+
// `Object.keys(event.data)` before it ever reached the network: the
|
|
173
|
+
// create was unsendable AND it sat at the head of the queue blocking
|
|
174
|
+
// every later mutation. The payload was simply never persisted, so
|
|
175
|
+
// those creates were unrecoverable.
|
|
176
|
+
return dataVar
|
|
177
|
+
? `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}, data = $${dataVar}`
|
|
178
|
+
: `CREATE ONLY $${mutationIdVar} SET mutationType = 'create', recordId = $${recordIdVar}`;
|
|
167
179
|
case 'update': {
|
|
168
180
|
let stmt = `CREATE ONLY $${mutationIdVar} SET mutationType = 'update', recordId = $${recordIdVar}, data = $${dataVar}`;
|
|
169
181
|
if (beforeRecordVar) {
|
package/tsdown.config.ts
CHANGED
|
@@ -1,9 +1,94 @@
|
|
|
1
1
|
import { defineConfig } from 'tsdown';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
|
|
6
|
+
// Resolve real package versions at build time so the DevTools can report the
|
|
7
|
+
// actual bundled frontend versions (used to detect frontend/backend drift).
|
|
8
|
+
const corePkg = require('./package.json');
|
|
9
|
+
const coreVersion: string = corePkg.version;
|
|
10
|
+
// ssp-wasm has no `exports` map; resolve its package.json directly, falling
|
|
11
|
+
// back to the relative workspace path if module resolution can't find it.
|
|
12
|
+
let wasmVersion: string;
|
|
13
|
+
try {
|
|
14
|
+
wasmVersion = require('@spooky-sync/ssp-wasm/package.json').version;
|
|
15
|
+
} catch {
|
|
16
|
+
wasmVersion = require('../ssp-wasm/package.json').version;
|
|
17
|
+
}
|
|
18
|
+
// Report the SurrealDB WASM ENGINE version (`@surrealdb/wasm`) — the engine the
|
|
19
|
+
// in-browser local DB actually runs — NOT the `surrealdb` JS client library
|
|
20
|
+
// version. The WASM engine is the one that must stay compatible with the server
|
|
21
|
+
// engine, so it's the meaningful number to compare in DevTools. Prefer the
|
|
22
|
+
// actually-installed version; fall back to our pinned range if its `exports`
|
|
23
|
+
// map blocks importing its package.json.
|
|
24
|
+
let surrealVersion: string;
|
|
25
|
+
try {
|
|
26
|
+
surrealVersion = require('@surrealdb/wasm/package.json').version;
|
|
27
|
+
} catch {
|
|
28
|
+
surrealVersion = String(corePkg.dependencies?.['@surrealdb/wasm'] ?? 'unknown').replace(
|
|
29
|
+
/^[\^~]/,
|
|
30
|
+
''
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// tsdown 0.12.x forwards a top-level `define` to rolldown's inputOptions, which
|
|
35
|
+
// rejects it — so we do the substitution ourselves with a tiny transform
|
|
36
|
+
// plugin. Only our own modules reference the `__SP00KY_*__` identifiers, and
|
|
37
|
+
// the early `includes` check skips everything else.
|
|
38
|
+
const replacements: Record<string, string> = {
|
|
39
|
+
__SP00KY_CORE_VERSION__: JSON.stringify(coreVersion),
|
|
40
|
+
__SP00KY_WASM_VERSION__: JSON.stringify(wasmVersion),
|
|
41
|
+
__SP00KY_SURREAL_VERSION__: JSON.stringify(surrealVersion),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const versionDefinePlugin = {
|
|
45
|
+
name: 'sp00ky-version-define',
|
|
46
|
+
transform(code: string) {
|
|
47
|
+
if (!code.includes('__SP00KY_')) return null;
|
|
48
|
+
let out = code;
|
|
49
|
+
for (const [token, value] of Object.entries(replacements)) {
|
|
50
|
+
out = out.split(token).join(value);
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Worker URLs are written as `./<name>.ts` in source so Vite's src-bundling
|
|
57
|
+
// consumers (the example app aliases core to `src`) resolve them. In the
|
|
58
|
+
// published build each worker is emitted at `dist/<name>.js`, so rewrite the
|
|
59
|
+
// references to `.js` here — otherwise the flat `dist/index.js` carries
|
|
60
|
+
// dangling `.ts` URLs that a consumer's bundler can't resolve.
|
|
61
|
+
const workerUrlPlugin = {
|
|
62
|
+
name: 'sp00ky-worker-url',
|
|
63
|
+
transform(code: string) {
|
|
64
|
+
if (!code.includes('sqlite-worker.ts') && !code.includes('tabs-broker-worker.ts')) return null;
|
|
65
|
+
return code
|
|
66
|
+
.split('sqlite-worker.ts')
|
|
67
|
+
.join('sqlite-worker.js')
|
|
68
|
+
.split('tabs-broker-worker.ts')
|
|
69
|
+
.join('tabs-broker-worker.js');
|
|
70
|
+
},
|
|
71
|
+
};
|
|
2
72
|
|
|
3
73
|
export default defineConfig({
|
|
4
|
-
|
|
74
|
+
// `sqlite-worker` is emitted at `dist/sqlite-worker.js` (top level, NOT under
|
|
75
|
+
// services/database) so the `new URL('./sqlite-worker.js', import.meta.url)`
|
|
76
|
+
// in `SqliteCacheEngine` — which gets bundled into the flat `dist/index.js` —
|
|
77
|
+
// resolves correctly. It (and `@sqlite.org/sqlite-wasm`) load lazily in the
|
|
78
|
+
// Worker; never pulled into the main bundle unless `localEngine: 'sqlite'`.
|
|
79
|
+
entry: {
|
|
80
|
+
index: 'src/index.ts',
|
|
81
|
+
'otel/index': 'src/otel/index.ts',
|
|
82
|
+
'sqlite-worker': 'src/services/database/sqlite-worker.ts',
|
|
83
|
+
// The shared-tabs broker MUST stay a self-contained emit (no chunk
|
|
84
|
+
// imports): bundlers cannot trace `new SharedWorker(url)` graphs, so a
|
|
85
|
+
// relative chunk import would 404 in consumers. The source keeps zero
|
|
86
|
+
// runtime imports and scripts/check-broker-bundle.mjs enforces it.
|
|
87
|
+
'tabs-broker-worker': 'src/services/tabs/tabs-broker-worker.ts',
|
|
88
|
+
},
|
|
5
89
|
format: ['esm'],
|
|
6
90
|
dts: true,
|
|
7
91
|
clean: true,
|
|
8
92
|
hash: false,
|
|
93
|
+
plugins: [versionDefinePlugin, workerUrlPlugin],
|
|
9
94
|
});
|