@spooky-sync/core 0.0.1-canary.6 → 0.0.1-canary.61
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.d.ts +103 -367
- package/dist/index.js +595 -289
- package/dist/otel/index.d.ts +21 -0
- package/dist/otel/index.js +86 -0
- package/dist/types.d.ts +361 -0
- package/package.json +35 -5
- 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/events/events.test.ts +2 -1
- 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 +23 -23
- package/src/modules/cache/types.ts +2 -2
- package/src/modules/crdt/crdt-field.ts +189 -0
- package/src/modules/crdt/index.ts +178 -0
- package/src/modules/data/index.ts +64 -43
- package/src/modules/devtools/index.ts +23 -20
- package/src/modules/sync/engine.ts +31 -21
- 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 +49 -36
- package/src/modules/sync/utils.test.ts +2 -2
- package/src/modules/sync/utils.ts +15 -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.ts +16 -15
- 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 +34 -0
- package/src/services/persistence/surrealdb.ts +9 -9
- package/src/services/stream-processor/index.ts +32 -31
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +2 -2
- package/src/{spooky.ts → sp00ky.ts} +67 -38
- package/src/types.ts +18 -11
- package/src/utils/index.ts +22 -10
- package/src/utils/parser.ts +3 -2
- package/src/utils/surql.ts +15 -15
- package/src/utils/withRetry.test.ts +1 -1
- package/tsdown.config.ts +1 -1
package/dist/index.js
CHANGED
|
@@ -2,12 +2,8 @@ import { DateTime, Duration, RecordId, Surreal, Uuid, applyDiagnostics, createRe
|
|
|
2
2
|
import { QueryBuilder, RecordId as RecordId$1 } from "@spooky-sync/query-builder";
|
|
3
3
|
import { createWasmWorkerEngines } from "@surrealdb/wasm";
|
|
4
4
|
import pino from "pino";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
8
|
-
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
9
|
-
import { createContextKey } from "@opentelemetry/api";
|
|
10
|
-
import init, { SpookyProcessor } from "@spooky-sync/ssp-wasm";
|
|
5
|
+
import init, { Sp00kyProcessor } from "@spooky-sync/ssp-wasm";
|
|
6
|
+
import { LoroDoc } from "loro-crdt";
|
|
11
7
|
|
|
12
8
|
//#region src/utils/surql.ts
|
|
13
9
|
const surql = {
|
|
@@ -33,7 +29,7 @@ const surql = {
|
|
|
33
29
|
return `SELECT ${returnValues.join(",")} FROM ONLY $${idVar}`;
|
|
34
30
|
},
|
|
35
31
|
selectByFieldsAnd(table, whereVar, returnValues) {
|
|
36
|
-
return `SELECT ${returnValues.map((
|
|
32
|
+
return `SELECT ${returnValues.map((rv) => typeof rv === "string" ? rv : `${rv.field} as ${rv.alias}`).join(",")} FROM ${table} WHERE ${whereVar.map((wv) => typeof wv === "string" ? `${wv} = $${wv}` : `${wv.field} = $${wv.variable}`).join(" AND ")}`;
|
|
37
33
|
},
|
|
38
34
|
create(idVar, dataVar) {
|
|
39
35
|
return `CREATE ONLY $${idVar} CONTENT $${dataVar}`;
|
|
@@ -48,7 +44,7 @@ const surql = {
|
|
|
48
44
|
return `UPDATE ONLY $${idVar} MERGE $${dataVar}`;
|
|
49
45
|
},
|
|
50
46
|
updateSet(idVar, keyDataVar) {
|
|
51
|
-
return `UPDATE $${idVar} SET ${keyDataVar.map((
|
|
47
|
+
return `UPDATE $${idVar} SET ${keyDataVar.map((kdv) => typeof kdv === "string" ? `${kdv} = $${kdv}` : "statement" in kdv ? kdv.statement : `${kdv.key} = $${kdv.variable}`).join(", ")}`;
|
|
52
48
|
},
|
|
53
49
|
delete(idVar) {
|
|
54
50
|
return `DELETE $${idVar}`;
|
|
@@ -76,7 +72,7 @@ const surql = {
|
|
|
76
72
|
//#region src/utils/parser.ts
|
|
77
73
|
function cleanRecord(tableSchema, record) {
|
|
78
74
|
const cleaned = {};
|
|
79
|
-
for (const [key, value] of Object.entries(record)) if (key === "id" || key in tableSchema) cleaned[key] = value;
|
|
75
|
+
for (const [key, value] of Object.entries(record)) if (key === "id" || key.startsWith("_00_") || key in tableSchema) cleaned[key] = value;
|
|
80
76
|
return cleaned;
|
|
81
77
|
}
|
|
82
78
|
function parseParams(tableSchema, params) {
|
|
@@ -162,7 +158,7 @@ function parseDuration(duration) {
|
|
|
162
158
|
if (typeof duration !== "string") return 6e5;
|
|
163
159
|
const match = duration.match(/^(\d+)([smh])$/);
|
|
164
160
|
if (!match) return 6e5;
|
|
165
|
-
const val = parseInt(match[1], 10);
|
|
161
|
+
const val = Number.parseInt(match[1], 10);
|
|
166
162
|
switch (match[2]) {
|
|
167
163
|
case "s": return val * 1e3;
|
|
168
164
|
case "h": return val * 36e5;
|
|
@@ -174,6 +170,13 @@ async function fileToUint8Array(file) {
|
|
|
174
170
|
return new Uint8Array(buffer);
|
|
175
171
|
}
|
|
176
172
|
/**
|
|
173
|
+
* Convert plain text to simple HTML paragraphs.
|
|
174
|
+
* Useful for seeding a rich-text editor (e.g. TipTap/ProseMirror) with fallback content.
|
|
175
|
+
*/
|
|
176
|
+
function textToHtml(text) {
|
|
177
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").split("\n").map((l) => `<p>${l || "<br>"}</p>`).join("");
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
177
180
|
* Helper for retrying DB operations with exponential backoff
|
|
178
181
|
*/
|
|
179
182
|
async function withRetry(logger, operation, retries = 3, delayMs = 100) {
|
|
@@ -188,7 +191,7 @@ async function withRetry(logger, operation, retries = 3, delayMs = 100) {
|
|
|
188
191
|
attempt: i + 1,
|
|
189
192
|
retries,
|
|
190
193
|
error: msg,
|
|
191
|
-
Category: "
|
|
194
|
+
Category: "sp00ky-client::utils::withRetry"
|
|
192
195
|
}, "Retrying DB operation");
|
|
193
196
|
await new Promise((res) => setTimeout(res, delayMs * (i + 1)));
|
|
194
197
|
continue;
|
|
@@ -221,7 +224,7 @@ var DataModule = class {
|
|
|
221
224
|
this.logger = logger.child({ service: "DataModule" });
|
|
222
225
|
}
|
|
223
226
|
async init() {
|
|
224
|
-
this.logger.info({ Category: "
|
|
227
|
+
this.logger.info({ Category: "sp00ky-client::DataModule::init" }, "DataModule initialized");
|
|
225
228
|
}
|
|
226
229
|
/**
|
|
227
230
|
* Register a query and return its hash for subscriptions
|
|
@@ -233,27 +236,27 @@ var DataModule = class {
|
|
|
233
236
|
});
|
|
234
237
|
this.logger.debug({
|
|
235
238
|
hash,
|
|
236
|
-
Category: "
|
|
239
|
+
Category: "sp00ky-client::DataModule::query"
|
|
237
240
|
}, "Query Initialization: started");
|
|
238
|
-
const recordId = new RecordId("
|
|
241
|
+
const recordId = new RecordId("_00_query", hash);
|
|
239
242
|
if (this.activeQueries.has(hash)) {
|
|
240
243
|
this.logger.debug({
|
|
241
244
|
hash,
|
|
242
|
-
Category: "
|
|
245
|
+
Category: "sp00ky-client::DataModule::query"
|
|
243
246
|
}, "Query Initialization: exists, returning");
|
|
244
247
|
return hash;
|
|
245
248
|
}
|
|
246
249
|
if (this.pendingQueries.has(hash)) {
|
|
247
250
|
this.logger.debug({
|
|
248
251
|
hash,
|
|
249
|
-
Category: "
|
|
252
|
+
Category: "sp00ky-client::DataModule::query"
|
|
250
253
|
}, "Query Initialization: pending, waiting for existing creation");
|
|
251
254
|
await this.pendingQueries.get(hash);
|
|
252
255
|
return hash;
|
|
253
256
|
}
|
|
254
257
|
this.logger.debug({
|
|
255
258
|
hash,
|
|
256
|
-
Category: "
|
|
259
|
+
Category: "sp00ky-client::DataModule::query"
|
|
257
260
|
}, "Query Initialization: not found, creating new query");
|
|
258
261
|
const promise = this.createAndRegisterQuery(hash, recordId, surqlString, params, ttl, tableName);
|
|
259
262
|
this.pendingQueries.set(hash, promise);
|
|
@@ -311,7 +314,7 @@ var DataModule = class {
|
|
|
311
314
|
if (!queryState) {
|
|
312
315
|
this.logger.warn({
|
|
313
316
|
queryHash,
|
|
314
|
-
Category: "
|
|
317
|
+
Category: "sp00ky-client::DataModule::onStreamUpdate"
|
|
315
318
|
}, "Received update for unknown query. Skipping...");
|
|
316
319
|
return;
|
|
317
320
|
}
|
|
@@ -329,7 +332,7 @@ var DataModule = class {
|
|
|
329
332
|
if (prevJson === newJson) {
|
|
330
333
|
this.logger.debug({
|
|
331
334
|
queryHash,
|
|
332
|
-
Category: "
|
|
335
|
+
Category: "sp00ky-client::DataModule::onStreamUpdate"
|
|
333
336
|
}, "Query records unchanged, skipping notification");
|
|
334
337
|
return;
|
|
335
338
|
}
|
|
@@ -339,13 +342,13 @@ var DataModule = class {
|
|
|
339
342
|
this.logger.debug({
|
|
340
343
|
queryHash,
|
|
341
344
|
recordCount: records?.length,
|
|
342
|
-
Category: "
|
|
345
|
+
Category: "sp00ky-client::DataModule::onStreamUpdate"
|
|
343
346
|
}, "Query updated from stream");
|
|
344
347
|
} catch (err) {
|
|
345
348
|
this.logger.error({
|
|
346
349
|
err,
|
|
347
350
|
queryHash,
|
|
348
|
-
Category: "
|
|
351
|
+
Category: "sp00ky-client::DataModule::onStreamUpdate"
|
|
349
352
|
}, "Failed to fetch records for stream update");
|
|
350
353
|
}
|
|
351
354
|
}
|
|
@@ -372,7 +375,7 @@ var DataModule = class {
|
|
|
372
375
|
if (!queryState) {
|
|
373
376
|
this.logger.warn({
|
|
374
377
|
id,
|
|
375
|
-
Category: "
|
|
378
|
+
Category: "sp00ky-client::DataModule::updateQueryLocalArray"
|
|
376
379
|
}, "Query to update local array not found");
|
|
377
380
|
return;
|
|
378
381
|
}
|
|
@@ -387,7 +390,7 @@ var DataModule = class {
|
|
|
387
390
|
if (!queryState) {
|
|
388
391
|
this.logger.warn({
|
|
389
392
|
hash,
|
|
390
|
-
Category: "
|
|
393
|
+
Category: "sp00ky-client::DataModule::updateQueryRemoteArray"
|
|
391
394
|
}, "Query to update remote array not found");
|
|
392
395
|
return;
|
|
393
396
|
}
|
|
@@ -431,6 +434,7 @@ var DataModule = class {
|
|
|
431
434
|
max_retries: options?.max_retries ?? 3,
|
|
432
435
|
retry_strategy: options?.retry_strategy ?? "linear"
|
|
433
436
|
};
|
|
437
|
+
if (options?.timeout != null) record.timeout = options.timeout;
|
|
434
438
|
if (options?.assignedTo) record.assigned_to = options.assignedTo;
|
|
435
439
|
const recordId = `${tableName}:${generateId()}`;
|
|
436
440
|
await this.create(recordId, record);
|
|
@@ -444,7 +448,7 @@ var DataModule = class {
|
|
|
444
448
|
if (!tableSchema) throw new Error(`Table ${tableName} not found`);
|
|
445
449
|
const rid = parseRecordIdString(id);
|
|
446
450
|
const params = parseParams(tableSchema.columns, data);
|
|
447
|
-
const mutationId = parseRecordIdString(`
|
|
451
|
+
const mutationId = parseRecordIdString(`_00_pending_mutations:${Date.now()}`);
|
|
448
452
|
const dataKeys = Object.keys(params).map((key) => ({
|
|
449
453
|
key,
|
|
450
454
|
variable: `data_${key}`
|
|
@@ -474,7 +478,7 @@ var DataModule = class {
|
|
|
474
478
|
for (const callback of this.mutationCallbacks) callback([mutationEvent]);
|
|
475
479
|
this.logger.debug({
|
|
476
480
|
id,
|
|
477
|
-
Category: "
|
|
481
|
+
Category: "sp00ky-client::DataModule::create"
|
|
478
482
|
}, "Record created");
|
|
479
483
|
return target;
|
|
480
484
|
}
|
|
@@ -487,10 +491,10 @@ var DataModule = class {
|
|
|
487
491
|
if (!tableSchema) throw new Error(`Table ${tableName} not found`);
|
|
488
492
|
const rid = parseRecordIdString(id);
|
|
489
493
|
const params = parseParams(tableSchema.columns, data);
|
|
490
|
-
const mutationId = parseRecordIdString(`
|
|
494
|
+
const mutationId = parseRecordIdString(`_00_pending_mutations:${Date.now()}`);
|
|
491
495
|
const [beforeRecord] = await withRetry(this.logger, () => this.local.query("SELECT * FROM ONLY $id", { id: rid }));
|
|
492
496
|
const query = surql.seal(surql.tx([
|
|
493
|
-
surql.updateSet("id", [{ statement: "
|
|
497
|
+
surql.updateSet("id", [{ statement: "_00_rv += 1" }]),
|
|
494
498
|
surql.let("updated", surql.updateMerge("id", "data")),
|
|
495
499
|
surql.createMutation("update", "mid", "id", "data"),
|
|
496
500
|
surql.returnObject([{
|
|
@@ -505,14 +509,14 @@ var DataModule = class {
|
|
|
505
509
|
}));
|
|
506
510
|
const updatedFields = { id: target.id };
|
|
507
511
|
for (const key of Object.keys(data)) if (key in target) updatedFields[key] = target[key];
|
|
508
|
-
if ("
|
|
512
|
+
if ("_00_rv" in target) updatedFields._00_rv = target._00_rv;
|
|
509
513
|
this.replaceRecordInQueries(updatedFields);
|
|
510
514
|
const parsedRecord = parseParams(tableSchema.columns, target);
|
|
511
515
|
await this.cache.save({
|
|
512
516
|
table,
|
|
513
517
|
op: "UPDATE",
|
|
514
518
|
record: parsedRecord,
|
|
515
|
-
version: target.
|
|
519
|
+
version: target._00_rv
|
|
516
520
|
}, true);
|
|
517
521
|
const pushEventOptions = parseUpdateOptions(id, data, options);
|
|
518
522
|
const mutationEvent = {
|
|
@@ -527,7 +531,7 @@ var DataModule = class {
|
|
|
527
531
|
for (const callback of this.mutationCallbacks) callback([mutationEvent]);
|
|
528
532
|
this.logger.debug({
|
|
529
533
|
id,
|
|
530
|
-
Category: "
|
|
534
|
+
Category: "sp00ky-client::DataModule::update"
|
|
531
535
|
}, "Record updated");
|
|
532
536
|
return target;
|
|
533
537
|
}
|
|
@@ -538,13 +542,16 @@ var DataModule = class {
|
|
|
538
542
|
const tableName = extractTablePart(id);
|
|
539
543
|
if (!this.schema.tables.find((t) => t.name === tableName)) throw new Error(`Table ${tableName} not found`);
|
|
540
544
|
const rid = parseRecordIdString(id);
|
|
541
|
-
const mutationId = parseRecordIdString(`
|
|
545
|
+
const mutationId = parseRecordIdString(`_00_pending_mutations:${Date.now()}`);
|
|
546
|
+
const [beforeRecords] = await this.local.query("SELECT * FROM ONLY $id", { id: rid });
|
|
547
|
+
const beforeRecord = beforeRecords ?? {};
|
|
542
548
|
const query = surql.seal(surql.tx([surql.delete("id"), surql.createMutation("delete", "mid", "id")]));
|
|
543
549
|
await withRetry(this.logger, () => this.local.execute(query, {
|
|
544
550
|
id: rid,
|
|
545
551
|
mid: mutationId
|
|
546
552
|
}));
|
|
547
|
-
await this.cache.delete(table, id, true);
|
|
553
|
+
await this.cache.delete(table, id, true, beforeRecord);
|
|
554
|
+
for (const [queryHash, queryState] of this.activeQueries) if (queryState.config.tableName === tableName) await this.notifyQuerySynced(queryHash);
|
|
548
555
|
const mutationEvent = {
|
|
549
556
|
type: "delete",
|
|
550
557
|
mutation_id: mutationId,
|
|
@@ -553,7 +560,7 @@ var DataModule = class {
|
|
|
553
560
|
for (const callback of this.mutationCallbacks) callback([mutationEvent]);
|
|
554
561
|
this.logger.debug({
|
|
555
562
|
id,
|
|
556
|
-
Category: "
|
|
563
|
+
Category: "sp00ky-client::DataModule::delete"
|
|
557
564
|
}, "Record deleted");
|
|
558
565
|
}
|
|
559
566
|
/**
|
|
@@ -568,14 +575,14 @@ var DataModule = class {
|
|
|
568
575
|
this.logger.info({
|
|
569
576
|
id,
|
|
570
577
|
tableName,
|
|
571
|
-
Category: "
|
|
578
|
+
Category: "sp00ky-client::DataModule::rollbackCreate"
|
|
572
579
|
}, "Rolled back optimistic create");
|
|
573
580
|
} catch (err) {
|
|
574
581
|
this.logger.error({
|
|
575
582
|
err,
|
|
576
583
|
id,
|
|
577
584
|
tableName,
|
|
578
|
-
Category: "
|
|
585
|
+
Category: "sp00ky-client::DataModule::rollbackCreate"
|
|
579
586
|
}, "Failed to rollback create");
|
|
580
587
|
}
|
|
581
588
|
}
|
|
@@ -596,20 +603,20 @@ var DataModule = class {
|
|
|
596
603
|
table: tableName,
|
|
597
604
|
op: "UPDATE",
|
|
598
605
|
record: parsedRecord,
|
|
599
|
-
version: beforeRecord.
|
|
606
|
+
version: beforeRecord._00_rv || 1
|
|
600
607
|
}, true);
|
|
601
608
|
await this.replaceRecordInQueries(beforeRecord);
|
|
602
609
|
this.logger.info({
|
|
603
610
|
id,
|
|
604
611
|
tableName,
|
|
605
|
-
Category: "
|
|
612
|
+
Category: "sp00ky-client::DataModule::rollbackUpdate"
|
|
606
613
|
}, "Rolled back optimistic update");
|
|
607
614
|
} catch (err) {
|
|
608
615
|
this.logger.error({
|
|
609
616
|
err,
|
|
610
617
|
id,
|
|
611
618
|
tableName,
|
|
612
|
-
Category: "
|
|
619
|
+
Category: "sp00ky-client::DataModule::rollbackUpdate"
|
|
613
620
|
}, "Failed to rollback update");
|
|
614
621
|
}
|
|
615
622
|
}
|
|
@@ -654,7 +661,7 @@ var DataModule = class {
|
|
|
654
661
|
hash,
|
|
655
662
|
tableName,
|
|
656
663
|
recordCount: queryState.records.length,
|
|
657
|
-
Category: "
|
|
664
|
+
Category: "sp00ky-client::DataModule::query"
|
|
658
665
|
}, "Query registered");
|
|
659
666
|
return hash;
|
|
660
667
|
}
|
|
@@ -689,7 +696,7 @@ var DataModule = class {
|
|
|
689
696
|
} catch (err) {
|
|
690
697
|
this.logger.warn({
|
|
691
698
|
err,
|
|
692
|
-
Category: "
|
|
699
|
+
Category: "sp00ky-client::DataModule::createNewQuery"
|
|
693
700
|
}, "Failed to load initial cached records");
|
|
694
701
|
}
|
|
695
702
|
return {
|
|
@@ -712,7 +719,7 @@ var DataModule = class {
|
|
|
712
719
|
queryState.ttlTimer = setTimeout(() => {
|
|
713
720
|
this.logger.debug({
|
|
714
721
|
id: encodeRecordId(queryState.config.id),
|
|
715
|
-
Category: "
|
|
722
|
+
Category: "sp00ky-client::DataModule::startTTLHeartbeat"
|
|
716
723
|
}, "TTL heartbeat");
|
|
717
724
|
this.startTTLHeartbeat(queryState);
|
|
718
725
|
}, heartbeatTime);
|
|
@@ -744,7 +751,7 @@ function parseUpdateOptions(id, data, options) {
|
|
|
744
751
|
let pushEventOptions = {};
|
|
745
752
|
if (options?.debounced) pushEventOptions = { debounced: {
|
|
746
753
|
delay: options.debounced !== true ? options.debounced?.delay ?? 200 : 200,
|
|
747
|
-
key: (options.debounced !== true ? options.debounced?.key ?? id : id) === "recordId_x_fields" ? `${id}::${Object.keys(data).
|
|
754
|
+
key: (options.debounced !== true ? options.debounced?.key ?? id : id) === "recordId_x_fields" ? `${id}::${Object.keys(data).toSorted().join("#")}` : id
|
|
748
755
|
} };
|
|
749
756
|
return pushEventOptions;
|
|
750
757
|
}
|
|
@@ -781,7 +788,7 @@ var AbstractDatabaseService = class {
|
|
|
781
788
|
this.logger.debug({
|
|
782
789
|
query,
|
|
783
790
|
vars,
|
|
784
|
-
Category: "
|
|
791
|
+
Category: "sp00ky-client::Database::query"
|
|
785
792
|
}, "Executing query");
|
|
786
793
|
const result = await this.client.query(query, vars);
|
|
787
794
|
const duration = performance.now() - startTime;
|
|
@@ -796,7 +803,7 @@ var AbstractDatabaseService = class {
|
|
|
796
803
|
this.logger.trace({
|
|
797
804
|
query,
|
|
798
805
|
result,
|
|
799
|
-
Category: "
|
|
806
|
+
Category: "sp00ky-client::Database::query"
|
|
800
807
|
}, "Query executed successfully");
|
|
801
808
|
} catch (err) {
|
|
802
809
|
const duration = performance.now() - startTime;
|
|
@@ -812,7 +819,7 @@ var AbstractDatabaseService = class {
|
|
|
812
819
|
query,
|
|
813
820
|
vars,
|
|
814
821
|
err,
|
|
815
|
-
Category: "
|
|
822
|
+
Category: "sp00ky-client::Database::query"
|
|
816
823
|
}, "Query execution failed");
|
|
817
824
|
reject(err);
|
|
818
825
|
}
|
|
@@ -824,7 +831,7 @@ var AbstractDatabaseService = class {
|
|
|
824
831
|
return query.extract(raw);
|
|
825
832
|
}
|
|
826
833
|
async close() {
|
|
827
|
-
this.logger.info({ Category: "
|
|
834
|
+
this.logger.info({ Category: "sp00ky-client::Database::close" }, "Closing database connection");
|
|
828
835
|
await this.client.close();
|
|
829
836
|
}
|
|
830
837
|
};
|
|
@@ -1004,7 +1011,7 @@ var LocalDatabaseService = class extends AbstractDatabaseService {
|
|
|
1004
1011
|
type,
|
|
1005
1012
|
phase,
|
|
1006
1013
|
service: "surrealdb:local",
|
|
1007
|
-
Category: "
|
|
1014
|
+
Category: "sp00ky-client::LocalDatabaseService::diagnostics"
|
|
1008
1015
|
}, `Local SurrealDB diagnostics captured ${type}:${phase}`);
|
|
1009
1016
|
})
|
|
1010
1017
|
}), logger, events);
|
|
@@ -1018,30 +1025,30 @@ var LocalDatabaseService = class extends AbstractDatabaseService {
|
|
|
1018
1025
|
this.logger.info({
|
|
1019
1026
|
namespace,
|
|
1020
1027
|
database,
|
|
1021
|
-
Category: "
|
|
1028
|
+
Category: "sp00ky-client::LocalDatabaseService::connect"
|
|
1022
1029
|
}, "Connecting to local database");
|
|
1023
1030
|
try {
|
|
1024
|
-
const storeUrl = (this.getConfig().store ?? "memory") === "memory" ? "mem://" : "indxdb://
|
|
1031
|
+
const storeUrl = (this.getConfig().store ?? "memory") === "memory" ? "mem://" : "indxdb://sp00ky";
|
|
1025
1032
|
this.logger.debug({
|
|
1026
1033
|
storeUrl,
|
|
1027
|
-
Category: "
|
|
1034
|
+
Category: "sp00ky-client::LocalDatabaseService::connect"
|
|
1028
1035
|
}, "[LocalDatabaseService] Calling client.connect");
|
|
1029
1036
|
await this.client.connect(storeUrl, {});
|
|
1030
1037
|
this.logger.debug({
|
|
1031
1038
|
namespace,
|
|
1032
1039
|
database,
|
|
1033
|
-
Category: "
|
|
1040
|
+
Category: "sp00ky-client::LocalDatabaseService::connect"
|
|
1034
1041
|
}, "[LocalDatabaseService] client.connect returned. Calling client.use");
|
|
1035
1042
|
await this.client.use({
|
|
1036
1043
|
namespace,
|
|
1037
1044
|
database
|
|
1038
1045
|
});
|
|
1039
|
-
this.logger.debug({ Category: "
|
|
1040
|
-
this.logger.info({ Category: "
|
|
1046
|
+
this.logger.debug({ Category: "sp00ky-client::LocalDatabaseService::connect" }, "[LocalDatabaseService] client.use returned");
|
|
1047
|
+
this.logger.info({ Category: "sp00ky-client::LocalDatabaseService::connect" }, "Connected to local database");
|
|
1041
1048
|
} catch (err) {
|
|
1042
1049
|
this.logger.error({
|
|
1043
1050
|
err,
|
|
1044
|
-
Category: "
|
|
1051
|
+
Category: "sp00ky-client::LocalDatabaseService::connect"
|
|
1045
1052
|
}, "Failed to connect to local database");
|
|
1046
1053
|
throw err;
|
|
1047
1054
|
}
|
|
@@ -1062,7 +1069,7 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1062
1069
|
type,
|
|
1063
1070
|
phase,
|
|
1064
1071
|
service: "surrealdb:remote",
|
|
1065
|
-
Category: "
|
|
1072
|
+
Category: "sp00ky-client::RemoteDatabaseService::diagnostics"
|
|
1066
1073
|
}, `Remote SurrealDB diagnostics captured ${type}:${phase}`);
|
|
1067
1074
|
}) }), logger, events);
|
|
1068
1075
|
this.config = config;
|
|
@@ -1077,7 +1084,7 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1077
1084
|
endpoint,
|
|
1078
1085
|
namespace,
|
|
1079
1086
|
database,
|
|
1080
|
-
Category: "
|
|
1087
|
+
Category: "sp00ky-client::RemoteDatabaseService::connect"
|
|
1081
1088
|
}, "Connecting to remote database");
|
|
1082
1089
|
try {
|
|
1083
1090
|
await this.client.connect(endpoint);
|
|
@@ -1086,18 +1093,18 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1086
1093
|
database
|
|
1087
1094
|
});
|
|
1088
1095
|
if (token) {
|
|
1089
|
-
this.logger.debug({ Category: "
|
|
1096
|
+
this.logger.debug({ Category: "sp00ky-client::RemoteDatabaseService::connect" }, "Authenticating with token");
|
|
1090
1097
|
await this.client.authenticate(token);
|
|
1091
1098
|
}
|
|
1092
|
-
this.logger.info({ Category: "
|
|
1099
|
+
this.logger.info({ Category: "sp00ky-client::RemoteDatabaseService::connect" }, "Connected to remote database");
|
|
1093
1100
|
} catch (err) {
|
|
1094
1101
|
this.logger.error({
|
|
1095
1102
|
err,
|
|
1096
|
-
Category: "
|
|
1103
|
+
Category: "sp00ky-client::RemoteDatabaseService::connect"
|
|
1097
1104
|
}, "Failed to connect to remote database");
|
|
1098
1105
|
throw err;
|
|
1099
1106
|
}
|
|
1100
|
-
} else this.logger.warn({ Category: "
|
|
1107
|
+
} else this.logger.warn({ Category: "sp00ky-client::RemoteDatabaseService::connect" }, "No endpoint configured for remote database");
|
|
1101
1108
|
}
|
|
1102
1109
|
async signin(params) {
|
|
1103
1110
|
return this.client.signin(params);
|
|
@@ -1115,70 +1122,14 @@ var RemoteDatabaseService = class extends AbstractDatabaseService {
|
|
|
1115
1122
|
|
|
1116
1123
|
//#endregion
|
|
1117
1124
|
//#region src/services/logger/index.ts
|
|
1118
|
-
|
|
1119
|
-
function mapLevelToSeverityNumber(level) {
|
|
1120
|
-
switch (level) {
|
|
1121
|
-
case "trace": return 1;
|
|
1122
|
-
case "debug": return 5;
|
|
1123
|
-
case "info": return 9;
|
|
1124
|
-
case "warn": return 13;
|
|
1125
|
-
case "error": return 17;
|
|
1126
|
-
case "fatal": return 21;
|
|
1127
|
-
default: return 9;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
function createLogger(level = "info", otelEndpoint) {
|
|
1125
|
+
function createLogger(level = "info", transmit) {
|
|
1131
1126
|
const browserConfig = {
|
|
1132
1127
|
asObject: true,
|
|
1133
1128
|
write: (o) => {
|
|
1134
1129
|
console.log(JSON.stringify(o));
|
|
1135
1130
|
}
|
|
1136
1131
|
};
|
|
1137
|
-
if (
|
|
1138
|
-
const loggerProvider = new LoggerProvider({
|
|
1139
|
-
resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "spooky-client" }),
|
|
1140
|
-
processors: [new BatchLogRecordProcessor(new OTLPLogExporter({ url: otelEndpoint }))]
|
|
1141
|
-
});
|
|
1142
|
-
const otelLogger = {};
|
|
1143
|
-
const getOtelLogger = (category) => {
|
|
1144
|
-
if (!otelLogger[category]) otelLogger[category] = loggerProvider.getLogger(category);
|
|
1145
|
-
return otelLogger[category];
|
|
1146
|
-
};
|
|
1147
|
-
browserConfig.transmit = {
|
|
1148
|
-
level,
|
|
1149
|
-
send: (levelLabel, logEvent) => {
|
|
1150
|
-
try {
|
|
1151
|
-
const messages = [...logEvent.messages];
|
|
1152
|
-
const severityNumber = mapLevelToSeverityNumber(levelLabel);
|
|
1153
|
-
let body = "";
|
|
1154
|
-
const msg = messages.pop();
|
|
1155
|
-
if (typeof msg === "string") body = msg;
|
|
1156
|
-
else if (msg) body = JSON.stringify(msg);
|
|
1157
|
-
let category = "spooky-client::unknown";
|
|
1158
|
-
const attributes = {};
|
|
1159
|
-
for (const msg of messages) if (typeof msg === "object") {
|
|
1160
|
-
if (msg.Category) {
|
|
1161
|
-
category = msg.Category;
|
|
1162
|
-
delete msg.Category;
|
|
1163
|
-
}
|
|
1164
|
-
Object.assign(attributes, msg);
|
|
1165
|
-
}
|
|
1166
|
-
getOtelLogger(category).emit({
|
|
1167
|
-
severityNumber,
|
|
1168
|
-
severityText: levelLabel.toUpperCase(),
|
|
1169
|
-
body,
|
|
1170
|
-
attributes: {
|
|
1171
|
-
...logEvent.bindings[0],
|
|
1172
|
-
...attributes
|
|
1173
|
-
},
|
|
1174
|
-
timestamp: new Date(logEvent.ts)
|
|
1175
|
-
});
|
|
1176
|
-
} catch (e) {
|
|
1177
|
-
console.warn("Failed to transmit log to OTEL endpoint", e);
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1132
|
+
if (transmit) browserConfig.transmit = transmit;
|
|
1182
1133
|
return pino({
|
|
1183
1134
|
level,
|
|
1184
1135
|
browser: browserConfig
|
|
@@ -1203,25 +1154,25 @@ var LocalMigrator = class {
|
|
|
1203
1154
|
const hash = await sha1(schemaSurql);
|
|
1204
1155
|
const { database } = this.localDb.getConfig();
|
|
1205
1156
|
if (await this.isSchemaUpToDate(hash)) {
|
|
1206
|
-
this.logger.info({ Category: "
|
|
1157
|
+
this.logger.info({ Category: "sp00ky-client::LocalMigrator::provision" }, "[Provisioning] Schema is up to date, skipping migration");
|
|
1207
1158
|
return;
|
|
1208
1159
|
}
|
|
1209
1160
|
await this.recreateDatabase(database);
|
|
1210
|
-
const fullSchema = schemaSurql + "\n\n DEFINE TABLE IF NOT EXISTS
|
|
1161
|
+
const fullSchema = schemaSurql + "\n\n DEFINE TABLE IF NOT EXISTS _00_stream_processor_state SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;\n DEFINE TABLE IF NOT EXISTS _00_query SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;\n DEFINE TABLE IF NOT EXISTS _00_schema SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;\n DEFINE TABLE IF NOT EXISTS _00_pending_mutations SCHEMALESS PERMISSIONS FOR select, create, update, delete WHERE true;\n ";
|
|
1211
1162
|
const statements = this.splitStatements(fullSchema);
|
|
1212
1163
|
for (let i = 0; i < statements.length; i++) {
|
|
1213
1164
|
const statement = statements[i];
|
|
1214
1165
|
const cleanStatement = statement.replace(/--.*/g, "").trim();
|
|
1215
1166
|
if (cleanStatement.toUpperCase().startsWith("DEFINE INDEX")) {
|
|
1216
|
-
this.logger.warn({ Category: "
|
|
1167
|
+
this.logger.warn({ Category: "sp00ky-client::LocalMigrator::provision" }, `[Provisioning] Skipping index definition (WASM hang avoidance): ${cleanStatement.substring(0, 50)}...`);
|
|
1217
1168
|
continue;
|
|
1218
1169
|
}
|
|
1219
1170
|
try {
|
|
1220
|
-
this.logger.info({ Category: "
|
|
1171
|
+
this.logger.info({ Category: "sp00ky-client::LocalMigrator::provision" }, `[Provisioning] (${i + 1}/${statements.length}) Executing: ${statement.substring(0, 50)}...`);
|
|
1221
1172
|
await this.localDb.query(statement);
|
|
1222
|
-
this.logger.info({ Category: "
|
|
1173
|
+
this.logger.info({ Category: "sp00ky-client::LocalMigrator::provision" }, `[Provisioning] (${i + 1}/${statements.length}) Done`);
|
|
1223
1174
|
} catch (e) {
|
|
1224
|
-
this.logger.error({ Category: "
|
|
1175
|
+
this.logger.error({ Category: "sp00ky-client::LocalMigrator::provision" }, `[Provisioning] (${i + 1}/${statements.length}) Error executing statement: ${statement}`);
|
|
1225
1176
|
throw e;
|
|
1226
1177
|
}
|
|
1227
1178
|
}
|
|
@@ -1229,22 +1180,22 @@ var LocalMigrator = class {
|
|
|
1229
1180
|
}
|
|
1230
1181
|
async isSchemaUpToDate(hash) {
|
|
1231
1182
|
try {
|
|
1232
|
-
const [lastSchemaRecord] = await this.localDb.query(`SELECT hash, created_at FROM ONLY
|
|
1183
|
+
const [lastSchemaRecord] = await this.localDb.query(`SELECT hash, created_at FROM ONLY _00_schema ORDER BY created_at DESC LIMIT 1;`);
|
|
1233
1184
|
return lastSchemaRecord?.hash === hash;
|
|
1234
|
-
} catch (
|
|
1185
|
+
} catch (_error) {
|
|
1235
1186
|
return false;
|
|
1236
1187
|
}
|
|
1237
1188
|
}
|
|
1238
1189
|
async recreateDatabase(database) {
|
|
1239
1190
|
try {
|
|
1240
|
-
await this.localDb.query(`DEFINE DATABASE
|
|
1241
|
-
} catch (
|
|
1191
|
+
await this.localDb.query(`DEFINE DATABASE _00_temp;`);
|
|
1192
|
+
} catch (_e) {}
|
|
1242
1193
|
try {
|
|
1243
1194
|
await this.localDb.query(`
|
|
1244
|
-
USE DB
|
|
1195
|
+
USE DB _00_temp;
|
|
1245
1196
|
REMOVE DATABASE ${database};
|
|
1246
1197
|
`);
|
|
1247
|
-
} catch (
|
|
1198
|
+
} catch (_e) {}
|
|
1248
1199
|
await this.localDb.query(`
|
|
1249
1200
|
DEFINE DATABASE ${database};
|
|
1250
1201
|
USE DB ${database};
|
|
@@ -1302,7 +1253,7 @@ var LocalMigrator = class {
|
|
|
1302
1253
|
return statements;
|
|
1303
1254
|
}
|
|
1304
1255
|
async createHashRecord(hash) {
|
|
1305
|
-
await this.localDb.query(`UPSERT
|
|
1256
|
+
await this.localDb.query(`UPSERT _00_schema SET hash = $hash, created_at = time::now() WHERE hash = $hash;`, { hash });
|
|
1306
1257
|
}
|
|
1307
1258
|
};
|
|
1308
1259
|
|
|
@@ -1394,7 +1345,7 @@ var UpQueue = class {
|
|
|
1394
1345
|
this.logger.error({
|
|
1395
1346
|
error,
|
|
1396
1347
|
event,
|
|
1397
|
-
Category: "
|
|
1348
|
+
Category: "sp00ky-client::UpQueue::next"
|
|
1398
1349
|
}, "Network error processing mutation, re-queuing");
|
|
1399
1350
|
this.queue.unshift(event);
|
|
1400
1351
|
throw error;
|
|
@@ -1402,7 +1353,7 @@ var UpQueue = class {
|
|
|
1402
1353
|
this.logger.error({
|
|
1403
1354
|
error,
|
|
1404
1355
|
event,
|
|
1405
|
-
Category: "
|
|
1356
|
+
Category: "sp00ky-client::UpQueue::next"
|
|
1406
1357
|
}, "Application error processing mutation, rolling back");
|
|
1407
1358
|
try {
|
|
1408
1359
|
await this.removeEventFromDatabase(event.mutation_id);
|
|
@@ -1410,7 +1361,7 @@ var UpQueue = class {
|
|
|
1410
1361
|
this.logger.error({
|
|
1411
1362
|
error: removeError,
|
|
1412
1363
|
event,
|
|
1413
|
-
Category: "
|
|
1364
|
+
Category: "sp00ky-client::UpQueue::next"
|
|
1414
1365
|
}, "Failed to remove rolled-back mutation from database");
|
|
1415
1366
|
}
|
|
1416
1367
|
if (onRollback) try {
|
|
@@ -1419,7 +1370,7 @@ var UpQueue = class {
|
|
|
1419
1370
|
this.logger.error({
|
|
1420
1371
|
error: rollbackError,
|
|
1421
1372
|
event,
|
|
1422
|
-
Category: "
|
|
1373
|
+
Category: "sp00ky-client::UpQueue::next"
|
|
1423
1374
|
}, "Rollback handler failed");
|
|
1424
1375
|
}
|
|
1425
1376
|
this._events.addEvent({
|
|
@@ -1434,7 +1385,7 @@ var UpQueue = class {
|
|
|
1434
1385
|
this.logger.error({
|
|
1435
1386
|
error,
|
|
1436
1387
|
event,
|
|
1437
|
-
Category: "
|
|
1388
|
+
Category: "sp00ky-client::UpQueue::next"
|
|
1438
1389
|
}, "Failed to remove mutation from database after successful processing");
|
|
1439
1390
|
}
|
|
1440
1391
|
this._events.addEvent({
|
|
@@ -1448,7 +1399,7 @@ var UpQueue = class {
|
|
|
1448
1399
|
}
|
|
1449
1400
|
async loadFromDatabase() {
|
|
1450
1401
|
try {
|
|
1451
|
-
const [records] = await this.local.query(`SELECT * FROM
|
|
1402
|
+
const [records] = await this.local.query(`SELECT * FROM _00_pending_mutations ORDER BY created_at ASC`);
|
|
1452
1403
|
this.queue = records.map((r) => {
|
|
1453
1404
|
switch (r.mutationType) {
|
|
1454
1405
|
case "create": return {
|
|
@@ -1474,7 +1425,7 @@ var UpQueue = class {
|
|
|
1474
1425
|
this.logger.warn({
|
|
1475
1426
|
mutationType: r.mutationType,
|
|
1476
1427
|
record: r,
|
|
1477
|
-
Category: "
|
|
1428
|
+
Category: "sp00ky-client::UpQueue::loadFromDatabase"
|
|
1478
1429
|
}, "Unknown mutation type");
|
|
1479
1430
|
return null;
|
|
1480
1431
|
}
|
|
@@ -1482,7 +1433,7 @@ var UpQueue = class {
|
|
|
1482
1433
|
} catch (error) {
|
|
1483
1434
|
this.logger.error({
|
|
1484
1435
|
error,
|
|
1485
|
-
Category: "
|
|
1436
|
+
Category: "sp00ky-client::UpQueue::loadFromDatabase"
|
|
1486
1437
|
}, "Failed to load pending mutations from database");
|
|
1487
1438
|
}
|
|
1488
1439
|
}
|
|
@@ -1523,7 +1474,7 @@ var DownQueue = class {
|
|
|
1523
1474
|
this.logger.error({
|
|
1524
1475
|
error,
|
|
1525
1476
|
event,
|
|
1526
|
-
Category: "
|
|
1477
|
+
Category: "sp00ky-client::DownQueue::next"
|
|
1527
1478
|
}, "Failed to process query");
|
|
1528
1479
|
this.queue.unshift(event);
|
|
1529
1480
|
throw error;
|
|
@@ -1538,8 +1489,8 @@ var ArraySyncer = class {
|
|
|
1538
1489
|
remoteArray;
|
|
1539
1490
|
needsSort = false;
|
|
1540
1491
|
constructor(localArray, remoteArray) {
|
|
1541
|
-
this.remoteArray = remoteArray.
|
|
1542
|
-
this.localArray = localArray.
|
|
1492
|
+
this.remoteArray = remoteArray.toSorted((a, b) => a[0].localeCompare(b[0]));
|
|
1493
|
+
this.localArray = localArray.toSorted((a, b) => a[0].localeCompare(b[0]));
|
|
1543
1494
|
}
|
|
1544
1495
|
/**
|
|
1545
1496
|
* Inserts an item into the local array
|
|
@@ -1575,7 +1526,6 @@ var ArraySyncer = class {
|
|
|
1575
1526
|
this.localArray.sort((a, b) => a[0].localeCompare(b[0]));
|
|
1576
1527
|
this.needsSort = false;
|
|
1577
1528
|
}
|
|
1578
|
-
console.log("xxxx555", this.localArray, this.remoteArray);
|
|
1579
1529
|
return diffRecordVersionArray(this.localArray, this.remoteArray);
|
|
1580
1530
|
}
|
|
1581
1531
|
};
|
|
@@ -1606,14 +1556,12 @@ function diffRecordVersionArray(local, remote) {
|
|
|
1606
1556
|
};
|
|
1607
1557
|
}
|
|
1608
1558
|
function createDiffFromDbOp(op, recordId, version, versions) {
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
};
|
|
1616
|
-
}
|
|
1559
|
+
const old = versions?.find((record) => record[0] === encodeRecordId(recordId));
|
|
1560
|
+
if (old && old[1] >= version) return {
|
|
1561
|
+
added: [],
|
|
1562
|
+
updated: [],
|
|
1563
|
+
removed: []
|
|
1564
|
+
};
|
|
1617
1565
|
if (op === "CREATE") return {
|
|
1618
1566
|
added: [{
|
|
1619
1567
|
id: recordId,
|
|
@@ -1643,7 +1591,7 @@ function createDiffFromDbOp(op, recordId, version, versions) {
|
|
|
1643
1591
|
* SyncEngine handles the core sync operations: fetching remote records,
|
|
1644
1592
|
* caching them locally, and ingesting into DBSP.
|
|
1645
1593
|
*
|
|
1646
|
-
* This is extracted from
|
|
1594
|
+
* This is extracted from Sp00kySync to separate "how to sync" from "when to sync".
|
|
1647
1595
|
*/
|
|
1648
1596
|
var SyncEngine = class {
|
|
1649
1597
|
logger;
|
|
@@ -1652,7 +1600,7 @@ var SyncEngine = class {
|
|
|
1652
1600
|
this.remote = remote;
|
|
1653
1601
|
this.cache = cache;
|
|
1654
1602
|
this.schema = schema;
|
|
1655
|
-
this.logger = logger.child({ service: "
|
|
1603
|
+
this.logger = logger.child({ service: "Sp00kySync:SyncEngine" });
|
|
1656
1604
|
}
|
|
1657
1605
|
/**
|
|
1658
1606
|
* Sync missing/updated/removed records between local and remote.
|
|
@@ -1665,33 +1613,36 @@ var SyncEngine = class {
|
|
|
1665
1613
|
added,
|
|
1666
1614
|
updated,
|
|
1667
1615
|
removed,
|
|
1668
|
-
Category: "
|
|
1616
|
+
Category: "sp00ky-client::SyncEngine::syncRecords"
|
|
1669
1617
|
}, "SyncEngine.syncRecords diff");
|
|
1670
1618
|
if (removed.length > 0) await this.handleRemovedRecords(removed);
|
|
1671
|
-
const
|
|
1619
|
+
const toFetch = [...added, ...updated];
|
|
1620
|
+
const idsToFetch = toFetch.map((x) => x.id);
|
|
1672
1621
|
if (idsToFetch.length === 0) return;
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1622
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
1623
|
+
for (const item of toFetch) versionMap.set(encodeRecordId(item.id), item.version);
|
|
1624
|
+
const [remoteResults] = await this.remote.query("SELECT * FROM $idsToFetch", { idsToFetch });
|
|
1675
1625
|
const cacheBatch = [];
|
|
1676
|
-
for (const
|
|
1626
|
+
for (const record of remoteResults) {
|
|
1677
1627
|
if (!record?.id) {
|
|
1678
1628
|
this.logger.warn({
|
|
1679
1629
|
record,
|
|
1680
1630
|
idsToFetch,
|
|
1681
|
-
Category: "
|
|
1682
|
-
}, "Remote record has no id. Skipping record");
|
|
1631
|
+
Category: "sp00ky-client::SyncEngine::syncRecords"
|
|
1632
|
+
}, "Remote record has no id (possibly deleted). Skipping record");
|
|
1683
1633
|
continue;
|
|
1684
1634
|
}
|
|
1685
1635
|
const fullId = encodeRecordId(record.id);
|
|
1686
1636
|
const table = record.id.table.toString();
|
|
1687
1637
|
const isAdded = added.some((item) => encodeRecordId(item.id) === fullId);
|
|
1638
|
+
const version = versionMap.get(fullId) ?? 0;
|
|
1688
1639
|
const localVersion = this.cache.lookup(fullId);
|
|
1689
|
-
if (localVersion &&
|
|
1640
|
+
if (localVersion && version <= localVersion) {
|
|
1690
1641
|
this.logger.info({
|
|
1691
1642
|
recordId: fullId,
|
|
1692
|
-
version
|
|
1643
|
+
version,
|
|
1693
1644
|
localVersion,
|
|
1694
|
-
Category: "
|
|
1645
|
+
Category: "sp00ky-client::SyncEngine::syncRecords"
|
|
1695
1646
|
}, "Local version is higher than remote version. Skipping record");
|
|
1696
1647
|
continue;
|
|
1697
1648
|
}
|
|
@@ -1701,7 +1652,7 @@ var SyncEngine = class {
|
|
|
1701
1652
|
table,
|
|
1702
1653
|
op: isAdded ? "CREATE" : "UPDATE",
|
|
1703
1654
|
record: cleanedRecord,
|
|
1704
|
-
version
|
|
1655
|
+
version
|
|
1705
1656
|
});
|
|
1706
1657
|
}
|
|
1707
1658
|
if (cacheBatch.length > 0) await this.cache.saveBatch(cacheBatch);
|
|
@@ -1713,21 +1664,21 @@ var SyncEngine = class {
|
|
|
1713
1664
|
async handleRemovedRecords(removed) {
|
|
1714
1665
|
this.logger.debug({
|
|
1715
1666
|
removed: removed.map((r) => r.toString()),
|
|
1716
|
-
Category: "
|
|
1667
|
+
Category: "sp00ky-client::SyncEngine::handleRemovedRecords"
|
|
1717
1668
|
}, "Checking removed records");
|
|
1718
1669
|
let existingRemoteIds = /* @__PURE__ */ new Set();
|
|
1719
1670
|
try {
|
|
1720
1671
|
const [existingRemote] = await this.remote.query("SELECT id FROM $ids", { ids: removed });
|
|
1721
1672
|
existingRemoteIds = new Set(existingRemote.map((r) => encodeRecordId(r.id)));
|
|
1722
1673
|
} catch {
|
|
1723
|
-
this.logger.debug({ Category: "
|
|
1674
|
+
this.logger.debug({ Category: "sp00ky-client::SyncEngine::handleRemovedRecords" }, "Remote existence check failed, proceeding with deletion");
|
|
1724
1675
|
}
|
|
1725
1676
|
for (const recordId of removed) {
|
|
1726
1677
|
const recordIdStr = encodeRecordId(recordId);
|
|
1727
1678
|
if (!existingRemoteIds.has(recordIdStr)) {
|
|
1728
1679
|
this.logger.debug({
|
|
1729
1680
|
recordId: recordIdStr,
|
|
1730
|
-
Category: "
|
|
1681
|
+
Category: "sp00ky-client::SyncEngine::handleRemovedRecords"
|
|
1731
1682
|
}, "Deleting confirmed removed record");
|
|
1732
1683
|
await this.cache.delete(recordId.table.name, recordIdStr);
|
|
1733
1684
|
}
|
|
@@ -1806,12 +1757,12 @@ var SyncScheduler = class {
|
|
|
1806
1757
|
//#endregion
|
|
1807
1758
|
//#region src/modules/sync/sync.ts
|
|
1808
1759
|
/**
|
|
1809
|
-
* The main synchronization engine for
|
|
1760
|
+
* The main synchronization engine for Sp00ky.
|
|
1810
1761
|
* Handles the bidirectional synchronization between the local database and the remote backend.
|
|
1811
1762
|
* Uses a queue-based architecture with 'up' (local to remote) and 'down' (remote to local) queues.
|
|
1812
1763
|
* @template S The schema structure type.
|
|
1813
1764
|
*/
|
|
1814
|
-
var
|
|
1765
|
+
var Sp00kySync = class {
|
|
1815
1766
|
clientId = "";
|
|
1816
1767
|
upQueue;
|
|
1817
1768
|
downQueue;
|
|
@@ -1840,7 +1791,7 @@ var SpookySync = class {
|
|
|
1840
1791
|
this.cache = cache;
|
|
1841
1792
|
this.dataModule = dataModule;
|
|
1842
1793
|
this.schema = schema;
|
|
1843
|
-
this.logger = logger.child({ service: "
|
|
1794
|
+
this.logger = logger.child({ service: "Sp00kySync" });
|
|
1844
1795
|
this.upQueue = new UpQueue(this.local, this.logger);
|
|
1845
1796
|
this.downQueue = new DownQueue(this.local, this.logger);
|
|
1846
1797
|
this.syncEngine = new SyncEngine(this.remote, this.cache, this.schema, this.logger);
|
|
@@ -1853,7 +1804,7 @@ var SpookySync = class {
|
|
|
1853
1804
|
* @throws Error if already initialized.
|
|
1854
1805
|
*/
|
|
1855
1806
|
async init(clientId) {
|
|
1856
|
-
if (this.isInit) throw new Error("
|
|
1807
|
+
if (this.isInit) throw new Error("Sp00kySync is already initialized");
|
|
1857
1808
|
this.clientId = clientId;
|
|
1858
1809
|
this.isInit = true;
|
|
1859
1810
|
await this.scheduler.init();
|
|
@@ -1865,29 +1816,37 @@ var SpookySync = class {
|
|
|
1865
1816
|
async startRefLiveQueries() {
|
|
1866
1817
|
this.logger.debug({
|
|
1867
1818
|
clientId: this.clientId,
|
|
1868
|
-
Category: "
|
|
1819
|
+
Category: "sp00ky-client::Sp00kySync::startRefLiveQueries"
|
|
1869
1820
|
}, "Starting ref live queries");
|
|
1870
|
-
const [queryUuid] = await this.remote.query("LIVE SELECT * FROM
|
|
1821
|
+
const [queryUuid] = await this.remote.query("LIVE SELECT * FROM _00_list_ref");
|
|
1871
1822
|
(await this.remote.getClient().liveOf(queryUuid)).subscribe((message) => {
|
|
1872
1823
|
this.logger.debug({
|
|
1873
1824
|
message,
|
|
1874
|
-
Category: "
|
|
1825
|
+
Category: "sp00ky-client::Sp00kySync::startRefLiveQueries"
|
|
1875
1826
|
}, "Live update received");
|
|
1876
1827
|
if (message.action === "KILLED") return;
|
|
1877
1828
|
this.handleRemoteListRefChange(message.action, message.value.in, message.value.out, message.value.version).catch((err) => {
|
|
1878
1829
|
this.logger.error({
|
|
1879
1830
|
err,
|
|
1880
|
-
Category: "
|
|
1831
|
+
Category: "sp00ky-client::Sp00kySync::startRefLiveQueries"
|
|
1881
1832
|
}, "Error handling remote list ref change");
|
|
1882
1833
|
});
|
|
1883
1834
|
});
|
|
1884
1835
|
}
|
|
1885
1836
|
async handleRemoteListRefChange(action, queryId, recordId, version) {
|
|
1837
|
+
if (action === "DELETE") {
|
|
1838
|
+
this.logger.debug({
|
|
1839
|
+
queryId: queryId.toString(),
|
|
1840
|
+
recordId: recordId.toString(),
|
|
1841
|
+
Category: "sp00ky-client::Sp00kySync::handleRemoteListRefChange"
|
|
1842
|
+
}, "Ignoring DELETE on list_ref — should not happen");
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1886
1845
|
const existing = this.dataModule.getQueryById(queryId);
|
|
1887
1846
|
if (!existing) {
|
|
1888
1847
|
this.logger.warn({
|
|
1889
1848
|
queryId: queryId.toString(),
|
|
1890
|
-
Category: "
|
|
1849
|
+
Category: "sp00ky-client::Sp00kySync::handleRemoteListRefChange"
|
|
1891
1850
|
}, "Received remote update for unknown local query");
|
|
1892
1851
|
return;
|
|
1893
1852
|
}
|
|
@@ -1898,7 +1857,7 @@ var SpookySync = class {
|
|
|
1898
1857
|
recordId,
|
|
1899
1858
|
version,
|
|
1900
1859
|
localArray,
|
|
1901
|
-
Category: "
|
|
1860
|
+
Category: "sp00ky-client::Sp00kySync::handleRemoteListRefChange"
|
|
1902
1861
|
}, "Live update is being processed");
|
|
1903
1862
|
const diff = createDiffFromDbOp(action, recordId, version, localArray);
|
|
1904
1863
|
await this.syncEngine.syncRecords(diff);
|
|
@@ -1913,11 +1872,10 @@ var SpookySync = class {
|
|
|
1913
1872
|
async processUpEvent(event) {
|
|
1914
1873
|
this.logger.debug({
|
|
1915
1874
|
event,
|
|
1916
|
-
Category: "
|
|
1875
|
+
Category: "sp00ky-client::Sp00kySync::processUpEvent"
|
|
1917
1876
|
}, "Processing up event");
|
|
1918
|
-
console.log("xx1", event);
|
|
1919
1877
|
switch (event.type) {
|
|
1920
|
-
case "create":
|
|
1878
|
+
case "create": {
|
|
1921
1879
|
const dataKeys = Object.keys(event.data).map((key) => ({
|
|
1922
1880
|
key,
|
|
1923
1881
|
variable: `data_${key}`
|
|
@@ -1929,6 +1887,7 @@ var SpookySync = class {
|
|
|
1929
1887
|
...prefixedParams
|
|
1930
1888
|
});
|
|
1931
1889
|
break;
|
|
1890
|
+
}
|
|
1932
1891
|
case "update":
|
|
1933
1892
|
await this.remote.query(`UPDATE $id MERGE $data`, {
|
|
1934
1893
|
id: event.record_id,
|
|
@@ -1941,7 +1900,7 @@ var SpookySync = class {
|
|
|
1941
1900
|
default:
|
|
1942
1901
|
this.logger.error({
|
|
1943
1902
|
event,
|
|
1944
|
-
Category: "
|
|
1903
|
+
Category: "sp00ky-client::Sp00kySync::processUpEvent"
|
|
1945
1904
|
}, "processUpEvent unknown event type");
|
|
1946
1905
|
return;
|
|
1947
1906
|
}
|
|
@@ -1954,7 +1913,7 @@ var SpookySync = class {
|
|
|
1954
1913
|
recordId,
|
|
1955
1914
|
tableName,
|
|
1956
1915
|
error: error.message,
|
|
1957
|
-
Category: "
|
|
1916
|
+
Category: "sp00ky-client::Sp00kySync::handleRollback"
|
|
1958
1917
|
}, "Rolling back failed mutation");
|
|
1959
1918
|
switch (event.type) {
|
|
1960
1919
|
case "create":
|
|
@@ -1964,13 +1923,13 @@ var SpookySync = class {
|
|
|
1964
1923
|
if (event.beforeRecord) await this.dataModule.rollbackUpdate(event.record_id, tableName, event.beforeRecord);
|
|
1965
1924
|
else this.logger.warn({
|
|
1966
1925
|
recordId,
|
|
1967
|
-
Category: "
|
|
1926
|
+
Category: "sp00ky-client::Sp00kySync::handleRollback"
|
|
1968
1927
|
}, "Cannot rollback update: no beforeRecord available. Down-sync will reconcile.");
|
|
1969
1928
|
break;
|
|
1970
1929
|
case "delete":
|
|
1971
1930
|
this.logger.warn({
|
|
1972
1931
|
recordId,
|
|
1973
|
-
Category: "
|
|
1932
|
+
Category: "sp00ky-client::Sp00kySync::handleRollback"
|
|
1974
1933
|
}, "Delete rollback not implemented. Down-sync will reconcile.");
|
|
1975
1934
|
break;
|
|
1976
1935
|
}
|
|
@@ -1983,7 +1942,7 @@ var SpookySync = class {
|
|
|
1983
1942
|
async processDownEvent(event) {
|
|
1984
1943
|
this.logger.debug({
|
|
1985
1944
|
event,
|
|
1986
|
-
Category: "
|
|
1945
|
+
Category: "sp00ky-client::Sp00kySync::processDownEvent"
|
|
1987
1946
|
}, "Processing down event");
|
|
1988
1947
|
switch (event.type) {
|
|
1989
1948
|
case "register": return this.registerQuery(event.payload.hash);
|
|
@@ -2002,7 +1961,7 @@ var SpookySync = class {
|
|
|
2002
1961
|
if (!queryState) {
|
|
2003
1962
|
this.logger.warn({
|
|
2004
1963
|
hash,
|
|
2005
|
-
Category: "
|
|
1964
|
+
Category: "sp00ky-client::Sp00kySync::syncQuery"
|
|
2006
1965
|
}, "Query not found");
|
|
2007
1966
|
return;
|
|
2008
1967
|
}
|
|
@@ -2021,7 +1980,7 @@ var SpookySync = class {
|
|
|
2021
1980
|
try {
|
|
2022
1981
|
this.logger.debug({
|
|
2023
1982
|
queryHash,
|
|
2024
|
-
Category: "
|
|
1983
|
+
Category: "sp00ky-client::Sp00kySync::registerQuery"
|
|
2025
1984
|
}, "Register Query state");
|
|
2026
1985
|
await this.createRemoteQuery(queryHash);
|
|
2027
1986
|
await this.syncQuery(queryHash);
|
|
@@ -2029,7 +1988,7 @@ var SpookySync = class {
|
|
|
2029
1988
|
} catch (e) {
|
|
2030
1989
|
this.logger.error({
|
|
2031
1990
|
err: e,
|
|
2032
|
-
Category: "
|
|
1991
|
+
Category: "sp00ky-client::Sp00kySync::registerQuery"
|
|
2033
1992
|
}, "registerQuery error");
|
|
2034
1993
|
throw e;
|
|
2035
1994
|
}
|
|
@@ -2039,7 +1998,7 @@ var SpookySync = class {
|
|
|
2039
1998
|
if (!queryState) {
|
|
2040
1999
|
this.logger.warn({
|
|
2041
2000
|
queryHash,
|
|
2042
|
-
Category: "
|
|
2001
|
+
Category: "sp00ky-client::Sp00kySync::createRemoteQuery"
|
|
2043
2002
|
}, "Query to register not found");
|
|
2044
2003
|
throw new Error("Query to register not found");
|
|
2045
2004
|
}
|
|
@@ -2050,17 +2009,17 @@ var SpookySync = class {
|
|
|
2050
2009
|
params: queryState.config.params,
|
|
2051
2010
|
ttl: queryState.config.ttl
|
|
2052
2011
|
} });
|
|
2053
|
-
const [items] = await this.remote.query(surql.selectByFieldsAnd("
|
|
2012
|
+
const [items] = await this.remote.query(surql.selectByFieldsAnd("_00_list_ref", ["in"], ["out", "version"]), { in: queryState.config.id });
|
|
2054
2013
|
this.logger.trace({
|
|
2055
2014
|
queryId: encodeRecordId(queryState.config.id),
|
|
2056
2015
|
items,
|
|
2057
|
-
Category: "
|
|
2016
|
+
Category: "sp00ky-client::Sp00kySync::createRemoteQuery"
|
|
2058
2017
|
}, "Got query record version array from remote");
|
|
2059
2018
|
const array = items.map((item) => [encodeRecordId(item.out), item.version]);
|
|
2060
2019
|
this.logger.debug({
|
|
2061
2020
|
queryId: encodeRecordId(queryState.config.id),
|
|
2062
2021
|
array,
|
|
2063
|
-
Category: "
|
|
2022
|
+
Category: "sp00ky-client::Sp00kySync::createRemoteQuery"
|
|
2064
2023
|
}, "createdRemoteQuery");
|
|
2065
2024
|
if (array) await this.dataModule.updateQueryRemoteArray(queryHash, array);
|
|
2066
2025
|
}
|
|
@@ -2069,7 +2028,7 @@ var SpookySync = class {
|
|
|
2069
2028
|
if (!queryState) {
|
|
2070
2029
|
this.logger.warn({
|
|
2071
2030
|
queryHash,
|
|
2072
|
-
Category: "
|
|
2031
|
+
Category: "sp00ky-client::Sp00kySync::heartbeatQuery"
|
|
2073
2032
|
}, "Query to register not found");
|
|
2074
2033
|
throw new Error("Query to register not found");
|
|
2075
2034
|
}
|
|
@@ -2080,7 +2039,7 @@ var SpookySync = class {
|
|
|
2080
2039
|
if (!queryState) {
|
|
2081
2040
|
this.logger.warn({
|
|
2082
2041
|
queryHash,
|
|
2083
|
-
Category: "
|
|
2042
|
+
Category: "sp00ky-client::Sp00kySync::cleanupQuery"
|
|
2084
2043
|
}, "Query to register not found");
|
|
2085
2044
|
throw new Error("Query to register not found");
|
|
2086
2045
|
}
|
|
@@ -2112,7 +2071,7 @@ var DevToolsService = class {
|
|
|
2112
2071
|
this.authService.eventSystem.subscribe(AuthEventTypes.AuthStateChanged, () => {
|
|
2113
2072
|
this.notifyDevTools();
|
|
2114
2073
|
});
|
|
2115
|
-
this.logger.debug({ Category: "
|
|
2074
|
+
this.logger.debug({ Category: "sp00ky-client::DevToolsService::init" }, "Service initialized");
|
|
2116
2075
|
}
|
|
2117
2076
|
getActiveQueries() {
|
|
2118
2077
|
const result = /* @__PURE__ */ new Map();
|
|
@@ -2138,7 +2097,7 @@ var DevToolsService = class {
|
|
|
2138
2097
|
onQueryInitialized(payload) {
|
|
2139
2098
|
this.logger.debug({
|
|
2140
2099
|
payload,
|
|
2141
|
-
Category: "
|
|
2100
|
+
Category: "sp00ky-client::DevToolsService::onQueryInitialized"
|
|
2142
2101
|
}, "QueryInitialized");
|
|
2143
2102
|
const queryHash = this.hashString(payload.queryId.toString());
|
|
2144
2103
|
this.addEvent("QUERY_REQUEST_INIT", {
|
|
@@ -2151,7 +2110,7 @@ var DevToolsService = class {
|
|
|
2151
2110
|
onQueryUpdated(payload) {
|
|
2152
2111
|
this.logger.debug({
|
|
2153
2112
|
id: payload.queryId?.toString(),
|
|
2154
|
-
Category: "
|
|
2113
|
+
Category: "sp00ky-client::DevToolsService::onQueryUpdated"
|
|
2155
2114
|
}, "QueryUpdated");
|
|
2156
2115
|
const queryHash = this.hashString(payload.queryId.toString());
|
|
2157
2116
|
this.addEvent("QUERY_UPDATED", {
|
|
@@ -2163,7 +2122,7 @@ var DevToolsService = class {
|
|
|
2163
2122
|
onStreamUpdate(update) {
|
|
2164
2123
|
this.logger.debug({
|
|
2165
2124
|
update,
|
|
2166
|
-
Category: "
|
|
2125
|
+
Category: "sp00ky-client::DevToolsService::onStreamUpdate"
|
|
2167
2126
|
}, "StreamUpdate");
|
|
2168
2127
|
this.addEvent("STREAM_UPDATE", { updates: [update] });
|
|
2169
2128
|
this.notifyDevTools();
|
|
@@ -2218,8 +2177,8 @@ var DevToolsService = class {
|
|
|
2218
2177
|
}
|
|
2219
2178
|
notifyDevTools() {
|
|
2220
2179
|
if (typeof window !== "undefined") window.postMessage({
|
|
2221
|
-
type: "
|
|
2222
|
-
source: "
|
|
2180
|
+
type: "SP00KY_STATE_CHANGED",
|
|
2181
|
+
source: "sp00ky-devtools-page",
|
|
2223
2182
|
state: this.getState()
|
|
2224
2183
|
}, "*");
|
|
2225
2184
|
}
|
|
@@ -2245,7 +2204,7 @@ var DevToolsService = class {
|
|
|
2245
2204
|
}
|
|
2246
2205
|
exposeToWindow() {
|
|
2247
2206
|
if (typeof window !== "undefined") {
|
|
2248
|
-
window.
|
|
2207
|
+
window.__00__ = {
|
|
2249
2208
|
version: this.version,
|
|
2250
2209
|
getState: () => this.getState(),
|
|
2251
2210
|
clearHistory: () => {
|
|
@@ -2266,7 +2225,7 @@ var DevToolsService = class {
|
|
|
2266
2225
|
} catch (e) {
|
|
2267
2226
|
this.logger.error({
|
|
2268
2227
|
err: e,
|
|
2269
|
-
Category: "
|
|
2228
|
+
Category: "sp00ky-client::DevToolsService::exposeToWindow"
|
|
2270
2229
|
}, "Failed to get table data");
|
|
2271
2230
|
return [];
|
|
2272
2231
|
}
|
|
@@ -2298,7 +2257,7 @@ var DevToolsService = class {
|
|
|
2298
2257
|
this.logger.debug({
|
|
2299
2258
|
query,
|
|
2300
2259
|
target,
|
|
2301
|
-
Category: "
|
|
2260
|
+
Category: "sp00ky-client::DevToolsService::runQuery"
|
|
2302
2261
|
}, "Running query (START)");
|
|
2303
2262
|
const service = target === "remote" ? this.remoteDatabaseService : this.databaseService;
|
|
2304
2263
|
const startTime = Date.now();
|
|
@@ -2309,7 +2268,7 @@ var DevToolsService = class {
|
|
|
2309
2268
|
time: queryTime,
|
|
2310
2269
|
resultType: typeof result,
|
|
2311
2270
|
isArray: Array.isArray(result),
|
|
2312
|
-
Category: "
|
|
2271
|
+
Category: "sp00ky-client::DevToolsService::runQuery"
|
|
2313
2272
|
}, "Database returned result");
|
|
2314
2273
|
const serializeStart = Date.now();
|
|
2315
2274
|
const serialized = this.serializeForDevTools(result);
|
|
@@ -2317,7 +2276,7 @@ var DevToolsService = class {
|
|
|
2317
2276
|
this.logger.debug({
|
|
2318
2277
|
serializeTime,
|
|
2319
2278
|
serializedLength: JSON.stringify(serialized).length,
|
|
2320
|
-
Category: "
|
|
2279
|
+
Category: "sp00ky-client::DevToolsService::runQuery"
|
|
2321
2280
|
}, "Serialization complete");
|
|
2322
2281
|
return {
|
|
2323
2282
|
success: true,
|
|
@@ -2329,7 +2288,7 @@ var DevToolsService = class {
|
|
|
2329
2288
|
err: e,
|
|
2330
2289
|
query,
|
|
2331
2290
|
target,
|
|
2332
|
-
Category: "
|
|
2291
|
+
Category: "sp00ky-client::DevToolsService::runQuery"
|
|
2333
2292
|
}, "Query execution failed");
|
|
2334
2293
|
return {
|
|
2335
2294
|
success: false,
|
|
@@ -2339,13 +2298,14 @@ var DevToolsService = class {
|
|
|
2339
2298
|
}
|
|
2340
2299
|
};
|
|
2341
2300
|
window.postMessage({
|
|
2342
|
-
type: "
|
|
2343
|
-
source: "
|
|
2301
|
+
type: "SP00KY_DETECTED",
|
|
2302
|
+
source: "sp00ky-devtools-page",
|
|
2344
2303
|
data: {
|
|
2345
2304
|
version: this.version,
|
|
2346
2305
|
detected: true
|
|
2347
2306
|
}
|
|
2348
2307
|
}, "*");
|
|
2308
|
+
window.dispatchEvent(new CustomEvent("sp00ky:init"));
|
|
2349
2309
|
}
|
|
2350
2310
|
}
|
|
2351
2311
|
};
|
|
@@ -2396,9 +2356,9 @@ var AuthService = class {
|
|
|
2396
2356
|
async check(accessToken) {
|
|
2397
2357
|
this.isLoading = true;
|
|
2398
2358
|
try {
|
|
2399
|
-
const token = accessToken || await this.persistenceClient.get("
|
|
2359
|
+
const token = accessToken || await this.persistenceClient.get("sp00ky_auth_token");
|
|
2400
2360
|
if (!token) {
|
|
2401
|
-
this.logger.debug({ Category: "
|
|
2361
|
+
this.logger.debug({ Category: "sp00ky-client::AuthService::check" }, "No token found in storage or arguments");
|
|
2402
2362
|
this.isLoading = false;
|
|
2403
2363
|
this.isAuthenticated = false;
|
|
2404
2364
|
this.notifyListeners();
|
|
@@ -2411,22 +2371,22 @@ var AuthService = class {
|
|
|
2411
2371
|
if (user && user.id) {
|
|
2412
2372
|
this.logger.info({
|
|
2413
2373
|
user,
|
|
2414
|
-
Category: "
|
|
2374
|
+
Category: "sp00ky-client::AuthService::check"
|
|
2415
2375
|
}, "Auth check complete (via $auth.id)");
|
|
2416
2376
|
await this.setSession(token, user);
|
|
2417
2377
|
} else {
|
|
2418
|
-
this.logger.warn({ Category: "
|
|
2378
|
+
this.logger.warn({ Category: "sp00ky-client::AuthService::check" }, "$auth.id empty, attempting manual user fetch");
|
|
2419
2379
|
const manualResult = await this.remote.query("SELECT * FROM user WHERE id = $auth.id LIMIT 1");
|
|
2420
2380
|
const manualItems = Array.isArray(manualResult) && Array.isArray(manualResult[0]) ? manualResult[0] : manualResult;
|
|
2421
2381
|
const manualUser = Array.isArray(manualItems) ? manualItems[0] : manualItems;
|
|
2422
2382
|
if (manualUser && manualUser.id) {
|
|
2423
2383
|
this.logger.info({
|
|
2424
2384
|
user: manualUser,
|
|
2425
|
-
Category: "
|
|
2385
|
+
Category: "sp00ky-client::AuthService::check"
|
|
2426
2386
|
}, "Auth check complete (via manual fetch)");
|
|
2427
2387
|
await this.setSession(token, manualUser);
|
|
2428
2388
|
} else {
|
|
2429
|
-
this.logger.warn({ Category: "
|
|
2389
|
+
this.logger.warn({ Category: "sp00ky-client::AuthService::check" }, "Token valid but user not found via fallback");
|
|
2430
2390
|
await this.signOut();
|
|
2431
2391
|
}
|
|
2432
2392
|
}
|
|
@@ -2434,7 +2394,7 @@ var AuthService = class {
|
|
|
2434
2394
|
this.logger.error({
|
|
2435
2395
|
error,
|
|
2436
2396
|
stack: error.stack,
|
|
2437
|
-
Category: "
|
|
2397
|
+
Category: "sp00ky-client::AuthService::check"
|
|
2438
2398
|
}, "Auth check failed");
|
|
2439
2399
|
await this.signOut();
|
|
2440
2400
|
} finally {
|
|
@@ -2448,17 +2408,17 @@ var AuthService = class {
|
|
|
2448
2408
|
this.token = null;
|
|
2449
2409
|
this.currentUser = null;
|
|
2450
2410
|
this.isAuthenticated = false;
|
|
2451
|
-
await this.persistenceClient.remove("
|
|
2411
|
+
await this.persistenceClient.remove("sp00ky_auth_token");
|
|
2452
2412
|
try {
|
|
2453
2413
|
await this.remote.getClient().invalidate();
|
|
2454
|
-
} catch (
|
|
2414
|
+
} catch (_e) {}
|
|
2455
2415
|
this.notifyListeners();
|
|
2456
2416
|
}
|
|
2457
2417
|
async setSession(token, user) {
|
|
2458
2418
|
this.token = token;
|
|
2459
2419
|
this.currentUser = user;
|
|
2460
2420
|
this.isAuthenticated = true;
|
|
2461
|
-
await this.persistenceClient.set("
|
|
2421
|
+
await this.persistenceClient.set("sp00ky_auth_token", token);
|
|
2462
2422
|
this.notifyListeners();
|
|
2463
2423
|
}
|
|
2464
2424
|
async signUp(accessName, params) {
|
|
@@ -2470,13 +2430,13 @@ var AuthService = class {
|
|
|
2470
2430
|
this.logger.info({
|
|
2471
2431
|
accessName,
|
|
2472
2432
|
runtimeParams,
|
|
2473
|
-
Category: "
|
|
2433
|
+
Category: "sp00ky-client::AuthService::signUp"
|
|
2474
2434
|
}, "Attempting signup");
|
|
2475
2435
|
const { access } = await this.remote.getClient().signup({
|
|
2476
2436
|
access: accessName,
|
|
2477
2437
|
variables: runtimeParams
|
|
2478
2438
|
});
|
|
2479
|
-
this.logger.info({ Category: "
|
|
2439
|
+
this.logger.info({ Category: "sp00ky-client::AuthService::signUp" }, "Signup successful, token received");
|
|
2480
2440
|
await this.check(access);
|
|
2481
2441
|
}
|
|
2482
2442
|
async signIn(accessName, params) {
|
|
@@ -2487,7 +2447,7 @@ var AuthService = class {
|
|
|
2487
2447
|
if (missingParams.length > 0) throw new Error(`Missing required signin params for '${accessName}': ${missingParams.join(", ")}`);
|
|
2488
2448
|
this.logger.info({
|
|
2489
2449
|
accessName,
|
|
2490
|
-
Category: "
|
|
2450
|
+
Category: "sp00ky-client::AuthService::signIn"
|
|
2491
2451
|
}, "Attempting signin");
|
|
2492
2452
|
const { access } = await this.remote.getClient().signin({
|
|
2493
2453
|
access: accessName,
|
|
@@ -2526,17 +2486,17 @@ var StreamProcessorService = class {
|
|
|
2526
2486
|
*/
|
|
2527
2487
|
async init() {
|
|
2528
2488
|
if (this.isInitialized) return;
|
|
2529
|
-
this.logger.info({ Category: "
|
|
2489
|
+
this.logger.info({ Category: "sp00ky-client::StreamProcessorService::init" }, "Initializing WASM...");
|
|
2530
2490
|
try {
|
|
2531
2491
|
await init();
|
|
2532
|
-
this.processor = new
|
|
2492
|
+
this.processor = new Sp00kyProcessor();
|
|
2533
2493
|
await this.loadState();
|
|
2534
2494
|
this.isInitialized = true;
|
|
2535
|
-
this.logger.info({ Category: "
|
|
2495
|
+
this.logger.info({ Category: "sp00ky-client::StreamProcessorService::init" }, "Initialized successfully");
|
|
2536
2496
|
} catch (e) {
|
|
2537
2497
|
this.logger.error({
|
|
2538
2498
|
error: e,
|
|
2539
|
-
Category: "
|
|
2499
|
+
Category: "sp00ky-client::StreamProcessorService::init"
|
|
2540
2500
|
}, "Failed to initialize");
|
|
2541
2501
|
throw e;
|
|
2542
2502
|
}
|
|
@@ -2544,20 +2504,20 @@ var StreamProcessorService = class {
|
|
|
2544
2504
|
async loadState() {
|
|
2545
2505
|
if (!this.processor) return;
|
|
2546
2506
|
try {
|
|
2547
|
-
const result = await this.persistenceClient.get("
|
|
2507
|
+
const result = await this.persistenceClient.get("_00_stream_processor_state");
|
|
2548
2508
|
if (Array.isArray(result) && result.length > 0 && Array.isArray(result[0]) && result[0].length > 0 && result[0][0]?.state) {
|
|
2549
2509
|
const state = result[0][0].state;
|
|
2550
2510
|
this.logger.info({
|
|
2551
2511
|
stateLength: state.length,
|
|
2552
|
-
Category: "
|
|
2512
|
+
Category: "sp00ky-client::StreamProcessorService::loadState"
|
|
2553
2513
|
}, "Loading state from DB");
|
|
2554
2514
|
if (typeof this.processor.load_state === "function") this.processor.load_state(state);
|
|
2555
|
-
else this.logger.warn({ Category: "
|
|
2556
|
-
} else this.logger.info({ Category: "
|
|
2515
|
+
else this.logger.warn({ Category: "sp00ky-client::StreamProcessorService::loadState" }, "load_state method not found on processor");
|
|
2516
|
+
} else this.logger.info({ Category: "sp00ky-client::StreamProcessorService::loadState" }, "No saved state found");
|
|
2557
2517
|
} catch (e) {
|
|
2558
2518
|
this.logger.error({
|
|
2559
2519
|
error: e,
|
|
2560
|
-
Category: "
|
|
2520
|
+
Category: "sp00ky-client::StreamProcessorService::loadState"
|
|
2561
2521
|
}, "Failed to load state");
|
|
2562
2522
|
}
|
|
2563
2523
|
}
|
|
@@ -2567,14 +2527,14 @@ var StreamProcessorService = class {
|
|
|
2567
2527
|
if (typeof this.processor.save_state === "function") {
|
|
2568
2528
|
const state = this.processor.save_state();
|
|
2569
2529
|
if (state) {
|
|
2570
|
-
await this.persistenceClient.set("
|
|
2571
|
-
this.logger.trace({ Category: "
|
|
2530
|
+
await this.persistenceClient.set("_00_stream_processor_state", state);
|
|
2531
|
+
this.logger.trace({ Category: "sp00ky-client::StreamProcessorService::saveState" }, "State saved");
|
|
2572
2532
|
}
|
|
2573
2533
|
}
|
|
2574
2534
|
} catch (e) {
|
|
2575
2535
|
this.logger.error({
|
|
2576
2536
|
error: e,
|
|
2577
|
-
Category: "
|
|
2537
|
+
Category: "sp00ky-client::StreamProcessorService::saveState"
|
|
2578
2538
|
}, "Failed to save state");
|
|
2579
2539
|
}
|
|
2580
2540
|
}
|
|
@@ -2588,10 +2548,10 @@ var StreamProcessorService = class {
|
|
|
2588
2548
|
table,
|
|
2589
2549
|
op,
|
|
2590
2550
|
id,
|
|
2591
|
-
Category: "
|
|
2551
|
+
Category: "sp00ky-client::StreamProcessorService::ingest"
|
|
2592
2552
|
}, "Ingesting into ssp");
|
|
2593
2553
|
if (!this.processor) {
|
|
2594
|
-
this.logger.warn({ Category: "
|
|
2554
|
+
this.logger.warn({ Category: "sp00ky-client::StreamProcessorService::ingest" }, "Not initialized, skipping ingest");
|
|
2595
2555
|
return [];
|
|
2596
2556
|
}
|
|
2597
2557
|
try {
|
|
@@ -2602,7 +2562,7 @@ var StreamProcessorService = class {
|
|
|
2602
2562
|
op,
|
|
2603
2563
|
id,
|
|
2604
2564
|
rawUpdates: rawUpdates.length,
|
|
2605
|
-
Category: "
|
|
2565
|
+
Category: "sp00ky-client::StreamProcessorService::ingest"
|
|
2606
2566
|
}, "Ingesting into ssp done");
|
|
2607
2567
|
if (rawUpdates && Array.isArray(rawUpdates) && rawUpdates.length > 0) {
|
|
2608
2568
|
const updates = rawUpdates.map((u) => ({
|
|
@@ -2617,7 +2577,7 @@ var StreamProcessorService = class {
|
|
|
2617
2577
|
} catch (e) {
|
|
2618
2578
|
this.logger.error({
|
|
2619
2579
|
error: e,
|
|
2620
|
-
Category: "
|
|
2580
|
+
Category: "sp00ky-client::StreamProcessorService::ingest"
|
|
2621
2581
|
}, "Ingesting into ssp failed");
|
|
2622
2582
|
}
|
|
2623
2583
|
return [];
|
|
@@ -2628,14 +2588,14 @@ var StreamProcessorService = class {
|
|
|
2628
2588
|
*/
|
|
2629
2589
|
registerQueryPlan(queryPlan) {
|
|
2630
2590
|
if (!this.processor) {
|
|
2631
|
-
this.logger.warn({ Category: "
|
|
2591
|
+
this.logger.warn({ Category: "sp00ky-client::StreamProcessorService::registerQueryPlan" }, "Not initialized, skipping registration");
|
|
2632
2592
|
return;
|
|
2633
2593
|
}
|
|
2634
2594
|
this.logger.debug({
|
|
2635
2595
|
queryHash: queryPlan.queryHash,
|
|
2636
2596
|
surql: queryPlan.surql,
|
|
2637
2597
|
params: queryPlan.params,
|
|
2638
|
-
Category: "
|
|
2598
|
+
Category: "sp00ky-client::StreamProcessorService::registerQueryPlan"
|
|
2639
2599
|
}, "Registering query plan");
|
|
2640
2600
|
try {
|
|
2641
2601
|
const normalizedParams = this.normalizeValue(queryPlan.params);
|
|
@@ -2649,7 +2609,7 @@ var StreamProcessorService = class {
|
|
|
2649
2609
|
});
|
|
2650
2610
|
this.logger.debug({
|
|
2651
2611
|
initialUpdate,
|
|
2652
|
-
Category: "
|
|
2612
|
+
Category: "sp00ky-client::StreamProcessorService::registerQueryPlan"
|
|
2653
2613
|
}, "register_view result");
|
|
2654
2614
|
if (!initialUpdate) throw new Error("Failed to register query plan");
|
|
2655
2615
|
const update = {
|
|
@@ -2661,13 +2621,13 @@ var StreamProcessorService = class {
|
|
|
2661
2621
|
queryHash: queryPlan.queryHash,
|
|
2662
2622
|
surql: queryPlan.surql,
|
|
2663
2623
|
params: queryPlan.params,
|
|
2664
|
-
Category: "
|
|
2624
|
+
Category: "sp00ky-client::StreamProcessorService::registerQueryPlan"
|
|
2665
2625
|
}, "Registered query plan");
|
|
2666
2626
|
return update;
|
|
2667
2627
|
} catch (e) {
|
|
2668
2628
|
this.logger.error({
|
|
2669
2629
|
error: e,
|
|
2670
|
-
Category: "
|
|
2630
|
+
Category: "sp00ky-client::StreamProcessorService::registerQueryPlan"
|
|
2671
2631
|
}, "Error registering query plan");
|
|
2672
2632
|
throw e;
|
|
2673
2633
|
}
|
|
@@ -2683,7 +2643,7 @@ var StreamProcessorService = class {
|
|
|
2683
2643
|
} catch (e) {
|
|
2684
2644
|
this.logger.error({
|
|
2685
2645
|
error: e,
|
|
2686
|
-
Category: "
|
|
2646
|
+
Category: "sp00ky-client::StreamProcessorService::unregisterQueryPlan"
|
|
2687
2647
|
}, "Error unregistering query plan");
|
|
2688
2648
|
}
|
|
2689
2649
|
}
|
|
@@ -2698,7 +2658,7 @@ var StreamProcessorService = class {
|
|
|
2698
2658
|
const result = value.toString();
|
|
2699
2659
|
this.logger.trace({
|
|
2700
2660
|
result,
|
|
2701
|
-
Category: "
|
|
2661
|
+
Category: "sp00ky-client::StreamProcessorService::normalizeValue"
|
|
2702
2662
|
}, "RecordId detected");
|
|
2703
2663
|
return result;
|
|
2704
2664
|
}
|
|
@@ -2741,7 +2701,7 @@ var CacheModule = class {
|
|
|
2741
2701
|
this.logger.debug({
|
|
2742
2702
|
queryHash: update.queryHash,
|
|
2743
2703
|
arrayLength: update.localArray?.length,
|
|
2744
|
-
Category: "
|
|
2704
|
+
Category: "sp00ky-client::CacheModule::onStreamUpdate"
|
|
2745
2705
|
}, "Stream update received");
|
|
2746
2706
|
this.streamUpdateCallback(update);
|
|
2747
2707
|
}
|
|
@@ -2764,7 +2724,7 @@ var CacheModule = class {
|
|
|
2764
2724
|
if (records.length === 0) return;
|
|
2765
2725
|
this.logger.debug({
|
|
2766
2726
|
count: records.length,
|
|
2767
|
-
Category: "
|
|
2727
|
+
Category: "sp00ky-client::CacheModule::saveBatch"
|
|
2768
2728
|
}, "Saving record batch");
|
|
2769
2729
|
try {
|
|
2770
2730
|
const populatedRecords = records.map((record) => {
|
|
@@ -2773,7 +2733,7 @@ var CacheModule = class {
|
|
|
2773
2733
|
...record,
|
|
2774
2734
|
record: {
|
|
2775
2735
|
...record.record,
|
|
2776
|
-
|
|
2736
|
+
_00_rv: record.version
|
|
2777
2737
|
}
|
|
2778
2738
|
};
|
|
2779
2739
|
});
|
|
@@ -2798,13 +2758,13 @@ var CacheModule = class {
|
|
|
2798
2758
|
}
|
|
2799
2759
|
this.logger.debug({
|
|
2800
2760
|
count: records.length,
|
|
2801
|
-
Category: "
|
|
2761
|
+
Category: "sp00ky-client::CacheModule::saveBatch"
|
|
2802
2762
|
}, "Batch saved successfully");
|
|
2803
2763
|
} catch (err) {
|
|
2804
2764
|
this.logger.error({
|
|
2805
2765
|
err,
|
|
2806
2766
|
count: records.length,
|
|
2807
|
-
Category: "
|
|
2767
|
+
Category: "sp00ky-client::CacheModule::saveBatch"
|
|
2808
2768
|
}, "Failed to save batch");
|
|
2809
2769
|
throw err;
|
|
2810
2770
|
}
|
|
@@ -2812,27 +2772,27 @@ var CacheModule = class {
|
|
|
2812
2772
|
/**
|
|
2813
2773
|
* Delete a record from local DB and ingest deletion into DBSP
|
|
2814
2774
|
*/
|
|
2815
|
-
async delete(table, id, skipDbDelete = false) {
|
|
2775
|
+
async delete(table, id, skipDbDelete = false, recordData = {}) {
|
|
2816
2776
|
this.logger.debug({
|
|
2817
2777
|
table,
|
|
2818
2778
|
id,
|
|
2819
|
-
Category: "
|
|
2779
|
+
Category: "sp00ky-client::CacheModule::delete"
|
|
2820
2780
|
}, "Deleting record");
|
|
2821
2781
|
try {
|
|
2822
2782
|
if (!skipDbDelete) await this.local.query("DELETE $id", { id: parseRecordIdString(id) });
|
|
2823
2783
|
delete this.versionLookups[id];
|
|
2824
|
-
|
|
2784
|
+
this.streamProcessor.ingest(table, "DELETE", id, recordData);
|
|
2825
2785
|
this.logger.debug({
|
|
2826
2786
|
table,
|
|
2827
2787
|
id,
|
|
2828
|
-
Category: "
|
|
2788
|
+
Category: "sp00ky-client::CacheModule::delete"
|
|
2829
2789
|
}, "Record deleted successfully");
|
|
2830
2790
|
} catch (err) {
|
|
2831
2791
|
this.logger.error({
|
|
2832
2792
|
err,
|
|
2833
2793
|
table,
|
|
2834
2794
|
id,
|
|
2835
|
-
Category: "
|
|
2795
|
+
Category: "sp00ky-client::CacheModule::delete"
|
|
2836
2796
|
}, "Failed to delete record");
|
|
2837
2797
|
throw err;
|
|
2838
2798
|
}
|
|
@@ -2845,7 +2805,7 @@ var CacheModule = class {
|
|
|
2845
2805
|
this.logger.debug({
|
|
2846
2806
|
queryHash: config.queryHash,
|
|
2847
2807
|
surql: config.surql,
|
|
2848
|
-
Category: "
|
|
2808
|
+
Category: "sp00ky-client::CacheModule::registerQuery"
|
|
2849
2809
|
}, "Registering query");
|
|
2850
2810
|
try {
|
|
2851
2811
|
const update = this.streamProcessor.registerQueryPlan({
|
|
@@ -2862,14 +2822,14 @@ var CacheModule = class {
|
|
|
2862
2822
|
this.logger.debug({
|
|
2863
2823
|
queryHash: config.queryHash,
|
|
2864
2824
|
arrayLength: update.localArray?.length,
|
|
2865
|
-
Category: "
|
|
2825
|
+
Category: "sp00ky-client::CacheModule::registerQuery"
|
|
2866
2826
|
}, "Query registered successfully");
|
|
2867
2827
|
return { localArray: update.localArray };
|
|
2868
2828
|
} catch (err) {
|
|
2869
2829
|
this.logger.error({
|
|
2870
2830
|
err,
|
|
2871
2831
|
queryHash: config.queryHash,
|
|
2872
|
-
Category: "
|
|
2832
|
+
Category: "sp00ky-client::CacheModule::registerQuery"
|
|
2873
2833
|
}, "Failed to register query");
|
|
2874
2834
|
throw err;
|
|
2875
2835
|
}
|
|
@@ -2880,24 +2840,323 @@ var CacheModule = class {
|
|
|
2880
2840
|
unregisterQuery(queryHash) {
|
|
2881
2841
|
this.logger.debug({
|
|
2882
2842
|
queryHash,
|
|
2883
|
-
Category: "
|
|
2843
|
+
Category: "sp00ky-client::CacheModule::unregisterQuery"
|
|
2884
2844
|
}, "Unregistering query");
|
|
2885
2845
|
try {
|
|
2886
2846
|
this.streamProcessor.unregisterQueryPlan(queryHash);
|
|
2887
2847
|
this.logger.debug({
|
|
2888
2848
|
queryHash,
|
|
2889
|
-
Category: "
|
|
2849
|
+
Category: "sp00ky-client::CacheModule::unregisterQuery"
|
|
2890
2850
|
}, "Query unregistered successfully");
|
|
2891
2851
|
} catch (err) {
|
|
2892
2852
|
this.logger.error({
|
|
2893
2853
|
err,
|
|
2894
2854
|
queryHash,
|
|
2895
|
-
Category: "
|
|
2855
|
+
Category: "sp00ky-client::CacheModule::unregisterQuery"
|
|
2896
2856
|
}, "Failed to unregister query");
|
|
2897
2857
|
}
|
|
2898
2858
|
}
|
|
2899
2859
|
};
|
|
2900
2860
|
|
|
2861
|
+
//#endregion
|
|
2862
|
+
//#region src/modules/crdt/crdt-field.ts
|
|
2863
|
+
const CURSOR_COLORS = [
|
|
2864
|
+
"#3b82f6",
|
|
2865
|
+
"#ef4444",
|
|
2866
|
+
"#22c55e",
|
|
2867
|
+
"#f59e0b",
|
|
2868
|
+
"#8b5cf6",
|
|
2869
|
+
"#ec4899",
|
|
2870
|
+
"#14b8a6",
|
|
2871
|
+
"#f97316"
|
|
2872
|
+
];
|
|
2873
|
+
function cursorColorFromName(name) {
|
|
2874
|
+
let hash = 0;
|
|
2875
|
+
for (let i = 0; i < name.length; i++) hash = (hash << 5) - hash + name.charCodeAt(i) | 0;
|
|
2876
|
+
return CURSOR_COLORS[Math.abs(hash) % CURSOR_COLORS.length];
|
|
2877
|
+
}
|
|
2878
|
+
var CrdtField = class {
|
|
2879
|
+
doc;
|
|
2880
|
+
pushTimer = null;
|
|
2881
|
+
remote = null;
|
|
2882
|
+
recordId = null;
|
|
2883
|
+
unsubscribe = null;
|
|
2884
|
+
lastPushTime = 0;
|
|
2885
|
+
lastCursorPushTime = 0;
|
|
2886
|
+
loadedFromCrdt = false;
|
|
2887
|
+
pushRetryCount = 0;
|
|
2888
|
+
logger;
|
|
2889
|
+
_onCursorUpdate = null;
|
|
2890
|
+
pendingCursorUpdate = null;
|
|
2891
|
+
/** Callback set by the editor to receive remote cursor updates.
|
|
2892
|
+
* Any cursor data that arrived before this callback was set will be replayed. */
|
|
2893
|
+
set onCursorUpdate(cb) {
|
|
2894
|
+
this._onCursorUpdate = cb;
|
|
2895
|
+
if (cb && this.pendingCursorUpdate) {
|
|
2896
|
+
try {
|
|
2897
|
+
cb(this.pendingCursorUpdate);
|
|
2898
|
+
} catch (e) {
|
|
2899
|
+
this.logger?.warn({
|
|
2900
|
+
error: e,
|
|
2901
|
+
Category: "sp00ky-client::CrdtField::onCursorUpdate"
|
|
2902
|
+
}, "Failed to replay pending cursor update");
|
|
2903
|
+
}
|
|
2904
|
+
this.pendingCursorUpdate = null;
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
get onCursorUpdate() {
|
|
2908
|
+
return this._onCursorUpdate;
|
|
2909
|
+
}
|
|
2910
|
+
constructor(fieldName, initialState, logger) {
|
|
2911
|
+
this.fieldName = fieldName;
|
|
2912
|
+
this.logger = logger ?? null;
|
|
2913
|
+
this.doc = new LoroDoc();
|
|
2914
|
+
if (initialState) {
|
|
2915
|
+
this.doc.import(decodeBase64(initialState));
|
|
2916
|
+
this.loadedFromCrdt = true;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
getDoc() {
|
|
2920
|
+
return this.doc;
|
|
2921
|
+
}
|
|
2922
|
+
/** Whether the LoroDoc was loaded from saved CRDT state */
|
|
2923
|
+
hasContent() {
|
|
2924
|
+
return this.loadedFromCrdt;
|
|
2925
|
+
}
|
|
2926
|
+
startSync(remote, recordId) {
|
|
2927
|
+
this.remote = remote;
|
|
2928
|
+
this.recordId = recordId;
|
|
2929
|
+
this.unsubscribe = this.doc.subscribeLocalUpdates(() => {
|
|
2930
|
+
this.schedulePush();
|
|
2931
|
+
});
|
|
2932
|
+
}
|
|
2933
|
+
stopSync() {
|
|
2934
|
+
if (this.unsubscribe) {
|
|
2935
|
+
this.unsubscribe();
|
|
2936
|
+
this.unsubscribe = null;
|
|
2937
|
+
}
|
|
2938
|
+
if (this.pushTimer) {
|
|
2939
|
+
clearTimeout(this.pushTimer);
|
|
2940
|
+
this.pushTimer = null;
|
|
2941
|
+
}
|
|
2942
|
+
if (this.remote && this.recordId) this.pushToRemote();
|
|
2943
|
+
}
|
|
2944
|
+
importRemote(base64State) {
|
|
2945
|
+
if (Date.now() - this.lastPushTime < 500) return;
|
|
2946
|
+
try {
|
|
2947
|
+
this.doc.import(decodeBase64(base64State));
|
|
2948
|
+
} catch (e) {
|
|
2949
|
+
this.logger?.warn({
|
|
2950
|
+
error: e,
|
|
2951
|
+
Category: "sp00ky-client::CrdtField::importRemote"
|
|
2952
|
+
}, "Failed to import remote CRDT state");
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
exportSnapshot() {
|
|
2956
|
+
return encodeBase64(this.doc.export({ mode: "snapshot" }));
|
|
2957
|
+
}
|
|
2958
|
+
/** Push cursor ephemeral state to _00_crdt as a "_cursor_<fieldName>" entry */
|
|
2959
|
+
async pushCursorState(encoded) {
|
|
2960
|
+
if (!this.remote || !this.recordId) return;
|
|
2961
|
+
this.lastCursorPushTime = Date.now();
|
|
2962
|
+
try {
|
|
2963
|
+
const state = encodeBase64(encoded);
|
|
2964
|
+
await this.remote.query(`INSERT INTO _00_crdt (record_id, field, state) VALUES ($rid, $field, $state)
|
|
2965
|
+
ON DUPLICATE KEY UPDATE state = $state`, {
|
|
2966
|
+
rid: parseRecordIdString(this.recordId),
|
|
2967
|
+
field: `_cursor_${this.fieldName}`,
|
|
2968
|
+
state
|
|
2969
|
+
});
|
|
2970
|
+
} catch (e) {
|
|
2971
|
+
this.logger?.warn({
|
|
2972
|
+
error: e,
|
|
2973
|
+
Category: "sp00ky-client::CrdtField::pushCursorState"
|
|
2974
|
+
}, "Failed to push cursor state");
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
/** Import remote cursor state (called by CrdtManager from LIVE SELECT) */
|
|
2978
|
+
importRemoteCursor(base64State) {
|
|
2979
|
+
if (Date.now() - this.lastCursorPushTime < 300) return;
|
|
2980
|
+
try {
|
|
2981
|
+
const data = decodeBase64(base64State);
|
|
2982
|
+
if (this._onCursorUpdate) this._onCursorUpdate(data);
|
|
2983
|
+
else this.pendingCursorUpdate = data;
|
|
2984
|
+
} catch (e) {
|
|
2985
|
+
this.logger?.warn({
|
|
2986
|
+
error: e,
|
|
2987
|
+
Category: "sp00ky-client::CrdtField::importRemoteCursor"
|
|
2988
|
+
}, "Failed to apply remote cursor data");
|
|
2989
|
+
}
|
|
2990
|
+
}
|
|
2991
|
+
schedulePush() {
|
|
2992
|
+
if (this.pushTimer) clearTimeout(this.pushTimer);
|
|
2993
|
+
this.pushTimer = setTimeout(() => void this.pushToRemote(), 300);
|
|
2994
|
+
}
|
|
2995
|
+
async pushToRemote() {
|
|
2996
|
+
if (!this.remote || !this.recordId) return;
|
|
2997
|
+
this.lastPushTime = Date.now();
|
|
2998
|
+
try {
|
|
2999
|
+
await this.remote.query(`INSERT INTO _00_crdt (record_id, field, state) VALUES ($rid, $field, $state)
|
|
3000
|
+
ON DUPLICATE KEY UPDATE state = $state`, {
|
|
3001
|
+
rid: parseRecordIdString(this.recordId),
|
|
3002
|
+
field: this.fieldName,
|
|
3003
|
+
state: this.exportSnapshot()
|
|
3004
|
+
});
|
|
3005
|
+
this.pushRetryCount = 0;
|
|
3006
|
+
} catch (e) {
|
|
3007
|
+
this.logger?.warn({
|
|
3008
|
+
error: e,
|
|
3009
|
+
Category: "sp00ky-client::CrdtField::pushToRemote"
|
|
3010
|
+
}, "Failed to push CRDT state to remote");
|
|
3011
|
+
if (this.pushRetryCount < 2) {
|
|
3012
|
+
this.pushRetryCount++;
|
|
3013
|
+
this.schedulePush();
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
function decodeBase64(b64) {
|
|
3019
|
+
if (typeof atob === "function") {
|
|
3020
|
+
const binary = atob(b64);
|
|
3021
|
+
const bytes = new Uint8Array(binary.length);
|
|
3022
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
3023
|
+
return bytes;
|
|
3024
|
+
}
|
|
3025
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
3026
|
+
}
|
|
3027
|
+
function encodeBase64(bytes) {
|
|
3028
|
+
if (typeof btoa === "function") {
|
|
3029
|
+
let binary = "";
|
|
3030
|
+
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
3031
|
+
return btoa(binary);
|
|
3032
|
+
}
|
|
3033
|
+
return Buffer.from(bytes).toString("base64");
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
//#endregion
|
|
3037
|
+
//#region src/modules/crdt/index.ts
|
|
3038
|
+
/**
|
|
3039
|
+
* CrdtManager manages active CrdtField instances and their sync channels.
|
|
3040
|
+
*
|
|
3041
|
+
* Each open record gets a LIVE SELECT on _00_crdt that delivers remote
|
|
3042
|
+
* changes in real time.
|
|
3043
|
+
*/
|
|
3044
|
+
var CrdtManager = class {
|
|
3045
|
+
fields = /* @__PURE__ */ new Map();
|
|
3046
|
+
liveQueries = /* @__PURE__ */ new Map();
|
|
3047
|
+
logger;
|
|
3048
|
+
constructor(schema, remote, logger) {
|
|
3049
|
+
this.schema = schema;
|
|
3050
|
+
this.remote = remote;
|
|
3051
|
+
this.logger = logger.child({ service: "CrdtManager" });
|
|
3052
|
+
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Open a CRDT field for collaborative editing.
|
|
3055
|
+
*
|
|
3056
|
+
* @param table - Table name
|
|
3057
|
+
* @param recordId - Full record ID (e.g., "thread:abc")
|
|
3058
|
+
* @param field - Field name (e.g., "title", "content")
|
|
3059
|
+
* @param fallbackText - Current plain text from the record, used to seed the
|
|
3060
|
+
* LoroDoc if no CRDT state exists yet (migration path)
|
|
3061
|
+
*/
|
|
3062
|
+
async open(table, recordId, field, fallbackText) {
|
|
3063
|
+
const key = this.makeKey(table, recordId, field);
|
|
3064
|
+
let crdtField = this.fields.get(key);
|
|
3065
|
+
if (crdtField) return crdtField;
|
|
3066
|
+
let initialCrdtState;
|
|
3067
|
+
try {
|
|
3068
|
+
const [result] = await this.remote.query("SELECT VALUE state FROM _00_crdt WHERE record_id = $rid AND field = $field LIMIT 1", {
|
|
3069
|
+
rid: parseRecordIdString(recordId),
|
|
3070
|
+
field
|
|
3071
|
+
});
|
|
3072
|
+
if (result && result.length > 0 && result[0]) initialCrdtState = result[0];
|
|
3073
|
+
} catch (e) {
|
|
3074
|
+
this.logger.debug({
|
|
3075
|
+
error: e,
|
|
3076
|
+
Category: "sp00ky-client::CrdtManager::open"
|
|
3077
|
+
}, "No existing CRDT state found");
|
|
3078
|
+
}
|
|
3079
|
+
crdtField = new CrdtField(field, initialCrdtState, this.logger);
|
|
3080
|
+
crdtField.startSync(this.remote, recordId);
|
|
3081
|
+
this.fields.set(key, crdtField);
|
|
3082
|
+
this.logger.info({
|
|
3083
|
+
key,
|
|
3084
|
+
hasInitialState: !!initialCrdtState,
|
|
3085
|
+
hasFallback: !!fallbackText,
|
|
3086
|
+
Category: "sp00ky-client::CrdtManager::open"
|
|
3087
|
+
}, "CrdtField opened");
|
|
3088
|
+
await this.ensureLiveSelect(table, recordId);
|
|
3089
|
+
return crdtField;
|
|
3090
|
+
}
|
|
3091
|
+
close(table, recordId, field) {
|
|
3092
|
+
const key = this.makeKey(table, recordId, field);
|
|
3093
|
+
const crdtField = this.fields.get(key);
|
|
3094
|
+
if (crdtField) {
|
|
3095
|
+
crdtField.stopSync();
|
|
3096
|
+
this.fields.delete(key);
|
|
3097
|
+
}
|
|
3098
|
+
const prefix = `${table}:${recordId}:`;
|
|
3099
|
+
if (!Array.from(this.fields.keys()).some((k) => k !== key && k.startsWith(prefix))) this.killLiveSelect(recordId);
|
|
3100
|
+
this.logger.debug({
|
|
3101
|
+
key,
|
|
3102
|
+
Category: "sp00ky-client::CrdtManager::close"
|
|
3103
|
+
}, "CrdtField closed");
|
|
3104
|
+
}
|
|
3105
|
+
closeAll() {
|
|
3106
|
+
for (const [_, field] of this.fields) field.stopSync();
|
|
3107
|
+
this.fields.clear();
|
|
3108
|
+
for (const recordId of this.liveQueries.keys()) this.killLiveSelect(recordId);
|
|
3109
|
+
}
|
|
3110
|
+
async ensureLiveSelect(table, recordId) {
|
|
3111
|
+
if (this.liveQueries.has(recordId)) return;
|
|
3112
|
+
try {
|
|
3113
|
+
const [uuid] = await this.remote.query(`LIVE SELECT * FROM _00_crdt WHERE record_id = $rid`, { rid: parseRecordIdString(recordId) });
|
|
3114
|
+
this.liveQueries.set(recordId, {
|
|
3115
|
+
uuid,
|
|
3116
|
+
table
|
|
3117
|
+
});
|
|
3118
|
+
(await this.remote.getClient().liveOf(uuid)).subscribe((message) => {
|
|
3119
|
+
if (message.action === "KILLED") return;
|
|
3120
|
+
if (message.action === "CREATE" || message.action === "UPDATE") {
|
|
3121
|
+
const fieldName = message.value.field;
|
|
3122
|
+
const state = message.value.state;
|
|
3123
|
+
if (!fieldName || !state) return;
|
|
3124
|
+
if (fieldName.startsWith("_cursor_")) {
|
|
3125
|
+
const actualField = fieldName.slice(8);
|
|
3126
|
+
const key = this.makeKey(table, recordId, actualField);
|
|
3127
|
+
const crdtField = this.fields.get(key);
|
|
3128
|
+
if (crdtField) crdtField.importRemoteCursor(state);
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
const key = this.makeKey(table, recordId, fieldName);
|
|
3132
|
+
const crdtField = this.fields.get(key);
|
|
3133
|
+
if (crdtField) crdtField.importRemote(state);
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
this.logger.info({
|
|
3137
|
+
recordId,
|
|
3138
|
+
Category: "sp00ky-client::CrdtManager::ensureLiveSelect"
|
|
3139
|
+
}, "LIVE SELECT on _00_crdt started");
|
|
3140
|
+
} catch (e) {
|
|
3141
|
+
this.logger.warn({
|
|
3142
|
+
error: e,
|
|
3143
|
+
recordId,
|
|
3144
|
+
Category: "sp00ky-client::CrdtManager::ensureLiveSelect"
|
|
3145
|
+
}, "Failed to start LIVE SELECT on _00_crdt");
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
killLiveSelect(recordId) {
|
|
3149
|
+
const entry = this.liveQueries.get(recordId);
|
|
3150
|
+
if (entry) {
|
|
3151
|
+
this.remote.query("KILL $uuid", { uuid: entry.uuid }).catch(() => {});
|
|
3152
|
+
this.liveQueries.delete(recordId);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
makeKey(table, recordId, field) {
|
|
3156
|
+
return `${table}:${recordId}:${field}`;
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
|
|
2901
3160
|
//#endregion
|
|
2902
3161
|
//#region src/services/persistence/localstorage.ts
|
|
2903
3162
|
var LocalStoragePersistenceClient = class {
|
|
@@ -2930,7 +3189,7 @@ var SurrealDBPersistenceClient = class {
|
|
|
2930
3189
|
}
|
|
2931
3190
|
async set(key, val) {
|
|
2932
3191
|
try {
|
|
2933
|
-
const id = parseRecordIdString(`
|
|
3192
|
+
const id = parseRecordIdString(`_00_kv:${key}`);
|
|
2934
3193
|
await this.db.query(surql.seal(surql.upsert("id", "data")), {
|
|
2935
3194
|
id,
|
|
2936
3195
|
data: { val }
|
|
@@ -2938,40 +3197,69 @@ var SurrealDBPersistenceClient = class {
|
|
|
2938
3197
|
} catch (error) {
|
|
2939
3198
|
this.logger.error({
|
|
2940
3199
|
error,
|
|
2941
|
-
Category: "
|
|
3200
|
+
Category: "sp00ky-client::SurrealDBPersistenceClient::set"
|
|
2942
3201
|
}, "Failed to set KV");
|
|
2943
3202
|
throw error;
|
|
2944
3203
|
}
|
|
2945
3204
|
}
|
|
2946
3205
|
async get(key) {
|
|
2947
3206
|
try {
|
|
2948
|
-
const id = parseRecordIdString(`
|
|
3207
|
+
const id = parseRecordIdString(`_00_kv:${key}`);
|
|
2949
3208
|
const [result] = await this.db.query(surql.seal(surql.selectById("id", ["val"])), { id });
|
|
2950
3209
|
if (!result?.val) return null;
|
|
2951
3210
|
return result.val;
|
|
2952
3211
|
} catch (error) {
|
|
2953
3212
|
this.logger.warn({
|
|
2954
3213
|
error,
|
|
2955
|
-
Category: "
|
|
3214
|
+
Category: "sp00ky-client::SurrealDBPersistenceClient::get"
|
|
2956
3215
|
}, "Failed to get KV");
|
|
2957
3216
|
return null;
|
|
2958
3217
|
}
|
|
2959
3218
|
}
|
|
2960
3219
|
async remove(key) {
|
|
2961
3220
|
try {
|
|
2962
|
-
const id = parseRecordIdString(`
|
|
3221
|
+
const id = parseRecordIdString(`_00_kv:${key}`);
|
|
2963
3222
|
await this.db.query(surql.seal(surql.delete("id")), { id });
|
|
2964
3223
|
} catch (err) {
|
|
2965
3224
|
this.logger.info({
|
|
2966
3225
|
err,
|
|
2967
|
-
Category: "
|
|
3226
|
+
Category: "sp00ky-client::SurrealDBPersistenceClient::remove"
|
|
2968
3227
|
}, "Failed to delete KV");
|
|
2969
3228
|
}
|
|
2970
3229
|
}
|
|
2971
3230
|
};
|
|
2972
3231
|
|
|
2973
3232
|
//#endregion
|
|
2974
|
-
//#region src/
|
|
3233
|
+
//#region src/services/persistence/resilient.ts
|
|
3234
|
+
var ResilientPersistenceClient = class {
|
|
3235
|
+
logger;
|
|
3236
|
+
constructor(inner, logger) {
|
|
3237
|
+
this.inner = inner;
|
|
3238
|
+
this.logger = logger.child({ service: "ResilientPersistenceClient" });
|
|
3239
|
+
}
|
|
3240
|
+
set(key, value) {
|
|
3241
|
+
return this.inner.set(key, value);
|
|
3242
|
+
}
|
|
3243
|
+
async get(key) {
|
|
3244
|
+
try {
|
|
3245
|
+
return await this.inner.get(key);
|
|
3246
|
+
} catch (e) {
|
|
3247
|
+
this.logger.warn({
|
|
3248
|
+
key,
|
|
3249
|
+
error: e,
|
|
3250
|
+
Category: "sp00ky-client::ResilientPersistenceClient::get"
|
|
3251
|
+
}, "Persistence read failed, dropping key");
|
|
3252
|
+
await this.inner.remove(key).catch(() => {});
|
|
3253
|
+
return null;
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
remove(key) {
|
|
3257
|
+
return this.inner.remove(key);
|
|
3258
|
+
}
|
|
3259
|
+
};
|
|
3260
|
+
|
|
3261
|
+
//#endregion
|
|
3262
|
+
//#region src/sp00ky.ts
|
|
2975
3263
|
var BucketHandle = class {
|
|
2976
3264
|
constructor(bucketName, remote) {
|
|
2977
3265
|
this.bucketName = bucketName;
|
|
@@ -3007,7 +3295,7 @@ var BucketHandle = class {
|
|
|
3007
3295
|
return result;
|
|
3008
3296
|
}
|
|
3009
3297
|
};
|
|
3010
|
-
var
|
|
3298
|
+
var Sp00kyClient = class {
|
|
3011
3299
|
local;
|
|
3012
3300
|
remote;
|
|
3013
3301
|
persistenceClient;
|
|
@@ -3016,6 +3304,7 @@ var SpookyClient = class {
|
|
|
3016
3304
|
dataModule;
|
|
3017
3305
|
sync;
|
|
3018
3306
|
devTools;
|
|
3307
|
+
crdtManager;
|
|
3019
3308
|
logger;
|
|
3020
3309
|
auth;
|
|
3021
3310
|
streamProcessor;
|
|
@@ -3033,28 +3322,30 @@ var SpookyClient = class {
|
|
|
3033
3322
|
}
|
|
3034
3323
|
constructor(config) {
|
|
3035
3324
|
this.config = config;
|
|
3036
|
-
const logger = createLogger(config.logLevel ?? "info", config.
|
|
3037
|
-
this.logger = logger.child({ service: "
|
|
3325
|
+
const logger = createLogger(config.logLevel ?? "info", config.otelTransmit);
|
|
3326
|
+
this.logger = logger.child({ service: "Sp00kyClient" });
|
|
3038
3327
|
this.logger.info({
|
|
3039
3328
|
config: {
|
|
3040
3329
|
...config,
|
|
3041
3330
|
schema: "[SchemaStructure]"
|
|
3042
3331
|
},
|
|
3043
|
-
Category: "
|
|
3044
|
-
}, "
|
|
3332
|
+
Category: "sp00ky-client::Sp00kyClient::constructor"
|
|
3333
|
+
}, "Sp00kyClient initialized");
|
|
3045
3334
|
this.local = new LocalDatabaseService(this.config.database, logger);
|
|
3046
3335
|
this.remote = new RemoteDatabaseService(this.config.database, logger);
|
|
3047
3336
|
if (config.persistenceClient === "surrealdb") this.persistenceClient = new SurrealDBPersistenceClient(this.local, logger);
|
|
3048
3337
|
else if (config.persistenceClient === "localstorage" || !config.persistenceClient) this.persistenceClient = new LocalStoragePersistenceClient(logger);
|
|
3049
3338
|
else this.persistenceClient = config.persistenceClient;
|
|
3339
|
+
this.persistenceClient = new ResilientPersistenceClient(this.persistenceClient, logger);
|
|
3050
3340
|
this.streamProcessor = new StreamProcessorService(new EventSystem(["stream_update"]), this.local, this.persistenceClient, logger);
|
|
3051
3341
|
this.migrator = new LocalMigrator(this.local, logger);
|
|
3052
3342
|
this.cache = new CacheModule(this.local, this.streamProcessor, (update) => {
|
|
3053
3343
|
this.dataModule.onStreamUpdate(update);
|
|
3054
3344
|
}, logger);
|
|
3345
|
+
this.crdtManager = new CrdtManager(this.config.schema, this.remote, logger);
|
|
3055
3346
|
this.dataModule = new DataModule(this.cache, this.local, this.config.schema, logger, this.config.streamDebounceTime);
|
|
3056
3347
|
this.auth = new AuthService(this.config.schema, this.remote, this.persistenceClient, logger);
|
|
3057
|
-
this.sync = new
|
|
3348
|
+
this.sync = new Sp00kySync(this.local, this.remote, this.cache, this.dataModule, this.config.schema, this.logger);
|
|
3058
3349
|
this.devTools = new DevToolsService(this.local, this.remote, logger, this.config.schema, this.auth, this.dataModule);
|
|
3059
3350
|
this.streamProcessor.addReceiver(this.devTools);
|
|
3060
3351
|
this.setupCallbacks();
|
|
@@ -3078,44 +3369,59 @@ var SpookyClient = class {
|
|
|
3078
3369
|
});
|
|
3079
3370
|
}
|
|
3080
3371
|
async init() {
|
|
3081
|
-
this.logger.info({ Category: "
|
|
3372
|
+
this.logger.info({ Category: "sp00ky-client::Sp00kyClient::init" }, "Sp00kyClient initialization started");
|
|
3082
3373
|
try {
|
|
3083
3374
|
const clientId = this.config.clientId ?? await this.loadOrGenerateClientId();
|
|
3084
3375
|
this.persistClientId(clientId);
|
|
3085
3376
|
this.logger.debug({
|
|
3086
3377
|
clientId,
|
|
3087
|
-
Category: "
|
|
3378
|
+
Category: "sp00ky-client::Sp00kyClient::init"
|
|
3088
3379
|
}, "Client ID loaded");
|
|
3089
3380
|
await this.local.connect();
|
|
3090
|
-
this.logger.debug({ Category: "
|
|
3381
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "Local database connected");
|
|
3091
3382
|
await this.migrator.provision(this.config.schemaSurql);
|
|
3092
|
-
this.logger.debug({ Category: "
|
|
3383
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "Schema provisioned");
|
|
3093
3384
|
await this.remote.connect();
|
|
3094
|
-
this.logger.debug({ Category: "
|
|
3385
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "Remote database connected");
|
|
3095
3386
|
await this.streamProcessor.init();
|
|
3096
|
-
this.logger.debug({ Category: "
|
|
3387
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "StreamProcessor initialized");
|
|
3097
3388
|
await this.auth.init();
|
|
3098
|
-
this.logger.debug({ Category: "
|
|
3389
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "Auth initialized");
|
|
3099
3390
|
await this.dataModule.init();
|
|
3100
|
-
this.logger.debug({ Category: "
|
|
3391
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "DataModule initialized");
|
|
3101
3392
|
await this.sync.init(clientId);
|
|
3102
|
-
this.logger.debug({ Category: "
|
|
3103
|
-
this.logger.info({ Category: "
|
|
3393
|
+
this.logger.debug({ Category: "sp00ky-client::Sp00kyClient::init" }, "Sync initialized");
|
|
3394
|
+
this.logger.info({ Category: "sp00ky-client::Sp00kyClient::init" }, "Sp00kyClient initialization completed successfully");
|
|
3104
3395
|
} catch (e) {
|
|
3105
3396
|
this.logger.error({
|
|
3106
3397
|
error: e,
|
|
3107
|
-
Category: "
|
|
3108
|
-
}, "
|
|
3398
|
+
Category: "sp00ky-client::Sp00kyClient::init"
|
|
3399
|
+
}, "Sp00kyClient initialization failed");
|
|
3109
3400
|
throw e;
|
|
3110
3401
|
}
|
|
3111
3402
|
}
|
|
3112
3403
|
async close() {
|
|
3404
|
+
this.crdtManager.closeAll();
|
|
3113
3405
|
await this.local.close();
|
|
3114
3406
|
await this.remote.close();
|
|
3115
3407
|
}
|
|
3116
3408
|
authenticate(token) {
|
|
3117
3409
|
return this.remote.getClient().authenticate(token);
|
|
3118
3410
|
}
|
|
3411
|
+
/**
|
|
3412
|
+
* Open a CRDT field for collaborative editing.
|
|
3413
|
+
* Returns a CrdtField with a LoroDoc that can be bound to any editor.
|
|
3414
|
+
* Also starts a LIVE SELECT on _00_crdt for real-time sync.
|
|
3415
|
+
*/
|
|
3416
|
+
async openCrdtField(table, recordId, field, fallbackText) {
|
|
3417
|
+
return this.crdtManager.open(table, recordId, field, fallbackText);
|
|
3418
|
+
}
|
|
3419
|
+
/**
|
|
3420
|
+
* Close a CRDT field when editing is done.
|
|
3421
|
+
*/
|
|
3422
|
+
closeCrdtField(table, recordId, field) {
|
|
3423
|
+
this.crdtManager.close(table, recordId, field);
|
|
3424
|
+
}
|
|
3119
3425
|
deauthenticate() {
|
|
3120
3426
|
return this.remote.getClient().invalidate();
|
|
3121
3427
|
}
|
|
@@ -3159,16 +3465,16 @@ var SpookyClient = class {
|
|
|
3159
3465
|
}
|
|
3160
3466
|
persistClientId(id) {
|
|
3161
3467
|
try {
|
|
3162
|
-
this.persistenceClient.set("
|
|
3468
|
+
this.persistenceClient.set("sp00ky_client_id", id);
|
|
3163
3469
|
} catch (e) {
|
|
3164
3470
|
this.logger.warn({
|
|
3165
3471
|
error: e,
|
|
3166
|
-
Category: "
|
|
3472
|
+
Category: "sp00ky-client::Sp00kyClient::persistClientId"
|
|
3167
3473
|
}, "Failed to persist client ID");
|
|
3168
3474
|
}
|
|
3169
3475
|
}
|
|
3170
3476
|
async loadOrGenerateClientId() {
|
|
3171
|
-
const clientId = await this.persistenceClient.get("
|
|
3477
|
+
const clientId = await this.persistenceClient.get("sp00ky_client_id");
|
|
3172
3478
|
if (clientId) return clientId;
|
|
3173
3479
|
const newId = generateId();
|
|
3174
3480
|
await this.persistClientId(newId);
|
|
@@ -3177,4 +3483,4 @@ var SpookyClient = class {
|
|
|
3177
3483
|
};
|
|
3178
3484
|
|
|
3179
3485
|
//#endregion
|
|
3180
|
-
export { AuthEventTypes, AuthService, BucketHandle,
|
|
3486
|
+
export { AuthEventTypes, AuthService, BucketHandle, CURSOR_COLORS, CrdtField, CrdtManager, Sp00kyClient, createAuthEventSystem, cursorColorFromName, fileToUint8Array, textToHtml };
|