@spooky-sync/core 0.0.1-canary.24 → 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 +32 -20
- 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/src/modules/sync/sync.ts +12 -0
- package/src/modules/sync/utils.ts +8 -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,
|
|
@@ -1544,14 +1547,12 @@ function diffRecordVersionArray(local, remote) {
|
|
|
1544
1547
|
};
|
|
1545
1548
|
}
|
|
1546
1549
|
function createDiffFromDbOp(op, recordId, version, versions) {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
};
|
|
1554
|
-
}
|
|
1550
|
+
const old = versions?.find((record) => record[0] === encodeRecordId(recordId));
|
|
1551
|
+
if (old && old[1] >= version) return {
|
|
1552
|
+
added: [],
|
|
1553
|
+
updated: [],
|
|
1554
|
+
removed: []
|
|
1555
|
+
};
|
|
1555
1556
|
if (op === "CREATE") return {
|
|
1556
1557
|
added: [{
|
|
1557
1558
|
id: recordId,
|
|
@@ -1606,28 +1607,31 @@ var SyncEngine = class {
|
|
|
1606
1607
|
Category: "spooky-client::SyncEngine::syncRecords"
|
|
1607
1608
|
}, "SyncEngine.syncRecords diff");
|
|
1608
1609
|
if (removed.length > 0) await this.handleRemovedRecords(removed);
|
|
1609
|
-
const
|
|
1610
|
+
const toFetch = [...added, ...updated];
|
|
1611
|
+
const idsToFetch = toFetch.map((x) => x.id);
|
|
1610
1612
|
if (idsToFetch.length === 0) return;
|
|
1611
|
-
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 });
|
|
1612
1616
|
const cacheBatch = [];
|
|
1613
|
-
for (const
|
|
1614
|
-
if (!
|
|
1617
|
+
for (const record of remoteResults) {
|
|
1618
|
+
if (!record?.id) {
|
|
1615
1619
|
this.logger.warn({
|
|
1616
|
-
|
|
1620
|
+
record,
|
|
1617
1621
|
idsToFetch,
|
|
1618
1622
|
Category: "spooky-client::SyncEngine::syncRecords"
|
|
1619
1623
|
}, "Remote record has no id (possibly deleted). Skipping record");
|
|
1620
1624
|
continue;
|
|
1621
1625
|
}
|
|
1622
|
-
const { spooky_rv, ...record } = result;
|
|
1623
1626
|
const fullId = encodeRecordId(record.id);
|
|
1624
1627
|
const table = record.id.table.toString();
|
|
1625
1628
|
const isAdded = added.some((item) => encodeRecordId(item.id) === fullId);
|
|
1629
|
+
const version = versionMap.get(fullId) ?? 0;
|
|
1626
1630
|
const localVersion = this.cache.lookup(fullId);
|
|
1627
|
-
if (localVersion &&
|
|
1631
|
+
if (localVersion && version <= localVersion) {
|
|
1628
1632
|
this.logger.info({
|
|
1629
1633
|
recordId: fullId,
|
|
1630
|
-
version
|
|
1634
|
+
version,
|
|
1631
1635
|
localVersion,
|
|
1632
1636
|
Category: "spooky-client::SyncEngine::syncRecords"
|
|
1633
1637
|
}, "Local version is higher than remote version. Skipping record");
|
|
@@ -1639,7 +1643,7 @@ var SyncEngine = class {
|
|
|
1639
1643
|
table,
|
|
1640
1644
|
op: isAdded ? "CREATE" : "UPDATE",
|
|
1641
1645
|
record: cleanedRecord,
|
|
1642
|
-
version
|
|
1646
|
+
version
|
|
1643
1647
|
});
|
|
1644
1648
|
}
|
|
1645
1649
|
if (cacheBatch.length > 0) await this.cache.saveBatch(cacheBatch);
|
|
@@ -1821,6 +1825,14 @@ var SpookySync = class {
|
|
|
1821
1825
|
});
|
|
1822
1826
|
}
|
|
1823
1827
|
async handleRemoteListRefChange(action, queryId, recordId, version) {
|
|
1828
|
+
if (action === "DELETE") {
|
|
1829
|
+
this.logger.debug({
|
|
1830
|
+
queryId: queryId.toString(),
|
|
1831
|
+
recordId: recordId.toString(),
|
|
1832
|
+
Category: "spooky-client::SpookySync::handleRemoteListRefChange"
|
|
1833
|
+
}, "Ignoring DELETE on list_ref — should not happen");
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1824
1836
|
const existing = this.dataModule.getQueryById(queryId);
|
|
1825
1837
|
if (!existing) {
|
|
1826
1838
|
this.logger.warn({
|
|
@@ -2749,7 +2761,7 @@ var CacheModule = class {
|
|
|
2749
2761
|
/**
|
|
2750
2762
|
* Delete a record from local DB and ingest deletion into DBSP
|
|
2751
2763
|
*/
|
|
2752
|
-
async delete(table, id, skipDbDelete = false) {
|
|
2764
|
+
async delete(table, id, skipDbDelete = false, recordData = {}) {
|
|
2753
2765
|
this.logger.debug({
|
|
2754
2766
|
table,
|
|
2755
2767
|
id,
|
|
@@ -2758,7 +2770,7 @@ var CacheModule = class {
|
|
|
2758
2770
|
try {
|
|
2759
2771
|
if (!skipDbDelete) await this.local.query("DELETE $id", { id: parseRecordIdString(id) });
|
|
2760
2772
|
delete this.versionLookups[id];
|
|
2761
|
-
|
|
2773
|
+
this.streamProcessor.ingest(table, "DELETE", id, recordData);
|
|
2762
2774
|
this.logger.debug({
|
|
2763
2775
|
table,
|
|
2764
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
|
|
package/src/modules/sync/sync.ts
CHANGED
|
@@ -126,6 +126,18 @@ export class SpookySync<S extends SchemaStructure> {
|
|
|
126
126
|
recordId: RecordId,
|
|
127
127
|
version: number
|
|
128
128
|
) {
|
|
129
|
+
if (action === 'DELETE') {
|
|
130
|
+
this.logger.debug(
|
|
131
|
+
{
|
|
132
|
+
queryId: queryId.toString(),
|
|
133
|
+
recordId: recordId.toString(),
|
|
134
|
+
Category: 'spooky-client::SpookySync::handleRemoteListRefChange',
|
|
135
|
+
},
|
|
136
|
+
'Ignoring DELETE on list_ref — should not happen'
|
|
137
|
+
);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
129
141
|
const existing = this.dataModule.getQueryById(queryId);
|
|
130
142
|
|
|
131
143
|
if (!existing) {
|
|
@@ -135,17 +135,14 @@ export function createDiffFromDbOp(
|
|
|
135
135
|
version: number,
|
|
136
136
|
versions?: RecordVersionArray
|
|
137
137
|
): RecordVersionDiff {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
removed: [],
|
|
147
|
-
};
|
|
148
|
-
}
|
|
138
|
+
const old = versions?.find((record) => record[0] === encodeRecordId(recordId));
|
|
139
|
+
|
|
140
|
+
if (old && old[1] >= version) {
|
|
141
|
+
return {
|
|
142
|
+
added: [],
|
|
143
|
+
updated: [],
|
|
144
|
+
removed: [],
|
|
145
|
+
};
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
if (op === 'CREATE') {
|