@spooky-sync/core 0.0.1-canary.25 → 0.0.1-canary.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +18 -12
- package/package.json +1 -1
- package/src/modules/cache/index.ts +3 -3
- package/src/modules/data/index.ts +16 -1
- package/src/modules/sync/engine.ts +18 -11
package/dist/index.js
CHANGED
|
@@ -534,12 +534,15 @@ var DataModule = class {
|
|
|
534
534
|
if (!this.schema.tables.find((t) => t.name === tableName)) throw new Error(`Table ${tableName} not found`);
|
|
535
535
|
const rid = parseRecordIdString(id);
|
|
536
536
|
const mutationId = parseRecordIdString(`_spooky_pending_mutations:${Date.now()}`);
|
|
537
|
+
const [beforeRecords] = await this.local.query("SELECT * FROM ONLY $id", { id: rid });
|
|
538
|
+
const beforeRecord = beforeRecords ?? {};
|
|
537
539
|
const query = surql.seal(surql.tx([surql.delete("id"), surql.createMutation("delete", "mid", "id")]));
|
|
538
540
|
await withRetry(this.logger, () => this.local.execute(query, {
|
|
539
541
|
id: rid,
|
|
540
542
|
mid: mutationId
|
|
541
543
|
}));
|
|
542
|
-
await this.cache.delete(table, id, true);
|
|
544
|
+
await this.cache.delete(table, id, true, beforeRecord);
|
|
545
|
+
for (const [queryHash, queryState] of this.activeQueries) if (queryState.config.tableName === tableName) await this.notifyQuerySynced(queryHash);
|
|
543
546
|
const mutationEvent = {
|
|
544
547
|
type: "delete",
|
|
545
548
|
mutation_id: mutationId,
|
|
@@ -1604,28 +1607,31 @@ var SyncEngine = class {
|
|
|
1604
1607
|
Category: "spooky-client::SyncEngine::syncRecords"
|
|
1605
1608
|
}, "SyncEngine.syncRecords diff");
|
|
1606
1609
|
if (removed.length > 0) await this.handleRemovedRecords(removed);
|
|
1607
|
-
const
|
|
1610
|
+
const toFetch = [...added, ...updated];
|
|
1611
|
+
const idsToFetch = toFetch.map((x) => x.id);
|
|
1608
1612
|
if (idsToFetch.length === 0) return;
|
|
1609
|
-
const
|
|
1613
|
+
const versionMap = /* @__PURE__ */ new Map();
|
|
1614
|
+
for (const item of toFetch) versionMap.set(encodeRecordId(item.id), item.version);
|
|
1615
|
+
const [remoteResults] = await this.remote.query("SELECT * FROM $idsToFetch", { idsToFetch });
|
|
1610
1616
|
const cacheBatch = [];
|
|
1611
|
-
for (const
|
|
1612
|
-
if (!
|
|
1617
|
+
for (const record of remoteResults) {
|
|
1618
|
+
if (!record?.id) {
|
|
1613
1619
|
this.logger.warn({
|
|
1614
|
-
|
|
1620
|
+
record,
|
|
1615
1621
|
idsToFetch,
|
|
1616
1622
|
Category: "spooky-client::SyncEngine::syncRecords"
|
|
1617
1623
|
}, "Remote record has no id (possibly deleted). Skipping record");
|
|
1618
1624
|
continue;
|
|
1619
1625
|
}
|
|
1620
|
-
const { spooky_rv, ...record } = result;
|
|
1621
1626
|
const fullId = encodeRecordId(record.id);
|
|
1622
1627
|
const table = record.id.table.toString();
|
|
1623
1628
|
const isAdded = added.some((item) => encodeRecordId(item.id) === fullId);
|
|
1629
|
+
const version = versionMap.get(fullId) ?? 0;
|
|
1624
1630
|
const localVersion = this.cache.lookup(fullId);
|
|
1625
|
-
if (localVersion &&
|
|
1631
|
+
if (localVersion && version <= localVersion) {
|
|
1626
1632
|
this.logger.info({
|
|
1627
1633
|
recordId: fullId,
|
|
1628
|
-
version
|
|
1634
|
+
version,
|
|
1629
1635
|
localVersion,
|
|
1630
1636
|
Category: "spooky-client::SyncEngine::syncRecords"
|
|
1631
1637
|
}, "Local version is higher than remote version. Skipping record");
|
|
@@ -1637,7 +1643,7 @@ var SyncEngine = class {
|
|
|
1637
1643
|
table,
|
|
1638
1644
|
op: isAdded ? "CREATE" : "UPDATE",
|
|
1639
1645
|
record: cleanedRecord,
|
|
1640
|
-
version
|
|
1646
|
+
version
|
|
1641
1647
|
});
|
|
1642
1648
|
}
|
|
1643
1649
|
if (cacheBatch.length > 0) await this.cache.saveBatch(cacheBatch);
|
|
@@ -2755,7 +2761,7 @@ var CacheModule = class {
|
|
|
2755
2761
|
/**
|
|
2756
2762
|
* Delete a record from local DB and ingest deletion into DBSP
|
|
2757
2763
|
*/
|
|
2758
|
-
async delete(table, id, skipDbDelete = false) {
|
|
2764
|
+
async delete(table, id, skipDbDelete = false, recordData = {}) {
|
|
2759
2765
|
this.logger.debug({
|
|
2760
2766
|
table,
|
|
2761
2767
|
id,
|
|
@@ -2764,7 +2770,7 @@ var CacheModule = class {
|
|
|
2764
2770
|
try {
|
|
2765
2771
|
if (!skipDbDelete) await this.local.query("DELETE $id", { id: parseRecordIdString(id) });
|
|
2766
2772
|
delete this.versionLookups[id];
|
|
2767
|
-
|
|
2773
|
+
this.streamProcessor.ingest(table, "DELETE", id, recordData);
|
|
2768
2774
|
this.logger.debug({
|
|
2769
2775
|
table,
|
|
2770
2776
|
id,
|
package/package.json
CHANGED
|
@@ -137,7 +137,7 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
137
137
|
/**
|
|
138
138
|
* Delete a record from local DB and ingest deletion into DBSP
|
|
139
139
|
*/
|
|
140
|
-
async delete(table: string, id: string, skipDbDelete: boolean = false): Promise<void> {
|
|
140
|
+
async delete(table: string, id: string, skipDbDelete: boolean = false, recordData: Record<string, any> = {}): Promise<void> {
|
|
141
141
|
this.logger.debug(
|
|
142
142
|
{ table, id, Category: 'spooky-client::CacheModule::delete' },
|
|
143
143
|
'Deleting record'
|
|
@@ -149,9 +149,9 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
149
149
|
await this.local.query('DELETE $id', { id: parseRecordIdString(id) });
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
// 2. Ingest deletion into DBSP
|
|
152
|
+
// 2. Ingest deletion into DBSP (pass record data so predicates can be matched)
|
|
153
153
|
delete this.versionLookups[id];
|
|
154
|
-
|
|
154
|
+
this.streamProcessor.ingest(table, 'DELETE', id, recordData);
|
|
155
155
|
|
|
156
156
|
this.logger.debug(
|
|
157
157
|
{ table, id, Category: 'spooky-client::CacheModule::delete' },
|
|
@@ -549,12 +549,27 @@ export class DataModule<S extends SchemaStructure> {
|
|
|
549
549
|
const rid = parseRecordIdString(id);
|
|
550
550
|
const mutationId = parseRecordIdString(`_spooky_pending_mutations:${Date.now()}`);
|
|
551
551
|
|
|
552
|
+
// Fetch the record before deleting so DBSP can match it against query predicates
|
|
553
|
+
const [beforeRecords] = await this.local.query<[Record<string, any>[]]>(
|
|
554
|
+
'SELECT * FROM ONLY $id',
|
|
555
|
+
{ id: rid }
|
|
556
|
+
);
|
|
557
|
+
const beforeRecord = beforeRecords ?? {};
|
|
558
|
+
|
|
552
559
|
const query = surql.seal<void>(
|
|
553
560
|
surql.tx([surql.delete('id'), surql.createMutation('delete', 'mid', 'id')])
|
|
554
561
|
);
|
|
555
562
|
|
|
556
563
|
await withRetry(this.logger, () => this.local.execute(query, { id: rid, mid: mutationId }));
|
|
557
|
-
await this.cache.delete(table, id, true);
|
|
564
|
+
await this.cache.delete(table, id, true, beforeRecord);
|
|
565
|
+
|
|
566
|
+
// DBSP may not emit view updates for DELETE ops —
|
|
567
|
+
// manually notify all queries that reference this table
|
|
568
|
+
for (const [queryHash, queryState] of this.activeQueries) {
|
|
569
|
+
if (queryState.config.tableName === tableName) {
|
|
570
|
+
await this.notifyQuerySynced(queryHash);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
558
573
|
|
|
559
574
|
// Emit mutation event
|
|
560
575
|
const mutationEvent: DeleteEvent = {
|
|
@@ -57,21 +57,28 @@ export class SyncEngine {
|
|
|
57
57
|
return;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// Build a version map from the diff (versions come from _spooky_list_ref)
|
|
61
|
+
const versionMap = new Map<string, number>();
|
|
62
|
+
for (const item of toFetch) {
|
|
63
|
+
versionMap.set(encodeRecordId(item.id), item.version);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Fetch records from remote — avoid SELECT *, <subquery> FROM $param
|
|
67
|
+
// pattern which drops the * fields in SurrealDB v3 (known bug).
|
|
68
|
+
// Versions are already known from the diff's list_ref data.
|
|
69
|
+
const [remoteResults] = await this.remote.query<[RecordWithId[]]>(
|
|
70
|
+
'SELECT * FROM $idsToFetch',
|
|
64
71
|
{ idsToFetch }
|
|
65
72
|
);
|
|
66
73
|
|
|
67
74
|
// Prepare batch for cache (which handles both DB and DBSP)
|
|
68
75
|
const cacheBatch: CacheRecord[] = [];
|
|
69
76
|
|
|
70
|
-
for (const
|
|
71
|
-
if (!
|
|
77
|
+
for (const record of remoteResults) {
|
|
78
|
+
if (!record?.id) {
|
|
72
79
|
this.logger.warn(
|
|
73
80
|
{
|
|
74
|
-
|
|
81
|
+
record,
|
|
75
82
|
idsToFetch,
|
|
76
83
|
Category: 'spooky-client::SyncEngine::syncRecords',
|
|
77
84
|
},
|
|
@@ -79,17 +86,17 @@ export class SyncEngine {
|
|
|
79
86
|
);
|
|
80
87
|
continue;
|
|
81
88
|
}
|
|
82
|
-
const { spooky_rv, ...record } = result;
|
|
83
89
|
const fullId = encodeRecordId(record.id);
|
|
84
90
|
const table = record.id.table.toString();
|
|
85
91
|
const isAdded = added.some((item) => encodeRecordId(item.id) === fullId);
|
|
92
|
+
const version = versionMap.get(fullId) ?? 0;
|
|
86
93
|
|
|
87
94
|
const localVersion = this.cache.lookup(fullId);
|
|
88
|
-
if (localVersion &&
|
|
95
|
+
if (localVersion && version <= localVersion) {
|
|
89
96
|
this.logger.info(
|
|
90
97
|
{
|
|
91
98
|
recordId: fullId,
|
|
92
|
-
version
|
|
99
|
+
version,
|
|
93
100
|
localVersion,
|
|
94
101
|
Category: 'spooky-client::SyncEngine::syncRecords',
|
|
95
102
|
},
|
|
@@ -106,7 +113,7 @@ export class SyncEngine {
|
|
|
106
113
|
table,
|
|
107
114
|
op: isAdded ? 'CREATE' : 'UPDATE',
|
|
108
115
|
record: cleanedRecord as RecordWithId,
|
|
109
|
-
version
|
|
116
|
+
version,
|
|
110
117
|
});
|
|
111
118
|
}
|
|
112
119
|
|