@spooky-sync/core 0.0.1-canary.4 → 0.0.1-canary.41

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.
Files changed (45) hide show
  1. package/dist/index.d.ts +18 -367
  2. package/dist/index.js +268 -287
  3. package/dist/otel/index.d.ts +21 -0
  4. package/dist/otel/index.js +86 -0
  5. package/dist/types.d.ts +361 -0
  6. package/package.json +34 -5
  7. package/skills/sp00ky-core/SKILL.md +258 -0
  8. package/skills/sp00ky-core/references/auth.md +98 -0
  9. package/skills/sp00ky-core/references/config.md +76 -0
  10. package/src/events/events.test.ts +2 -1
  11. package/src/index.ts +1 -1
  12. package/src/modules/auth/events/index.ts +2 -1
  13. package/src/modules/auth/index.ts +17 -20
  14. package/src/modules/cache/index.ts +23 -23
  15. package/src/modules/cache/types.ts +2 -2
  16. package/src/modules/data/index.ts +61 -43
  17. package/src/modules/devtools/index.ts +23 -20
  18. package/src/modules/sync/engine.ts +31 -21
  19. package/src/modules/sync/events/index.ts +3 -2
  20. package/src/modules/sync/queue/queue-down.ts +5 -4
  21. package/src/modules/sync/queue/queue-up.ts +14 -13
  22. package/src/modules/sync/scheduler.ts +2 -2
  23. package/src/modules/sync/sync.ts +47 -35
  24. package/src/modules/sync/utils.test.ts +2 -2
  25. package/src/modules/sync/utils.ts +15 -17
  26. package/src/otel/index.ts +127 -0
  27. package/src/services/database/database.ts +11 -11
  28. package/src/services/database/events/index.ts +2 -1
  29. package/src/services/database/local-migrator.ts +21 -21
  30. package/src/services/database/local.ts +16 -15
  31. package/src/services/database/remote.ts +13 -13
  32. package/src/services/logger/index.ts +6 -101
  33. package/src/services/persistence/localstorage.ts +2 -2
  34. package/src/services/persistence/resilient.ts +34 -0
  35. package/src/services/persistence/surrealdb.ts +9 -9
  36. package/src/services/stream-processor/index.ts +32 -31
  37. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  38. package/src/services/stream-processor/wasm-types.ts +2 -2
  39. package/src/{spooky.ts → sp00ky.ts} +40 -38
  40. package/src/types.ts +18 -11
  41. package/src/utils/index.ts +10 -10
  42. package/src/utils/parser.ts +2 -1
  43. package/src/utils/surql.ts +15 -15
  44. package/src/utils/withRetry.test.ts +1 -1
  45. package/tsdown.config.ts +1 -1
@@ -1,13 +1,13 @@
1
- import { LocalDatabaseService } from '../../services/database/index';
2
- import {
1
+ import type { LocalDatabaseService } from '../../services/database/index';
2
+ import type {
3
3
  StreamProcessorService,
4
4
  StreamUpdate,
5
5
  StreamUpdateReceiver,
6
6
  } from '../../services/stream-processor/index';
7
- import { Logger } from '../../services/logger/index';
7
+ import type { Logger } from '../../services/logger/index';
8
8
  import { parseRecordIdString, encodeRecordId, surql } from '../../utils/index';
9
- import { CacheRecord, QueryConfig } from './types';
10
- import { RecordVersionArray } from '../../types';
9
+ import type { CacheRecord, QueryConfig } from './types';
10
+ import type { RecordVersionArray } from '../../types';
11
11
 
12
12
  export * from './types';
13
13
 
@@ -43,7 +43,7 @@ export class CacheModule implements StreamUpdateReceiver {
43
43
  {
44
44
  queryHash: update.queryHash,
45
45
  arrayLength: update.localArray?.length,
46
- Category: 'spooky-client::CacheModule::onStreamUpdate',
46
+ Category: 'sp00ky-client::CacheModule::onStreamUpdate',
47
47
  },
48
48
  'Stream update received'
49
49
  );
@@ -73,7 +73,7 @@ export class CacheModule implements StreamUpdateReceiver {
73
73
  this.logger.debug(
74
74
  {
75
75
  count: records.length,
76
- Category: 'spooky-client::CacheModule::saveBatch',
76
+ Category: 'sp00ky-client::CacheModule::saveBatch',
77
77
  },
78
78
  'Saving record batch'
79
79
  );
@@ -85,7 +85,7 @@ export class CacheModule implements StreamUpdateReceiver {
85
85
  ...record,
86
86
  record: {
87
87
  ...record.record,
88
- spooky_rv: record.version,
88
+ _00_rv: record.version,
89
89
  },
90
90
  };
91
91
  });
