@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 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
- if (op !== "DELETE") {
1548
- const old = versions?.find((record) => record[0] === encodeRecordId(recordId));
1549
- if (old && old[1] >= version) return {
1550
- added: [],
1551
- updated: [],
1552
- removed: []
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 idsToFetch = [...added, ...updated].map((x) => x.id);
1610
+ const toFetch = [...added, ...updated];
1611
+ const idsToFetch = toFetch.map((x) => x.id);
1610
1612
  if (idsToFetch.length === 0) return;
1611
- const [remoteResults] = await this.remote.query("SELECT *, (SELECT version FROM ONLY _spooky_version WHERE record_id = $parent.id)['version'] as spooky_rv FROM $idsToFetch", { idsToFetch });
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 result of remoteResults) {
1614
- if (!result?.id) {
1617
+ for (const record of remoteResults) {
1618
+ if (!record?.id) {
1615
1619
  this.logger.warn({
1616
- result,
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 && spooky_rv <= localVersion) {
1631
+ if (localVersion && version <= localVersion) {
1628
1632
  this.logger.info({
1629
1633
  recordId: fullId,
1630
- version: spooky_rv,
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: spooky_rv
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
- await this.streamProcessor.ingest(table, "DELETE", id, {});
2773
+ this.streamProcessor.ingest(table, "DELETE", id, recordData);
2762
2774
  this.logger.debug({
2763
2775
  table,
2764
2776
  id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.24",
3
+ "version": "0.0.1-canary.26",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -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
- await this.streamProcessor.ingest(table, 'DELETE', id, {});
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
- const [remoteResults] = await this.remote.query<
61
- [(RecordWithId & { spooky_rv: number })[]]
62
- >(
63
- "SELECT *, (SELECT version FROM ONLY _spooky_version WHERE record_id = $parent.id)['version'] as spooky_rv FROM $idsToFetch",
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 result of remoteResults) {
71
- if (!result?.id) {
77
+ for (const record of remoteResults) {
78
+ if (!record?.id) {
72
79
  this.logger.warn(
73
80
  {
74
- result,
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 && spooky_rv <= localVersion) {
95
+ if (localVersion && version <= localVersion) {
89
96
  this.logger.info(
90
97
  {
91
98
  recordId: fullId,
92
- version: spooky_rv,
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: spooky_rv,
116
+ version,
110
117
  });
111
118
  }
112
119
 
@@ -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
- // Version guard: skip stale CREATE/UPDATE, but always process DELETE
139
- if (op !== 'DELETE') {
140
- const old = versions?.find((record) => record[0] === encodeRecordId(recordId));
141
-
142
- if (old && old[1] >= version) {
143
- return {
144
- added: [],
145
- updated: [],
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') {