@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 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 idsToFetch = [...added, ...updated].map((x) => x.id);
1610
+ const toFetch = [...added, ...updated];
1611
+ const idsToFetch = toFetch.map((x) => x.id);
1608
1612
  if (idsToFetch.length === 0) return;
1609
- 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 });
1610
1616
  const cacheBatch = [];
1611
- for (const result of remoteResults) {
1612
- if (!result?.id) {
1617
+ for (const record of remoteResults) {
1618
+ if (!record?.id) {
1613
1619
  this.logger.warn({
1614
- result,
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 && spooky_rv <= localVersion) {
1631
+ if (localVersion && version <= localVersion) {
1626
1632
  this.logger.info({
1627
1633
  recordId: fullId,
1628
- version: spooky_rv,
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: spooky_rv
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
- await this.streamProcessor.ingest(table, "DELETE", id, {});
2773
+ this.streamProcessor.ingest(table, "DELETE", id, recordData);
2768
2774
  this.logger.debug({
2769
2775
  table,
2770
2776
  id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spooky-sync/core",
3
- "version": "0.0.1-canary.25",
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