@spooky-sync/core 0.0.1-canary.7 → 0.0.1-canary.71
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 +56 -0
- package/dist/index.d.ts +299 -369
- package/dist/index.js +2278 -399
- package/dist/otel/index.d.ts +21 -0
- package/dist/otel/index.js +86 -0
- package/dist/types.d.ts +460 -0
- package/package.json +37 -7
- package/skills/sp00ky-core/SKILL.md +258 -0
- package/skills/sp00ky-core/references/auth.md +98 -0
- package/skills/sp00ky-core/references/config.md +76 -0
- 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 +3 -2
- package/src/modules/auth/events/index.ts +2 -1
- package/src/modules/auth/index.ts +17 -20
- package/src/modules/cache/index.ts +41 -29
- package/src/modules/cache/types.ts +2 -2
- package/src/modules/crdt/crdt-field.ts +281 -0
- package/src/modules/crdt/crdt-hydration.test.ts +206 -0
- package/src/modules/crdt/index.ts +352 -0
- package/src/modules/data/data.status.test.ts +108 -0
- package/src/modules/data/index.ts +662 -108
- package/src/modules/data/window-query.test.ts +52 -0
- package/src/modules/data/window-query.ts +130 -0
- package/src/modules/devtools/index.ts +89 -21
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +81 -0
- package/src/modules/ref-tables.test.ts +56 -0
- package/src/modules/ref-tables.ts +57 -0
- package/src/modules/sync/engine.ts +97 -37
- package/src/modules/sync/events/index.ts +3 -2
- package/src/modules/sync/queue/queue-down.ts +5 -4
- package/src/modules/sync/queue/queue-up.ts +14 -13
- package/src/modules/sync/scheduler.ts +2 -2
- package/src/modules/sync/sync.ts +553 -58
- package/src/modules/sync/utils.test.ts +239 -2
- package/src/modules/sync/utils.ts +166 -17
- package/src/otel/index.ts +127 -0
- package/src/services/database/database.ts +11 -11
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/local-migrator.ts +21 -21
- package/src/services/database/local.test.ts +33 -0
- package/src/services/database/local.ts +192 -32
- package/src/services/database/remote.ts +13 -13
- package/src/services/logger/index.ts +6 -101
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +41 -0
- package/src/services/persistence/surrealdb.ts +9 -9
- package/src/services/stream-processor/index.ts +205 -36
- 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.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +18 -2
- package/src/sp00ky.auth-order.test.ts +65 -0
- package/src/sp00ky.ts +582 -0
- package/src/types.ts +132 -13
- package/src/utils/index.ts +35 -13
- package/src/utils/parser.ts +3 -2
- package/src/utils/surql.ts +24 -15
- package/src/utils/withRetry.test.ts +1 -1
- package/tsdown.config.ts +55 -1
- package/src/spooky.ts +0 -392
package/src/types.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { RecordId, SchemaStructure } from '@spooky-sync/query-builder';
|
|
2
|
-
import { Level } from 'pino';
|
|
3
|
-
import { PushEventOptions } from './events/index';
|
|
4
|
-
import { UpEvent } from './modules/sync/index';
|
|
1
|
+
import type { RecordId, SchemaStructure } from '@spooky-sync/query-builder';
|
|
2
|
+
import type { Level, LoggerOptions } from 'pino';
|
|
3
|
+
import type { PushEventOptions } from './events/index';
|
|
4
|
+
import type { UpEvent } from './modules/sync/index';
|
|
5
5
|
|
|
6
|
-
export type { Level }
|
|
6
|
+
export type { Level };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A pino browser transmit object for forwarding logs to an external sink (e.g. OpenTelemetry).
|
|
10
|
+
*/
|
|
11
|
+
export type PinoTransmit = NonNullable<NonNullable<LoggerOptions['browser']>['transmit']>;
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* The type of storage backend to use for the local database.
|
|
@@ -65,22 +70,22 @@ export type QueryTimeToLive =
|
|
|
65
70
|
/**
|
|
66
71
|
* Result object returned when a query is registered or executed.
|
|
67
72
|
*/
|
|
68
|
-
export interface
|
|
73
|
+
export interface Sp00kyQueryResult {
|
|
69
74
|
/** The unique hash identifier for the query. */
|
|
70
75
|
hash: string;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
export type
|
|
78
|
+
export type Sp00kyQueryResultPromise = Promise<Sp00kyQueryResult>;
|
|
74
79
|
|
|
75
80
|
export interface EventSubscriptionOptions {
|
|
76
81
|
priority?: number;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
|
-
* Configuration options for the
|
|
85
|
+
* Configuration options for the Sp00ky client.
|
|
81
86
|
* @template S The schema structure type.
|
|
82
87
|
*/
|
|
83
|
-
export interface
|
|
88
|
+
export interface Sp00kyConfig<S extends SchemaStructure> {
|
|
84
89
|
/** Database connection configuration. */
|
|
85
90
|
database: {
|
|
86
91
|
/** The SurrealDB endpoint URL. */
|
|
@@ -94,8 +99,6 @@ export interface SpookyConfig<S extends SchemaStructure> {
|
|
|
94
99
|
/** Authentication token. */
|
|
95
100
|
token?: string;
|
|
96
101
|
};
|
|
97
|
-
/** Unique client identifier. If not provided, one will be generated. */
|
|
98
|
-
clientId?: string;
|
|
99
102
|
/** The schema definition. */
|
|
100
103
|
schema: S;
|
|
101
104
|
/** The compiled SURQL schema string. */
|
|
@@ -107,13 +110,39 @@ export interface SpookyConfig<S extends SchemaStructure> {
|
|
|
107
110
|
* Can be a custom implementation, 'surrealdb' (default), or 'localstorage'.
|
|
108
111
|
*/
|
|
109
112
|
persistenceClient?: PersistenceClient | 'surrealdb' | 'localstorage';
|
|
110
|
-
/**
|
|
111
|
-
|
|
113
|
+
/** A pino browser transmit object for forwarding logs (e.g. via @spooky-sync/core/otel). */
|
|
114
|
+
otelTransmit?: PinoTransmit;
|
|
112
115
|
/**
|
|
113
116
|
* Debounce time in milliseconds for stream updates.
|
|
114
117
|
* Defaults to 100ms.
|
|
115
118
|
*/
|
|
116
119
|
streamDebounceTime?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Debounce time in milliseconds for syncing collaborative (CRDT) field
|
|
122
|
+
* changes to the remote database. Local writes happen immediately on
|
|
123
|
+
* every keystroke (so reload/offline works), but the remote UPSERT is
|
|
124
|
+
* coalesced over this window. Lower = snappier remote propagation +
|
|
125
|
+
* more network traffic; higher = less traffic + more lag for other
|
|
126
|
+
* collaborators. Defaults to 500ms.
|
|
127
|
+
*/
|
|
128
|
+
crdtDebounceMs?: number;
|
|
129
|
+
/**
|
|
130
|
+
* Cadence (ms) for the `_00_list_ref` poll that catches cross-session
|
|
131
|
+
* UPDATEs the SurrealDB v3 LIVE-permission gap drops. Lower = faster
|
|
132
|
+
* convergence + more query load; higher = the inverse. Non-positive
|
|
133
|
+
* values fall back to the default (500ms).
|
|
134
|
+
*/
|
|
135
|
+
refSyncIntervalMs?: number;
|
|
136
|
+
/**
|
|
137
|
+
* Instant-hydrate cold queries: when a query is registered with no local
|
|
138
|
+
* data yet, first run its surql directly on the remote (one-shot) and display
|
|
139
|
+
* the result immediately, THEN do the full realtime registration in the
|
|
140
|
+
* background. The hydrated rows are ingested with their versions so the
|
|
141
|
+
* registration's `syncRecords` skips re-pulling unchanged bodies. Cuts cold
|
|
142
|
+
* first-paint from ~one full registration round-trip to ~one query.
|
|
143
|
+
* Defaults to `true`; set `false` to keep the old wait-for-registration path.
|
|
144
|
+
*/
|
|
145
|
+
instantHydrate?: boolean;
|
|
117
146
|
}
|
|
118
147
|
|
|
119
148
|
export type QueryHash = string;
|
|
@@ -159,6 +188,15 @@ export interface QueryConfig {
|
|
|
159
188
|
|
|
160
189
|
export type QueryConfigRecord = QueryConfig & { id: string };
|
|
161
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Runtime fetch status of a live query.
|
|
193
|
+
* - `idle`: not currently fetching missing records.
|
|
194
|
+
* - `fetching`: the sync engine is fetching/ingesting missing records for this
|
|
195
|
+
* query. UI notifications are coalesced so the result lands as a single
|
|
196
|
+
* update once fetching completes.
|
|
197
|
+
*/
|
|
198
|
+
export type QueryStatus = 'idle' | 'fetching';
|
|
199
|
+
|
|
162
200
|
/**
|
|
163
201
|
* Internal state of a live query.
|
|
164
202
|
*/
|
|
@@ -167,16 +205,95 @@ export interface QueryState {
|
|
|
167
205
|
config: QueryConfig;
|
|
168
206
|
/** The current cached records for this query. */
|
|
169
207
|
records: Record<string, any>[];
|
|
208
|
+
/** Set once `applyHydration` has run for this query, so the cold instant-hydrate
|
|
209
|
+
* path fires at most once per query (see DataModule.isCold/applyHydration). */
|
|
210
|
+
hydrated?: boolean;
|
|
170
211
|
/** Timer for TTL expiration. */
|
|
171
212
|
ttlTimer: NodeJS.Timeout | null;
|
|
172
213
|
/** TTL duration in milliseconds. */
|
|
173
214
|
ttlDurationMs: number;
|
|
174
215
|
/** Number of times the query has been updated. */
|
|
175
216
|
updateCount: number;
|
|
217
|
+
/**
|
|
218
|
+
* Rolling window of the most recent materialization-step latencies (ms).
|
|
219
|
+
* Capped at MATERIALIZATION_SAMPLE_WINDOW; used to recompute p55/p90/p99
|
|
220
|
+
* before each persist to `_00_query`. Samples themselves are not persisted.
|
|
221
|
+
*/
|
|
222
|
+
materializationSamples: number[];
|
|
223
|
+
/** Most recent end-to-end ingest latency in ms, or null until the first ingest. */
|
|
224
|
+
lastIngestLatencyMs: number | null;
|
|
225
|
+
/** Cumulative count of ingest/materialization errors observed for this query. */
|
|
226
|
+
errorCount: number;
|
|
227
|
+
/**
|
|
228
|
+
* Ephemeral runtime fetch status. Not persisted to `_00_query`; observable
|
|
229
|
+
* via DevTools and the `useQuery` hook. `fetching` while the sync engine is
|
|
230
|
+
* pulling missing records for this query, otherwise `idle`.
|
|
231
|
+
*/
|
|
232
|
+
status: QueryStatus;
|
|
233
|
+
/**
|
|
234
|
+
* Rolling per-phase timing samples (ms), in addition to `materializationSamples`
|
|
235
|
+
* (which holds the SSP whole-ingest wall time). Keyed by `TimingPhase` minus
|
|
236
|
+
* `ssp`. Not persisted — surfaced live to DevTools + MCP via `phaseTimings`.
|
|
237
|
+
*/
|
|
238
|
+
phaseSamples: Record<string, number[]>;
|
|
239
|
+
/** Most recent sample (ms) per phase, or null. */
|
|
240
|
+
phaseLast: Record<string, number | null>;
|
|
241
|
+
/** One-shot SSP registration timings (ms). */
|
|
242
|
+
registrationTimings: RegistrationTimings;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Cap on the rolling materialization-sample window kept per query in memory. */
|
|
246
|
+
export const MATERIALIZATION_SAMPLE_WINDOW = 100;
|
|
247
|
+
|
|
248
|
+
/** Timed processing phases surfaced per query. `ssp` is the WASM-ingest wall
|
|
249
|
+
* time; the `ssp*` phases are its internal breakdown from the SSP binding. */
|
|
250
|
+
export type TimingPhase =
|
|
251
|
+
| 'ssp'
|
|
252
|
+
| 'sspStoreApply'
|
|
253
|
+
| 'sspCircuitStep'
|
|
254
|
+
| 'sspTransform'
|
|
255
|
+
| 'localFetch'
|
|
256
|
+
| 'remoteFetch'
|
|
257
|
+
| 'frontend';
|
|
258
|
+
|
|
259
|
+
/** One-shot registration timings (ms), captured once when a query registers. */
|
|
260
|
+
export interface RegistrationTimings {
|
|
261
|
+
/** SSP surql→plan parse + permission injection. */
|
|
262
|
+
parseMs: number | null;
|
|
263
|
+
/** SSP operator-DAG build. */
|
|
264
|
+
planMs: number | null;
|
|
265
|
+
/** SSP initial snapshot evaluation. */
|
|
266
|
+
snapshotMs: number | null;
|
|
267
|
+
/** Wall time of `cache.registerQuery` (register_view round-trip). */
|
|
268
|
+
wallMs: number | null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Percentile summary for one timed phase, surfaced to DevTools + MCP. */
|
|
272
|
+
export interface PhaseStat {
|
|
273
|
+
lastMs: number | null;
|
|
274
|
+
p50: number | null;
|
|
275
|
+
p90: number | null;
|
|
276
|
+
p99: number | null;
|
|
277
|
+
count: number;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Per-query processing-time breakdown surfaced via DevTools panel + MCP. */
|
|
281
|
+
export interface QueryTimings {
|
|
282
|
+
ssp: PhaseStat;
|
|
283
|
+
sspStoreApply: PhaseStat;
|
|
284
|
+
sspCircuitStep: PhaseStat;
|
|
285
|
+
sspTransform: PhaseStat;
|
|
286
|
+
localFetch: PhaseStat;
|
|
287
|
+
remoteFetch: PhaseStat;
|
|
288
|
+
frontend: PhaseStat;
|
|
289
|
+
registration: RegistrationTimings;
|
|
290
|
+
updateCount: number;
|
|
291
|
+
errorCount: number;
|
|
176
292
|
}
|
|
177
293
|
|
|
178
294
|
// Callback types
|
|
179
295
|
export type QueryUpdateCallback = (records: Record<string, any>[]) => void;
|
|
296
|
+
export type QueryStatusCallback = (status: QueryStatus) => void;
|
|
180
297
|
export type MutationCallback = (mutations: UpEvent[]) => void;
|
|
181
298
|
|
|
182
299
|
export type MutationEventType = 'create' | 'update' | 'delete';
|
|
@@ -209,6 +326,8 @@ export interface RunOptions {
|
|
|
209
326
|
assignedTo?: string;
|
|
210
327
|
max_retries?: number;
|
|
211
328
|
retry_strategy?: 'linear' | 'exponential';
|
|
329
|
+
/** Timeout in seconds for the backend HTTP call. Only used if the backend allows timeout override. */
|
|
330
|
+
timeout?: number;
|
|
212
331
|
}
|
|
213
332
|
|
|
214
333
|
/**
|
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
|
);
|
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
|
}
|
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,16 +91,16 @@ export const surql: SurqlHelper = {
|
|
|
90
91
|
returnValues: ({ field: string; alias: string } | string)[]
|
|
91
92
|
) {
|
|
92
93
|
return `SELECT ${returnValues
|
|
93
|
-
.map((
|
|
94
|
-
typeof
|
|
95
|
-
?
|
|
96
|
-
: `${
|
|
94
|
+
.map((rv) =>
|
|
95
|
+
typeof rv === 'string'
|
|
96
|
+
? rv
|
|
97
|
+
: `${rv.field} as ${rv.alias}`
|
|
97
98
|
)
|
|
98
99
|
.join(',')} FROM ${table} WHERE ${whereVar
|
|
99
|
-
.map((
|
|
100
|
-
typeof
|
|
101
|
-
? `${
|
|
102
|
-
: `${
|
|
100
|
+
.map((wv) =>
|
|
101
|
+
typeof wv === 'string'
|
|
102
|
+
? `${wv} = $${wv}`
|
|
103
|
+
: `${wv.field} = $${wv.variable}`
|
|
103
104
|
)
|
|
104
105
|
.join(' AND ')}`;
|
|
105
106
|
},
|
|
@@ -127,6 +128,14 @@ export const surql: SurqlHelper = {
|
|
|
127
128
|
return `UPSERT ONLY $${idVar} REPLACE $${dataVar}`;
|
|
128
129
|
},
|
|
129
130
|
|
|
131
|
+
// Use this for sync-down ingestion: the remote payload omits local-only
|
|
132
|
+
// fields injected by the CLI's local-schema setup (`_00_crdt`,
|
|
133
|
+
// `_00_cursor`), and REPLACE would wipe them on every round-trip and
|
|
134
|
+
// break offline reload of CRDT state.
|
|
135
|
+
upsertMerge(idVar: string, dataVar: string) {
|
|
136
|
+
return `UPSERT ONLY $${idVar} MERGE $${dataVar}`;
|
|
137
|
+
},
|
|
138
|
+
|
|
130
139
|
updateMerge(idVar: string, dataVar: string) {
|
|
131
140
|
return `UPDATE ONLY $${idVar} MERGE $${dataVar}`;
|
|
132
141
|
},
|
|
@@ -136,12 +145,12 @@ export const surql: SurqlHelper = {
|
|
|
136
145
|
keyDataVar: ({ key: string; variable: string } | { statement: string } | string)[]
|
|
137
146
|
) {
|
|
138
147
|
return `UPDATE $${idVar} SET ${keyDataVar
|
|
139
|
-
.map((
|
|
140
|
-
typeof
|
|
141
|
-
? `${
|
|
142
|
-
: 'statement' in
|
|
143
|
-
?
|
|
144
|
-
: `${
|
|
148
|
+
.map((kdv) =>
|
|
149
|
+
typeof kdv === 'string'
|
|
150
|
+
? `${kdv} = $${kdv}`
|
|
151
|
+
: 'statement' in kdv
|
|
152
|
+
? kdv.statement
|
|
153
|
+
: `${kdv.key} = $${kdv.variable}`
|
|
145
154
|
)
|
|
146
155
|
.join(', ')}`;
|
|
147
156
|
},
|
package/tsdown.config.ts
CHANGED
|
@@ -1,9 +1,63 @@
|
|
|
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
|
+
};
|
|
2
55
|
|
|
3
56
|
export default defineConfig({
|
|
4
|
-
entry: ['src/index.ts'],
|
|
57
|
+
entry: ['src/index.ts', 'src/otel/index.ts'],
|
|
5
58
|
format: ['esm'],
|
|
6
59
|
dts: true,
|
|
7
60
|
clean: true,
|
|
8
61
|
hash: false,
|
|
62
|
+
plugins: [versionDefinePlugin],
|
|
9
63
|
});
|