@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.
- package/dist/index.d.ts +18 -367
- package/dist/index.js +268 -287
- package/dist/otel/index.d.ts +21 -0
- package/dist/otel/index.js +86 -0
- package/dist/types.d.ts +361 -0
- package/package.json +34 -5
- package/skills/sp00ky-core/SKILL.md +258 -0
- package/skills/sp00ky-core/references/auth.md +98 -0
- package/skills/sp00ky-core/references/config.md +76 -0
- package/src/events/events.test.ts +2 -1
- package/src/index.ts +1 -1
- package/src/modules/auth/events/index.ts +2 -1
- package/src/modules/auth/index.ts +17 -20
- package/src/modules/cache/index.ts +23 -23
- package/src/modules/cache/types.ts +2 -2
- package/src/modules/data/index.ts +61 -43
- package/src/modules/devtools/index.ts +23 -20
- package/src/modules/sync/engine.ts +31 -21
- package/src/modules/sync/events/index.ts +3 -2
- package/src/modules/sync/queue/queue-down.ts +5 -4
- package/src/modules/sync/queue/queue-up.ts +14 -13
- package/src/modules/sync/scheduler.ts +2 -2
- package/src/modules/sync/sync.ts +47 -35
- package/src/modules/sync/utils.test.ts +2 -2
- package/src/modules/sync/utils.ts +15 -17
- package/src/otel/index.ts +127 -0
- package/src/services/database/database.ts +11 -11
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/local-migrator.ts +21 -21
- package/src/services/database/local.ts +16 -15
- package/src/services/database/remote.ts +13 -13
- package/src/services/logger/index.ts +6 -101
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +34 -0
- package/src/services/persistence/surrealdb.ts +9 -9
- package/src/services/stream-processor/index.ts +32 -31
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +2 -2
- package/src/{spooky.ts → sp00ky.ts} +40 -38
- package/src/types.ts +18 -11
- package/src/utils/index.ts +10 -10
- package/src/utils/parser.ts +2 -1
- package/src/utils/surql.ts +15 -15
- package/src/utils/withRetry.test.ts +1 -1
- 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: '
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
154
|
+
this.streamProcessor.ingest(table, 'DELETE', id, recordData);
|
|
155
155
|
|
|
156
156
|
this.logger.debug(
|
|
157
|
-
{ table, id, Category: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
|
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: '
|
|
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: '
|
|
79
|
+
{ hash, Category: 'sp00ky-client::DataModule::query' },
|
|
82
80
|
'Query Initialization: started'
|
|
83
81
|
);
|
|
84
82
|
|
|
85
|
-
const recordId = new RecordId('
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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(`
|
|
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: '
|
|
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(`
|
|
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: '
|
|
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 ('
|
|
500
|
-
updatedFields.
|
|
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.
|
|
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: '
|
|
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(`
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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.
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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).
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
175
|
-
source: '
|
|
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).
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
357
|
-
source: '
|
|
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
|
}
|