@@ -114,7 +114,7 @@ export class CacheModule implements StreamUpdateReceiver {
114
114
  await this.local.execute(query, params);
115
115
  }
116
116
 
117
- // 2. Batch ingest into DBSP (use populatedRecords which has spooky_rv set)
117
+ // 2. Batch ingest into DBSP (use populatedRecords which has _00_rv set)
118
118
  for (const record of populatedRecords) {
119
119
  const recordId = encodeRecordId(record.record.id);
120
120
  this.versionLookups[recordId] = record.version;
@@ -122,12 +122,12 @@ export class CacheModule implements StreamUpdateReceiver {
122
122
  }
123
123
 
124
124
  this.logger.debug(
125
- { count: records.length, Category: 'spooky-client::CacheModule::saveBatch' },
125
+ { count: records.length, Category: 'sp00ky-client::CacheModule::saveBatch' },
126
126
  'Batch saved successfully'
127
127
  );
128
128
  } catch (err) {
129
129
  this.logger.error(
130
- { err, count: records.length, Category: 'spooky-client::CacheModule::saveBatch' },
130
+ { err, count: records.length, Category: 'sp00ky-client::CacheModule::saveBatch' },
131
131
  'Failed to save batch'
132
132
  );
133
133
  throw err;
@@ -137,9 +137,9 @@ 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
- { table, id, Category: 'spooky-client::CacheModule::delete' },
142
+ { table, id, Category: 'sp00ky-client::CacheModule::delete' },
143
143
  'Deleting record'
144
144
  );
145
145
 
@@ -149,17 +149,17 @@ 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
- { table, id, Category: 'spooky-client::CacheModule::delete' },
157
+ { table, id, Category: 'sp00ky-client::CacheModule::delete' },
158
158
  'Record deleted successfully'
159
159
  );
160
160
  } catch (err) {
161
161
  this.logger.error(
162
- { err, table, id, Category: 'spooky-client::CacheModule::delete' },
162
+ { err, table, id, Category: 'sp00ky-client::CacheModule::delete' },
163
163
  'Failed to delete record'
164
164
  );
165
165
  throw err;
@@ -175,7 +175,7 @@ export class CacheModule implements StreamUpdateReceiver {
175
175
  {
176
176
  queryHash: config.queryHash,
177
177
  surql: config.surql,
178
- Category: 'spooky-client::CacheModule::registerQuery',
178
+ Category: 'sp00ky-client::CacheModule::registerQuery',
179
179
  },
180
180
  'Registering query'
181
181
  );
@@ -202,7 +202,7 @@ export class CacheModule implements StreamUpdateReceiver {
202
202
  {
203
203
  queryHash: config.queryHash,
204
204
  arrayLength: update.localArray?.length,
205
- Category: 'spooky-client::CacheModule::registerQuery',
205
+ Category: 'sp00ky-client::CacheModule::registerQuery',
206
206
  },
207
207
  'Query registered successfully'
208
208
  );
@@ -210,7 +210,7 @@ export class CacheModule implements StreamUpdateReceiver {
210
210
  return { localArray: update.localArray };
211
211
  } catch (err) {
212
212
  this.logger.error(
213
- { err, queryHash: config.queryHash, Category: 'spooky-client::CacheModule::registerQuery' },
213
+ { err, queryHash: config.queryHash, Category: 'sp00ky-client::CacheModule::registerQuery' },
214
214
  'Failed to register query'
215
215
  );
216
216
  throw err;
@@ -222,18 +222,18 @@ export class CacheModule implements StreamUpdateReceiver {
222
222
  */
223
223
  unregisterQuery(queryHash: string): void {
224
224
  this.logger.debug(
225
- { queryHash, Category: 'spooky-client::CacheModule::unregisterQuery' },
225
+ { queryHash, Category: 'sp00ky-client::CacheModule::unregisterQuery' },
226
226
  'Unregistering query'
227
227
  );
228
228
  try {
229
229
  this.streamProcessor.unregisterQueryPlan(queryHash);
230
230
  this.logger.debug(
231
- { queryHash, Category: 'spooky-client::CacheModule::unregisterQuery' },
231
+ { queryHash, Category: 'sp00ky-client::CacheModule::unregisterQuery' },
232
232
  'Query unregistered successfully'
233
233
  );
234
234
  } catch (err) {
235
235
  this.logger.error(
236
- { err, queryHash, Category: 'spooky-client::CacheModule::unregisterQuery' },
236
+ { err, queryHash, Category: 'sp00ky-client::CacheModule::unregisterQuery' },
237
237
  'Failed to unregister query'
238
238
  );
239
239
  }
@@ -1,5 +1,5 @@
1
- import { RecordId, Duration } from 'surrealdb';
2
- import { QueryTimeToLive, RecordVersionArray } from '../../types';
1
+ import type { RecordId, Duration } from 'surrealdb';
2
+ import type { QueryTimeToLive } from '../../types';
3
3
 
4
4
  export type RecordWithId = Record<string, any> & { id: RecordId<string> };
5
5
 
@@ -1,17 +1,16 @@
1
1
  import { RecordId, Duration } from 'surrealdb';
2
- import {
2
+ import type {
3
3
  SchemaStructure,
4
4
  TableNames,
5
5
  BackendNames,
6
6
  BackendRoutes,
7
7
  RoutePayload,
8
8
  } from '@spooky-sync/query-builder';
9
- import { LocalDatabaseService } from '../../services/database/index';
10
- import { CacheModule, RecordWithId } from '../cache/index';
11
- import { Logger } from '../../services/logger/index';
12
- import { StreamUpdate } from '../../services/stream-processor/index';
13
- import {
14
- MutationEvent,
9
+ import type { LocalDatabaseService } from '../../services/database/index';
10
+ import type { CacheModule, RecordWithId } from '../cache/index';
11
+ import type { Logger } from '../../services/logger/index';
12
+ import type { StreamUpdate } from '../../services/stream-processor/index';
13
+ import type {
15
14
  QueryConfig,
16
15
  QueryHash,
17
16
  QueryState,
@@ -21,8 +20,7 @@ import {
21
20
  RecordVersionArray,
22
21
  QueryConfigRecord,
23
22
  UpdateOptions,
24
- RunOptions,
25
- } from '../../types';
23
+ RunOptions} from '../../types';
26
24
  import {
27
25
  parseRecordIdString,
28
26
  extractIdPart,
@@ -34,8 +32,8 @@ import {
34
32
  extractTablePart,
35
33
  generateId,
36
34
  } from '../../utils/index';
37
- import { CreateEvent, DeleteEvent, UpdateEvent } from '../sync/index';
38
- import { PushEventOptions } from '../../events/index';
35
+ import type { CreateEvent, DeleteEvent, UpdateEvent } from '../sync/index';
36
+ import type { PushEventOptions } from '../../events/index';
39
37
 
40
38
  /**
41
39
  * DataModule - Unified query and mutation management
@@ -62,7 +60,7 @@ export class DataModule<S extends SchemaStructure> {
62
60
  }
63
61
 
64
62
  async init(): Promise<void> {
65
- this.logger.info({ Category: 'spooky-client::DataModule::init' }, 'DataModule initialized');
63
+ this.logger.info({ Category: 'sp00ky-client::DataModule::init' }, 'DataModule initialized');
66
64
  }
67
65
 
68
66
  // ==================== QUERY MANAGEMENT ====================
@@ -78,15 +76,15 @@ export class DataModule<S extends SchemaStructure> {
78
76
  ): Promise<QueryHash> {
79
77
  const hash = await this.calculateHash({ surql: surqlString, params });
80
78
  this.logger.debug(
81
- { hash, Category: 'spooky-client::DataModule::query' },
79
+ { hash, Category: 'sp00ky-client::DataModule::query' },
82
80
  'Query Initialization: started'
83
81
  );
84
82
 
85
- const recordId = new RecordId('_spooky_query', hash);
83
+ const recordId = new RecordId('_00_query', hash);
86
84
 
87
85
  if (this.activeQueries.has(hash)) {
88
86
  this.logger.debug(
89
- { hash, Category: 'spooky-client::DataModule::query' },
87
+ { hash, Category: 'sp00ky-client::DataModule::query' },
90
88
  'Query Initialization: exists, returning'
91
89
  );
92
90
  return hash;
@@ -95,7 +93,7 @@ export class DataModule<S extends SchemaStructure> {
95
93
  // Another call is already creating this query — wait for it
96
94
  if (this.pendingQueries.has(hash)) {
97
95
  this.logger.debug(
98
- { hash, Category: 'spooky-client::DataModule::query' },
96
+ { hash, Category: 'sp00ky-client::DataModule::query' },
99
97
  'Query Initialization: pending, waiting for existing creation'
100
98
  );
101
99
  await this.pendingQueries.get(hash);
@@ -103,7 +101,7 @@ export class DataModule<S extends SchemaStructure> {
103
101
  }
104
102
 
105
103
  this.logger.debug(
106
- { hash, Category: 'spooky-client::DataModule::query' },
104
+ { hash, Category: 'sp00ky-client::DataModule::query' },
107
105
  'Query Initialization: not found, creating new query'
108
106
  );
109
107
 
@@ -173,6 +171,7 @@ export class DataModule<S extends SchemaStructure> {
173
171
  if (op === 'UPDATE') {
174
172
  // Clear existing timer if any
175
173
  if (this.debounceTimers.has(queryHash)) {
174
+ // oxlint-disable-next-line no-non-null-assertion -- guarded by .has() check above
176
175
  clearTimeout(this.debounceTimers.get(queryHash)!);
177
176
  }
178
177
 
@@ -194,7 +193,7 @@ export class DataModule<S extends SchemaStructure> {
194
193
  const queryState = this.activeQueries.get(queryHash);
195
194
  if (!queryState) {
196
195
  this.logger.warn(
197
- { queryHash, Category: 'spooky-client::DataModule::onStreamUpdate' },
196
+ { queryHash, Category: 'sp00ky-client::DataModule::onStreamUpdate' },
198
197
  'Received update for unknown query. Skipping...'
199
198
  );
200
199
  return;
@@ -221,7 +220,7 @@ export class DataModule<S extends SchemaStructure> {
221
220
  queryState.records = newRecords;
222
221
  if (prevJson === newJson) {
223
222
  this.logger.debug(
224
- { queryHash, Category: 'spooky-client::DataModule::onStreamUpdate' },
223
+ { queryHash, Category: 'sp00ky-client::DataModule::onStreamUpdate' },
225
224
  'Query records unchanged, skipping notification'
226
225
  );
227
226
  return;
@@ -241,13 +240,13 @@ export class DataModule<S extends SchemaStructure> {
241
240
  {
242
241
  queryHash,
243
242
  recordCount: records?.length,
244
- Category: 'spooky-client::DataModule::onStreamUpdate',
243
+ Category: 'sp00ky-client::DataModule::onStreamUpdate',
245
244
  },
246
245
  'Query updated from stream'
247
246
  );
248
247
  } catch (err) {
249
248
  this.logger.error(
250
- { err, queryHash, Category: 'spooky-client::DataModule::onStreamUpdate' },
249
+ { err, queryHash, Category: 'sp00ky-client::DataModule::onStreamUpdate' },
251
250
  'Failed to fetch records for stream update'
252
251
  );
253
252
  }
@@ -278,7 +277,7 @@ export class DataModule<S extends SchemaStructure> {
278
277
  const queryState = this.activeQueries.get(id);
279
278
  if (!queryState) {
280
279
  this.logger.warn(
281
- { id, Category: 'spooky-client::DataModule::updateQueryLocalArray' },
280
+ { id, Category: 'sp00ky-client::DataModule::updateQueryLocalArray' },
282
281
  'Query to update local array not found'
283
282
  );
284
283
  return;
@@ -294,7 +293,7 @@ export class DataModule<S extends SchemaStructure> {
294
293
  const queryState = this.getQueryByHash(hash);
295
294
  if (!queryState) {
296
295
  this.logger.warn(
297
- { hash, Category: 'spooky-client::DataModule::updateQueryRemoteArray' },
296
+ { hash, Category: 'sp00ky-client::DataModule::updateQueryRemoteArray' },
298
297
  'Query to update remote array not found'
299
298
  );
300
299
  return;
@@ -370,6 +369,10 @@ export class DataModule<S extends SchemaStructure> {
370
369
  retry_strategy: options?.retry_strategy ?? 'linear',
371
370
  };
372
371
 
372
+ if (options?.timeout != null) {
373
+ record.timeout = options.timeout;
374
+ }
375
+
373
376
  if (options?.assignedTo) {
374
377
  record.assigned_to = options.assignedTo;
375
378
  }
@@ -392,7 +395,7 @@ export class DataModule<S extends SchemaStructure> {
392
395
 
393
396
  const rid = parseRecordIdString(id);
394
397
  const params = parseParams(tableSchema.columns, data);
395
- const mutationId = parseRecordIdString(`_spooky_pending_mutations:${Date.now()}`);
398
+ const mutationId = parseRecordIdString(`_00_pending_mutations:${Date.now()}`);
396
399
 
397
400
  const dataKeys = Object.keys(params).map((key) => ({ key, variable: `data_${key}` }));
398
401
  const prefixedParams = Object.fromEntries(
@@ -441,7 +444,7 @@ export class DataModule<S extends SchemaStructure> {
441
444
  callback([mutationEvent]);
442
445
  }
443
446
 
444
- this.logger.debug({ id, Category: 'spooky-client::DataModule::create' }, 'Record created');
447
+ this.logger.debug({ id, Category: 'sp00ky-client::DataModule::create' }, 'Record created');
445
448
 
446
449
  return target;
447
450
  }
@@ -463,7 +466,7 @@ export class DataModule<S extends SchemaStructure> {
463
466
 
464
467
  const rid = parseRecordIdString(id);
465
468
  const params = parseParams(tableSchema.columns, data);
466
- const mutationId = parseRecordIdString(`_spooky_pending_mutations:${Date.now()}`);
469
+ const mutationId = parseRecordIdString(`_00_pending_mutations:${Date.now()}`);
467
470
 
468
471
  // Capture current record state before mutation for rollback support
469
472
  const [beforeRecord] = await withRetry(this.logger, () =>
@@ -472,7 +475,7 @@ export class DataModule<S extends SchemaStructure> {
472
475
 
473
476
  const query = surql.seal<{ target: T }>(
474
477
  surql.tx([
475
- surql.updateSet('id', [{ statement: 'spooky_rv += 1' }]),
478
+ surql.updateSet('id', [{ statement: '_00_rv += 1' }]),
476
479
  surql.let('updated', surql.updateMerge('id', 'data')),
477
480
  surql.createMutation('update', 'mid', 'id', 'data'),
478
481
  surql.returnObject([{ key: 'target', variable: 'updated' }]),
@@ -496,8 +499,8 @@ export class DataModule<S extends SchemaStructure> {
496
499
  updatedFields[key] = (target as Record<string, any>)[key];
497
500
  }
498
501
  }
499
- if ('spooky_rv' in (target as Record<string, any>)) {
500
- updatedFields.spooky_rv = (target as Record<string, any>).spooky_rv;
502
+ if ('_00_rv' in (target as Record<string, any>)) {
503
+ updatedFields._00_rv = (target as Record<string, any>)._00_rv;
501
504
  }
502
505
  this.replaceRecordInQueries(updatedFields);
503
506
 
@@ -509,7 +512,7 @@ export class DataModule<S extends SchemaStructure> {
509
512
  table: table,
510
513
  op: 'UPDATE',
511
514
  record: parsedRecord,
512
- version: target.spooky_rv as number,
515
+ version: target._00_rv as number,
513
516
  },
514
517
  true
515
518
  );
@@ -531,7 +534,7 @@ export class DataModule<S extends SchemaStructure> {
531
534
  callback([mutationEvent]);
532
535
  }
533
536
 
534
- this.logger.debug({ id, Category: 'spooky-client::DataModule::update' }, 'Record updated');
537
+ this.logger.debug({ id, Category: 'sp00ky-client::DataModule::update' }, 'Record updated');
535
538
 
536
539
  return target;
537
540
  }
@@ -547,14 +550,29 @@ export class DataModule<S extends SchemaStructure> {
547
550
  }
548
551
 
549
552
  const rid = parseRecordIdString(id);
550
- const mutationId = parseRecordIdString(`_spooky_pending_mutations:${Date.now()}`);
553
+ const mutationId = parseRecordIdString(`_00_pending_mutations:${Date.now()}`);
554
+
555
+ // Fetch the record before deleting so DBSP can match it against query predicates
556
+ const [beforeRecords] = await this.local.query<[Record<string, any>[]]>(
557
+ 'SELECT * FROM ONLY $id',
558
+ { id: rid }
559
+ );
560
+ const beforeRecord = beforeRecords ?? {};
551
561
 
552
562
  const query = surql.seal<void>(
553
563
  surql.tx([surql.delete('id'), surql.createMutation('delete', 'mid', 'id')])
554
564
  );
555
565
 
556
566
  await withRetry(this.logger, () => this.local.execute(query, { id: rid, mid: mutationId }));
557
- await this.cache.delete(table, id, true);
567
+ await this.cache.delete(table, id, true, beforeRecord);
568
+
569
+ // DBSP may not emit view updates for DELETE ops —
570
+ // manually notify all queries that reference this table
571
+ for (const [queryHash, queryState] of this.activeQueries) {
572
+ if (queryState.config.tableName === tableName) {
573
+ await this.notifyQuerySynced(queryHash);
574
+ }
575
+ }
558
576
 
559
577
  // Emit mutation event
560
578
  const mutationEvent: DeleteEvent = {
@@ -567,7 +585,7 @@ export class DataModule<S extends SchemaStructure> {
567
585
  callback([mutationEvent]);
568
586
  }
569
587
 
570
- this.logger.debug({ id, Category: 'spooky-client::DataModule::delete' }, 'Record deleted');
588
+ this.logger.debug({ id, Category: 'sp00ky-client::DataModule::delete' }, 'Record deleted');
571
589
  }
572
590
 
573
591
  // ==================== ROLLBACK METHODS ====================
@@ -586,12 +604,12 @@ export class DataModule<S extends SchemaStructure> {
586
604
  this.removeRecordFromQueries(recordId);
587
605
 
588
606
  this.logger.info(
589
- { id, tableName, Category: 'spooky-client::DataModule::rollbackCreate' },
607
+ { id, tableName, Category: 'sp00ky-client::DataModule::rollbackCreate' },
590
608
  'Rolled back optimistic create'
591
609
  );
592
610
  } catch (err) {
593
611
  this.logger.error(
594
- { err, id, tableName, Category: 'spooky-client::DataModule::rollbackCreate' },
612
+ { err, id, tableName, Category: 'sp00ky-client::DataModule::rollbackCreate' },
595
613
  'Failed to rollback create'
596
614
  );
597
615
  }
@@ -626,7 +644,7 @@ export class DataModule<S extends SchemaStructure> {
626
644
  table: tableName,
627
645
  op: 'UPDATE',
628
646
  record: parsedRecord,
629
- version: (beforeRecord.spooky_rv as number) || 1,
647
+ version: (beforeRecord._00_rv as number) || 1,
630
648
  },
631
649
  true
632
650
  );
@@ -635,12 +653,12 @@ export class DataModule<S extends SchemaStructure> {
635
653
  await this.replaceRecordInQueries(beforeRecord);
636
654
 
637
655
  this.logger.info(
638
- { id, tableName, Category: 'spooky-client::DataModule::rollbackUpdate' },
656
+ { id, tableName, Category: 'sp00ky-client::DataModule::rollbackUpdate' },
639
657
  'Rolled back optimistic update'
640
658
  );
641
659
  } catch (err) {
642
660
  this.logger.error(
643
- { err, id, tableName, Category: 'spooky-client::DataModule::rollbackUpdate' },
661
+ { err, id, tableName, Category: 'sp00ky-client::DataModule::rollbackUpdate' },
644
662
  'Failed to rollback update'
645
663
  );
646
664
  }
@@ -710,7 +728,7 @@ export class DataModule<S extends SchemaStructure> {
710
728
  hash,
711
729
  tableName,
712
730
  recordCount: queryState.records.length,
713
- Category: 'spooky-client::DataModule::query',
731
+ Category: 'sp00ky-client::DataModule::query',
714
732
  },
715
733
  'Query registered'
716
734
  );
@@ -772,7 +790,7 @@ export class DataModule<S extends SchemaStructure> {
772
790
  records = result || [];
773
791
  } catch (err) {
774
792
  this.logger.warn(
775
- { err, Category: 'spooky-client::DataModule::createNewQuery' },
793
+ { err, Category: 'sp00ky-client::DataModule::createNewQuery' },
776
794
  'Failed to load initial cached records'
777
795
  );
778
796
  }
@@ -804,7 +822,7 @@ export class DataModule<S extends SchemaStructure> {
804
822
  this.logger.debug(
805
823
  {
806
824
  id: encodeRecordId(queryState.config.id),
807
- Category: 'spooky-client::DataModule::startTTLHeartbeat',
825
+ Category: 'sp00ky-client::DataModule::startTTLHeartbeat',
808
826
  },
809
827
  'TTL heartbeat'
810
828
  );
@@ -851,7 +869,7 @@ export function parseUpdateOptions(
851
869
  const delay = options.debounced !== true ? (options.debounced?.delay ?? 200) : 200;
852
870
  const keyType = options.debounced !== true ? (options.debounced?.key ?? id) : id;
853
871
  const key =
854
- keyType === 'recordId_x_fields' ? `${id}::${Object.keys(data).sort().join('#')}` : id;
872
+ keyType === 'recordId_x_fields' ? `${id}::${Object.keys(data).toSorted().join('#')}` : id;
855
873
 
856
874
  pushEventOptions = {
857
875
  debounced: {
@@ -1,8 +1,8 @@
1
- import { LocalDatabaseService, RemoteDatabaseService } from '../../services/database/index';
2
- import { Logger } from '../../services/logger/index';
3
- import { SchemaStructure } from '@spooky-sync/query-builder';
1
+ import type { LocalDatabaseService, RemoteDatabaseService } from '../../services/database/index';
2
+ import type { Logger } from '../../services/logger/index';
3
+ import type { SchemaStructure } from '@spooky-sync/query-builder';
4
4
  import { RecordId } from 'surrealdb';
5
- import { StreamUpdate, StreamUpdateReceiver } from '../../services/stream-processor/index';
5
+ import type { StreamUpdate, StreamUpdateReceiver } from '../../services/stream-processor/index';
6
6
  import { encodeRecordId } from '../../utils/index';
7
7
 
8
8
  // DevTools interfaces (matching extension expectations)
@@ -13,8 +13,8 @@ export interface DevToolsEvent {
13
13
  payload: any;
14
14
  }
15
15
 
16
- import { DataModule } from '../data/index';
17
- import { AuthService } from '../auth/index';
16
+ import type { DataModule } from '../data/index';
17
+ import type { AuthService } from '../auth/index';
18
18
  import { AuthEventTypes } from '../auth/events/index';
19
19
 
20
20
  export class DevToolsService implements StreamUpdateReceiver {
@@ -37,7 +37,7 @@ export class DevToolsService implements StreamUpdateReceiver {
37
37
  this.notifyDevTools();
38
38
  });
39
39
 
40
- this.logger.debug({ Category: 'spooky-client::DevToolsService::init' }, 'Service initialized');
40
+ this.logger.debug({ Category: 'sp00ky-client::DevToolsService::init' }, 'Service initialized');
41
41
  }
42
42
 
43
43
  // Get active queries directly from DataManager (single source of truth)
@@ -70,7 +70,7 @@ export class DevToolsService implements StreamUpdateReceiver {
70
70
 
71
71
  public onQueryInitialized(payload: any) {
72
72
  this.logger.debug(
73
- { payload, Category: 'spooky-client::DevToolsService::onQueryInitialized' },
73
+ { payload, Category: 'sp00ky-client::DevToolsService::onQueryInitialized' },
74
74
  'QueryInitialized'
75
75
  );
76
76
  const queryHash = this.hashString(payload.queryId.toString());
@@ -87,7 +87,7 @@ export class DevToolsService implements StreamUpdateReceiver {
87
87
  this.logger.debug(
88
88
  {
89
89
  id: payload.queryId?.toString(),
90
- Category: 'spooky-client::DevToolsService::onQueryUpdated',
90
+ Category: 'sp00ky-client::DevToolsService::onQueryUpdated',
91
91
  },
92
92
  'QueryUpdated'
93
93
  );
@@ -102,7 +102,7 @@ export class DevToolsService implements StreamUpdateReceiver {
102
102
 
103
103
  public onStreamUpdate(update: StreamUpdate) {
104
104
  this.logger.debug(
105
- { update, Category: 'spooky-client::DevToolsService::onStreamUpdate' },
105
+ { update, Category: 'sp00ky-client::DevToolsService::onStreamUpdate' },
106
106
  'StreamUpdate'
107
107
  );
108
108
  this.addEvent('STREAM_UPDATE', {
@@ -171,8 +171,8 @@ export class DevToolsService implements StreamUpdateReceiver {
171
171
  if (typeof window !== 'undefined') {
172
172
  window.postMessage(
173
173
  {
174
- type: 'SPOOKY_STATE_CHANGED',
175
- source: 'spooky-devtools-page',
174
+ type: 'SP00KY_STATE_CHANGED',
175
+ source: 'sp00ky-devtools-page',
176
176
  state: this.getState(),
177
177
  },
178
178
  '*'
@@ -229,7 +229,7 @@ export class DevToolsService implements StreamUpdateReceiver {
229
229
 
230
230
  private exposeToWindow() {
231
231
  if (typeof window !== 'undefined') {
232
- (window as any).__SPOOKY__ = {
232
+ (window as any).__00__ = {
233
233
  version: this.version,
234
234
  getState: () => this.getState(),
235
235
  clearHistory: () => {
@@ -270,7 +270,7 @@ export class DevToolsService implements StreamUpdateReceiver {
270
270
  return this.serializeForDevTools(records) || [];
271
271
  } catch (e) {
272
272
  this.logger.error(
273
- { err: e, Category: 'spooky-client::DevToolsService::exposeToWindow' },
273
+ { err: e, Category: 'sp00ky-client::DevToolsService::exposeToWindow' },
274
274
  'Failed to get table data'
275
275
  );
276
276
  return [];
@@ -299,7 +299,7 @@ export class DevToolsService implements StreamUpdateReceiver {
299
299
  runQuery: async (query: string, target: 'local' | 'remote' = 'local') => {
300
300
  try {
301
301
  this.logger.debug(
302
- { query, target, Category: 'spooky-client::DevToolsService::runQuery' },
302
+ { query, target, Category: 'sp00ky-client::DevToolsService::runQuery' },
303
303
  'Running query (START)'
304
304
  );
305
305
  const service = target === 'remote' ? this.remoteDatabaseService : this.databaseService;
@@ -314,7 +314,7 @@ export class DevToolsService implements StreamUpdateReceiver {
314
314
  time: queryTime,
315
315
  resultType: typeof result,
316
316
  isArray: Array.isArray(result),
317
- Category: 'spooky-client::DevToolsService::runQuery',
317
+ Category: 'sp00ky-client::DevToolsService::runQuery',
318
318
  },
319
319
  'Database returned result'
320
320
  );
@@ -328,7 +328,7 @@ export class DevToolsService implements StreamUpdateReceiver {
328
328
  {
329
329
  serializeTime,
330
330
  serializedLength: JSON.stringify(serialized).length,
331
- Category: 'spooky-client::DevToolsService::runQuery',
331
+ Category: 'sp00ky-client::DevToolsService::runQuery',
332
332
  },
333
333
  'Serialization complete'
334
334
  );
@@ -340,7 +340,7 @@ export class DevToolsService implements StreamUpdateReceiver {
340
340
  };
341
341
  } catch (e: any) {
342
342
  this.logger.error(
343
- { err: e, query, target, Category: 'spooky-client::DevToolsService::runQuery' },
343
+ { err: e, query, target, Category: 'sp00ky-client::DevToolsService::runQuery' },
344
344
  'Query execution failed'
345
345
  );
346
346
  // Ensure we always return a string for error
@@ -353,12 +353,15 @@ export class DevToolsService implements StreamUpdateReceiver {
353
353
 
354
354
  window.postMessage(
355
355
  {
356
- type: 'SPOOKY_DETECTED',
357
- source: 'spooky-devtools-page',
356
+ type: 'SP00KY_DETECTED',
357
+ source: 'sp00ky-devtools-page',
358
358
  data: { version: this.version, detected: true },
359
359
  },
360
360
  '*'
361
361
  );
362
+
363
+ // Dispatch custom event so the devtools page-script can detect late initialization
364
+ window.dispatchEvent(new CustomEvent('sp00ky:init'));
362
365
  }
363
366
  }
364
367
  }