@spooky-sync/core 0.0.1-canary.13 → 0.0.1-canary.130
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/AGENTS.md +56 -0
- package/dist/index.d.ts +1171 -372
- package/dist/index.js +5716 -1278
- package/dist/otel/index.d.ts +21 -0
- package/dist/otel/index.js +86 -0
- package/dist/sqlite-worker.d.ts +1 -0
- package/dist/sqlite-worker.js +107 -0
- package/dist/types.d.ts +746 -0
- package/package.json +39 -8
- 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/build-globals.d.ts +12 -0
- package/src/events/events.test.ts +2 -1
- package/src/events/index.ts +3 -0
- package/src/index.ts +9 -2
- package/src/modules/auth/events/index.ts +2 -1
- package/src/modules/auth/index.ts +59 -20
- package/src/modules/cache/index.ts +77 -32
- package/src/modules/cache/types.ts +2 -2
- package/src/modules/crdt/crdt-field.ts +288 -0
- package/src/modules/crdt/crdt-hydration.test.ts +206 -0
- package/src/modules/crdt/index.ts +357 -0
- package/src/modules/data/data.hydration.test.ts +150 -0
- package/src/modules/data/data.rebind.test.ts +147 -0
- package/src/modules/data/data.recurring.test.ts +137 -0
- package/src/modules/data/data.status.test.ts +249 -0
- package/src/modules/data/index.ts +1228 -127
- package/src/modules/data/window-query.test.ts +52 -0
- package/src/modules/data/window-query.ts +154 -0
- package/src/modules/devtools/index.ts +191 -30
- package/src/modules/devtools/versions.test.ts +74 -0
- package/src/modules/devtools/versions.ts +81 -0
- package/src/modules/feature-flag/index.test.ts +120 -0
- package/src/modules/feature-flag/index.ts +209 -0
- package/src/modules/ref-tables.test.ts +91 -0
- package/src/modules/ref-tables.ts +88 -0
- package/src/modules/sync/engine.ts +101 -37
- package/src/modules/sync/events/index.ts +9 -2
- package/src/modules/sync/queue/queue-down.ts +12 -5
- package/src/modules/sync/queue/queue-up.ts +29 -14
- package/src/modules/sync/scheduler.pause.test.ts +109 -0
- package/src/modules/sync/scheduler.ts +73 -7
- package/src/modules/sync/sync.health.test.ts +149 -0
- package/src/modules/sync/sync.subquery.test.ts +82 -0
- package/src/modules/sync/sync.ts +1017 -62
- package/src/modules/sync/utils.test.ts +269 -2
- package/src/modules/sync/utils.ts +182 -17
- package/src/otel/index.ts +127 -0
- package/src/services/database/cache-engine.ts +143 -0
- package/src/services/database/database.ts +11 -11
- package/src/services/database/engine-factory.ts +32 -0
- package/src/services/database/events/index.ts +2 -1
- package/src/services/database/index.ts +6 -0
- package/src/services/database/local-migrator.ts +28 -27
- package/src/services/database/local.test.ts +64 -0
- package/src/services/database/local.ts +452 -66
- package/src/services/database/plan-render.test.ts +133 -0
- package/src/services/database/plan-render.ts +100 -0
- package/src/services/database/relation-resolver.test.ts +413 -0
- package/src/services/database/relation-resolver.ts +0 -0
- package/src/services/database/remote.ts +13 -13
- package/src/services/database/sqlite-cache-engine.test.ts +85 -0
- package/src/services/database/sqlite-cache-engine.ts +699 -0
- package/src/services/database/sqlite-worker.ts +116 -0
- package/src/services/database/surql-translate.ts +291 -0
- package/src/services/database/surreal-cache-engine.ts +122 -0
- package/src/services/logger/index.ts +6 -101
- package/src/services/persistence/localstorage.ts +2 -2
- package/src/services/persistence/resilient.ts +41 -0
- package/src/services/persistence/surrealdb.ts +10 -10
- package/src/services/stream-processor/index.ts +295 -38
- package/src/services/stream-processor/permissions.test.ts +47 -0
- package/src/services/stream-processor/permissions.ts +53 -0
- package/src/services/stream-processor/stream-processor.batch.test.ts +136 -0
- package/src/services/stream-processor/stream-processor.reset.test.ts +104 -0
- package/src/services/stream-processor/stream-processor.test.ts +1 -1
- package/src/services/stream-processor/wasm-types.ts +18 -2
- package/src/sp00ky.auth-order.test.ts +92 -0
- package/src/sp00ky.init-query.test.ts +185 -0
- package/src/sp00ky.ts +1016 -0
- package/src/types.ts +258 -15
- package/src/utils/error-classification.test.ts +44 -0
- package/src/utils/error-classification.ts +7 -0
- package/src/utils/index.ts +35 -13
- package/src/utils/parser.ts +3 -2
- package/src/utils/surql.ts +24 -15
- package/src/utils/withRetry.test.ts +1 -1
- package/tsdown.config.ts +77 -1
- package/src/spooky.ts +0 -392
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
import { RemoteDatabaseService } from '../../services/database/remote';
|
|
2
|
-
import {
|
|
3
|
-
import { DataModule } from '../data/index';
|
|
4
|
-
import {
|
|
1
|
+
import type { RemoteDatabaseService } from '../../services/database/remote';
|
|
2
|
+
import type {
|
|
5
3
|
SchemaStructure,
|
|
6
4
|
AccessDefinition,
|
|
7
5
|
ColumnSchema,
|
|
8
6
|
TypeNameToTypeMap,
|
|
9
7
|
} from '@spooky-sync/query-builder';
|
|
10
|
-
import { Logger } from '../../services/logger/index';
|
|
11
|
-
import { encodeRecordId } from '../../utils/index';
|
|
8
|
+
import type { Logger } from '../../services/logger/index';
|
|
12
9
|
export * from './events/index';
|
|
13
10
|
import { AuthEventTypes, createAuthEventSystem } from './events/index';
|
|
14
|
-
import { PersistenceClient } from '../../types';
|
|
11
|
+
import type { PersistenceClient } from '../../types';
|
|
15
12
|
|
|
16
13
|
// Helper to pretty print types
|
|
17
14
|
type Prettify<T> = {
|
|
@@ -38,11 +35,41 @@ type ExtractAccessParams<
|
|
|
38
35
|
}>
|
|
39
36
|
: never;
|
|
40
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Read the `AC` (access-method name) claim from a SurrealDB record-access
|
|
40
|
+
* JWT without verifying it — we only need the claim, the server enforces the
|
|
41
|
+
* token. Returns null on any malformed input. The in-browser SSP needs this
|
|
42
|
+
* to resolve `$access` in table permission predicates (mirrors the session's
|
|
43
|
+
* `$access` that the server's `fn::query::register` reads).
|
|
44
|
+
*/
|
|
45
|
+
function decodeAccessFromToken(token: string): string | null {
|
|
46
|
+
try {
|
|
47
|
+
const payload = token.split('.')[1];
|
|
48
|
+
if (!payload) return null;
|
|
49
|
+
let b64 = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
50
|
+
b64 += '='.repeat((4 - (b64.length % 4)) % 4);
|
|
51
|
+
const json =
|
|
52
|
+
typeof atob === 'function' ? atob(b64) : Buffer.from(b64, 'base64').toString('binary');
|
|
53
|
+
const claims = JSON.parse(json) as Record<string, unknown>;
|
|
54
|
+
const ac = claims.AC ?? claims.ac;
|
|
55
|
+
return typeof ac === 'string' ? ac : null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
export class AuthService<S extends SchemaStructure> {
|
|
42
62
|
// State
|
|
43
63
|
public token: string | null = null;
|
|
44
64
|
public currentUser: any | null = null;
|
|
45
65
|
public isAuthenticated: boolean = false;
|
|
66
|
+
/**
|
|
67
|
+
* The record-access method name for the current session (e.g. `"account"`),
|
|
68
|
+
* derived from the token's `AC` claim. Consumed by the in-browser SSP's
|
|
69
|
+
* permission injection so `$access`-gated table predicates resolve locally,
|
|
70
|
+
* mirroring the server's `$access`. Null when logged out.
|
|
71
|
+
*/
|
|
72
|
+
public access: string | null = null;
|
|
46
73
|
public isLoading: boolean = true;
|
|
47
74
|
|
|
48
75
|
private events = createAuthEventSystem();
|
|
@@ -95,11 +122,11 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
95
122
|
this.isLoading = true;
|
|
96
123
|
|
|
97
124
|
try {
|
|
98
|
-
const token = accessToken || (await this.persistenceClient.get<string>('
|
|
125
|
+
const token = accessToken || (await this.persistenceClient.get<string>('sp00ky_auth_token'));
|
|
99
126
|
|
|
100
127
|
if (!token) {
|
|
101
128
|
this.logger.debug(
|
|
102
|
-
{ Category: '
|
|
129
|
+
{ Category: 'sp00ky-client::AuthService::check' },
|
|
103
130
|
'No token found in storage or arguments'
|
|
104
131
|
);
|
|
105
132
|
this.isLoading = false;
|
|
@@ -119,13 +146,13 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
119
146
|
|
|
120
147
|
if (user && user.id) {
|
|
121
148
|
this.logger.info(
|
|
122
|
-
{ user, Category: '
|
|
149
|
+
{ user, Category: 'sp00ky-client::AuthService::check' },
|
|
123
150
|
'Auth check complete (via $auth.id)'
|
|
124
151
|
);
|
|
125
152
|
await this.setSession(token, user);
|
|
126
153
|
} else {
|
|
127
154
|
this.logger.warn(
|
|
128
|
-
{ Category: '
|
|
155
|
+
{ Category: 'sp00ky-client::AuthService::check' },
|
|
129
156
|
'$auth.id empty, attempting manual user fetch'
|
|
130
157
|
);
|
|
131
158
|
|
|
@@ -140,13 +167,13 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
140
167
|
|
|
141
168
|
if (manualUser && manualUser.id) {
|
|
142
169
|
this.logger.info(
|
|
143
|
-
{ user: manualUser, Category: '
|
|
170
|
+
{ user: manualUser, Category: 'sp00ky-client::AuthService::check' },
|
|
144
171
|
'Auth check complete (via manual fetch)'
|
|
145
172
|
);
|
|
146
173
|
await this.setSession(token, manualUser);
|
|
147
174
|
} else {
|
|
148
175
|
this.logger.warn(
|
|
149
|
-
{ Category: '
|
|
176
|
+
{ Category: 'sp00ky-client::AuthService::check' },
|
|
150
177
|
'Token valid but user not found via fallback'
|
|
151
178
|
);
|
|
152
179
|
await this.signOut();
|
|
@@ -154,7 +181,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
154
181
|
}
|
|
155
182
|
} catch (error) {
|
|
156
183
|
this.logger.error(
|
|
157
|
-
{ error, stack: (error as Error).stack, Category: '
|
|
184
|
+
{ error, stack: (error as Error).stack, Category: 'sp00ky-client::AuthService::check' },
|
|
158
185
|
'Auth check failed'
|
|
159
186
|
);
|
|
160
187
|
await this.signOut();
|
|
@@ -170,12 +197,13 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
170
197
|
this.token = null;
|
|
171
198
|
this.currentUser = null;
|
|
172
199
|
this.isAuthenticated = false;
|
|
200
|
+
this.access = null;
|
|
173
201
|
|
|
174
|
-
await this.persistenceClient.remove('
|
|
202
|
+
await this.persistenceClient.remove('sp00ky_auth_token');
|
|
175
203
|
|
|
176
204
|
try {
|
|
177
205
|
await this.remote.getClient().invalidate();
|
|
178
|
-
} catch (
|
|
206
|
+
} catch (_e) {
|
|
179
207
|
// Ignore invalidation errors
|
|
180
208
|
}
|
|
181
209
|
|
|
@@ -186,10 +214,21 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
186
214
|
this.token = token;
|
|
187
215
|
this.currentUser = user;
|
|
188
216
|
this.isAuthenticated = true;
|
|
189
|
-
|
|
217
|
+
// Resolve the access-method name (e.g. "account") for in-browser SSP
|
|
218
|
+
// permission injection. Prefer the token's `AC` claim; fall back to the
|
|
219
|
+
// schema's sole record-access method if the claim is absent.
|
|
220
|
+
this.access = decodeAccessFromToken(token) ?? this.defaultAccessName();
|
|
221
|
+
await this.persistenceClient.set('sp00ky_auth_token', token);
|
|
190
222
|
this.notifyListeners();
|
|
191
223
|
}
|
|
192
224
|
|
|
225
|
+
/** Fallback when the token carries no `AC` claim: if the schema defines
|
|
226
|
+
* exactly one record-access method, assume the session used it. */
|
|
227
|
+
private defaultAccessName(): string | null {
|
|
228
|
+
const names = Object.keys(this.schema.access ?? {});
|
|
229
|
+
return names.length === 1 ? names[0] : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
193
232
|
async signUp<Name extends keyof S['access'] & string>(
|
|
194
233
|
accessName: Name,
|
|
195
234
|
params: ExtractAccessParams<S, Name, 'signup'>
|
|
@@ -212,7 +251,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
this.logger.info(
|
|
215
|
-
{ accessName, runtimeParams, Category: '
|
|
254
|
+
{ accessName, runtimeParams, Category: 'sp00ky-client::AuthService::signUp' },
|
|
216
255
|
'Attempting signup'
|
|
217
256
|
);
|
|
218
257
|
|
|
@@ -222,7 +261,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
222
261
|
});
|
|
223
262
|
|
|
224
263
|
this.logger.info(
|
|
225
|
-
{ Category: '
|
|
264
|
+
{ Category: 'sp00ky-client::AuthService::signUp' },
|
|
226
265
|
'Signup successful, token received'
|
|
227
266
|
);
|
|
228
267
|
|
|
@@ -253,7 +292,7 @@ export class AuthService<S extends SchemaStructure> {
|
|
|
253
292
|
}
|
|
254
293
|
|
|
255
294
|
this.logger.info(
|
|
256
|
-
{ accessName, Category: '
|
|
295
|
+
{ accessName, Category: 'sp00ky-client::AuthService::signIn' },
|
|
257
296
|
'Attempting signin'
|
|
258
297
|
);
|
|
259
298
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { LocalStore } from '../../services/database/index';
|
|
2
|
+
import { StaleEpochError } from '../../services/database/index';
|
|
3
|
+
import type {
|
|
3
4
|
StreamProcessorService,
|
|
4
5
|
StreamUpdate,
|
|
5
6
|
StreamUpdateReceiver,
|
|
6
7
|
} from '../../services/stream-processor/index';
|
|
7
|
-
import { Logger } from '../../services/logger/index';
|
|
8
|
+
import type { Logger } from '../../services/logger/index';
|
|
8
9
|
import { parseRecordIdString, encodeRecordId, surql } from '../../utils/index';
|
|
9
|
-
import { CacheRecord, QueryConfig } from './types';
|
|
10
|
-
import { RecordVersionArray } from '../../types';
|
|
10
|
+
import type { CacheRecord, QueryConfig } from './types';
|
|
11
|
+
import type { RecordVersionArray } from '../../types';
|
|
11
12
|
|
|
12
13
|
export * from './types';
|
|
13
14
|
|
|
@@ -23,7 +24,7 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
23
24
|
private versionLookups: Record<string, number> = {};
|
|
24
25
|
|
|
25
26
|
constructor(
|
|
26
|
-
private local:
|
|
27
|
+
private local: LocalStore,
|
|
27
28
|
private streamProcessor: StreamProcessorService,
|
|
28
29
|
streamUpdateCallback: (update: StreamUpdate) => void,
|
|
29
30
|
logger: Logger
|
|
@@ -43,7 +44,7 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
43
44
|
{
|
|
44
45
|
queryHash: update.queryHash,
|
|
45
46
|
arrayLength: update.localArray?.length,
|
|
46
|
-
Category: '
|
|
47
|
+
Category: 'sp00ky-client::CacheModule::onStreamUpdate',
|
|
47
48
|
},
|
|
48
49
|
'Stream update received'
|
|
49
50
|
);
|
|
@@ -54,6 +55,12 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
54
55
|
return this.versionLookups[recordId] ?? 0;
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/** Drop the version cache on a bucket switch — a stale version would make
|
|
59
|
+
* the sync diff skip fetching a body the new bucket legitimately needs. */
|
|
60
|
+
public clearVersionLookups(): void {
|
|
61
|
+
this.versionLookups = {};
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
/**
|
|
58
65
|
* Save a single record to local DB and ingest into DBSP
|
|
59
66
|
* Used by mutations (create/update)
|
|
@@ -70,10 +77,17 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
70
77
|
async saveBatch(records: CacheRecord[], skipDbInsert: boolean = false): Promise<void> {
|
|
71
78
|
if (records.length === 0) return;
|
|
72
79
|
|
|
80
|
+
// Fence against bucket switches: this batch's records were derived from
|
|
81
|
+
// reads against the CURRENT store/user. If a switch lands while we await
|
|
82
|
+
// the (gated) local write, the write throws StaleEpochError and the whole
|
|
83
|
+
// batch — including the SSP ingest — is dropped: the new bucket re-syncs
|
|
84
|
+
// its own data from the server.
|
|
85
|
+
const epoch = this.local.epoch;
|
|
86
|
+
|
|
73
87
|
this.logger.debug(
|
|
74
88
|
{
|
|
75
89
|
count: records.length,
|
|
76
|
-
Category: '
|
|
90
|
+
Category: 'sp00ky-client::CacheModule::saveBatch',
|
|
77
91
|
},
|
|
78
92
|
'Saving record batch'
|
|
79
93
|
);
|
|
@@ -85,7 +99,7 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
85
99
|
...record,
|
|
86
100
|
record: {
|
|
87
101
|
...record.record,
|
|
88
|
-
|
|
102
|
+
_00_rv: record.version,
|
|
89
103
|
},
|
|
90
104
|
};
|
|
91
105
|
});
|
|
@@ -94,7 +108,12 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
94
108
|
const query = surql.seal<void>(
|
|
95
109
|
surql.tx(
|
|
96
110
|
populatedRecords.map((_, i) => {
|
|
97
|
-
|
|
111
|
+
// MERGE, not REPLACE: the remote payload omits local-only
|
|
112
|
+
// fields (`_00_crdt`, `_00_cursor`) injected by the CLI's
|
|
113
|
+
// local schema, so REPLACE would wipe the persisted CRDT
|
|
114
|
+
// snapshot on every sync-down round-trip and break offline
|
|
115
|
+
// reload of formatted text.
|
|
116
|
+
return surql.upsertMerge(`id${i}`, `content${i}`);
|
|
98
117
|
})
|
|
99
118
|
)
|
|
100
119
|
);
|
|
@@ -111,23 +130,37 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
111
130
|
{} as Record<string, any>
|
|
112
131
|
);
|
|
113
132
|
|
|
114
|
-
await this.local.execute(query, params);
|
|
133
|
+
await this.local.execute(query, params, { epoch });
|
|
115
134
|
}
|
|
116
135
|
|
|
117
|
-
//
|
|
118
|
-
|
|
136
|
+
// Late fence for the skipDbInsert path (no gated write above to trip on).
|
|
137
|
+
if (this.local.epoch !== epoch) throw new StaleEpochError();
|
|
138
|
+
|
|
139
|
+
// 2. Bulk ingest into DBSP (use populatedRecords which has _00_rv set).
|
|
140
|
+
// ingestMany coalesces the per-record stream updates into a single
|
|
141
|
+
// notification per affected query — the UI then updates once, after the
|
|
142
|
+
// whole batch is ingested, instead of row-by-row.
|
|
143
|
+
const bulk = populatedRecords.map((record) => {
|
|
119
144
|
const recordId = encodeRecordId(record.record.id);
|
|
120
145
|
this.versionLookups[recordId] = record.version;
|
|
121
|
-
|
|
122
|
-
}
|
|
146
|
+
return { table: record.table, op: record.op, id: recordId, record: record.record };
|
|
147
|
+
});
|
|
148
|
+
this.streamProcessor.ingestMany(bulk);
|
|
123
149
|
|
|
124
150
|
this.logger.debug(
|
|
125
|
-
{ count: records.length, Category: '
|
|
151
|
+
{ count: records.length, Category: 'sp00ky-client::CacheModule::saveBatch' },
|
|
126
152
|
'Batch saved successfully'
|
|
127
153
|
);
|
|
128
154
|
} catch (err) {
|
|
155
|
+
if (err instanceof StaleEpochError) {
|
|
156
|
+
this.logger.debug(
|
|
157
|
+
{ count: records.length, Category: 'sp00ky-client::CacheModule::saveBatch' },
|
|
158
|
+
'Dropped batch from before a bucket switch'
|
|
159
|
+
);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
129
162
|
this.logger.error(
|
|
130
|
-
{ err, count: records.length, Category: '
|
|
163
|
+
{ err, count: records.length, Category: 'sp00ky-client::CacheModule::saveBatch' },
|
|
131
164
|
'Failed to save batch'
|
|
132
165
|
);
|
|
133
166
|
throw err;
|
|
@@ -137,29 +170,38 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
137
170
|
/**
|
|
138
171
|
* Delete a record from local DB and ingest deletion into DBSP
|
|
139
172
|
*/
|
|
140
|
-
async delete(table: string, id: string, skipDbDelete: boolean = false): Promise<void> {
|
|
173
|
+
async delete(table: string, id: string, skipDbDelete: boolean = false, recordData: Record<string, any> = {}): Promise<void> {
|
|
141
174
|
this.logger.debug(
|
|
142
|
-
{ table, id, Category: '
|
|
175
|
+
{ table, id, Category: 'sp00ky-client::CacheModule::delete' },
|
|
143
176
|
'Deleting record'
|
|
144
177
|
);
|
|
145
178
|
|
|
179
|
+
const epoch = this.local.epoch;
|
|
146
180
|
try {
|
|
147
181
|
// 1. Delete from local database
|
|
148
182
|
if (!skipDbDelete) {
|
|
149
|
-
await this.local.query('DELETE $id', { id: parseRecordIdString(id) });
|
|
183
|
+
await this.local.query('DELETE $id', { id: parseRecordIdString(id) }, { epoch });
|
|
150
184
|
}
|
|
185
|
+
if (this.local.epoch !== epoch) throw new StaleEpochError();
|
|
151
186
|
|
|
152
|
-
// 2. Ingest deletion into DBSP
|
|
187
|
+
// 2. Ingest deletion into DBSP (pass record data so predicates can be matched)
|
|
153
188
|
delete this.versionLookups[id];
|
|
154
|
-
|
|
189
|
+
this.streamProcessor.ingest(table, 'DELETE', id, recordData);
|
|
155
190
|
|
|
156
191
|
this.logger.debug(
|
|
157
|
-
{ table, id, Category: '
|
|
192
|
+
{ table, id, Category: 'sp00ky-client::CacheModule::delete' },
|
|
158
193
|
'Record deleted successfully'
|
|
159
194
|
);
|
|
160
195
|
} catch (err) {
|
|
196
|
+
if (err instanceof StaleEpochError) {
|
|
197
|
+
this.logger.debug(
|
|
198
|
+
{ table, id, Category: 'sp00ky-client::CacheModule::delete' },
|
|
199
|
+
'Dropped delete from before a bucket switch'
|
|
200
|
+
);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
161
203
|
this.logger.error(
|
|
162
|
-
{ err, table, id, Category: '
|
|
204
|
+
{ err, table, id, Category: 'sp00ky-client::CacheModule::delete' },
|
|
163
205
|
'Failed to delete record'
|
|
164
206
|
);
|
|
165
207
|
throw err;
|
|
@@ -170,12 +212,15 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
170
212
|
* Register a query with DBSP to create a materialized view
|
|
171
213
|
* Returns the initial result array
|
|
172
214
|
*/
|
|
173
|
-
registerQuery(config: QueryConfig): {
|
|
215
|
+
registerQuery(config: QueryConfig): {
|
|
216
|
+
localArray: RecordVersionArray;
|
|
217
|
+
registrationTimings?: { parseMs: number; planMs: number; snapshotMs: number };
|
|
218
|
+
} {
|
|
174
219
|
this.logger.debug(
|
|
175
220
|
{
|
|
176
221
|
queryHash: config.queryHash,
|
|
177
222
|
surql: config.surql,
|
|
178
|
-
Category: '
|
|
223
|
+
Category: 'sp00ky-client::CacheModule::registerQuery',
|
|
179
224
|
},
|
|
180
225
|
'Registering query'
|
|
181
226
|
);
|
|
@@ -202,15 +247,15 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
202
247
|
{
|
|
203
248
|
queryHash: config.queryHash,
|
|
204
249
|
arrayLength: update.localArray?.length,
|
|
205
|
-
Category: '
|
|
250
|
+
Category: 'sp00ky-client::CacheModule::registerQuery',
|
|
206
251
|
},
|
|
207
252
|
'Query registered successfully'
|
|
208
253
|
);
|
|
209
254
|
|
|
210
|
-
return { localArray: update.localArray };
|
|
255
|
+
return { localArray: update.localArray, registrationTimings: update.registration };
|
|
211
256
|
} catch (err) {
|
|
212
257
|
this.logger.error(
|
|
213
|
-
{ err, queryHash: config.queryHash, Category: '
|
|
258
|
+
{ err, queryHash: config.queryHash, Category: 'sp00ky-client::CacheModule::registerQuery' },
|
|
214
259
|
'Failed to register query'
|
|
215
260
|
);
|
|
216
261
|
throw err;
|
|
@@ -222,18 +267,18 @@ export class CacheModule implements StreamUpdateReceiver {
|
|
|
222
267
|
*/
|
|
223
268
|
unregisterQuery(queryHash: string): void {
|
|
224
269
|
this.logger.debug(
|
|
225
|
-
{ queryHash, Category: '
|
|
270
|
+
{ queryHash, Category: 'sp00ky-client::CacheModule::unregisterQuery' },
|
|
226
271
|
'Unregistering query'
|
|
227
272
|
);
|
|
228
273
|
try {
|
|
229
274
|
this.streamProcessor.unregisterQueryPlan(queryHash);
|
|
230
275
|
this.logger.debug(
|
|
231
|
-
{ queryHash, Category: '
|
|
276
|
+
{ queryHash, Category: 'sp00ky-client::CacheModule::unregisterQuery' },
|
|
232
277
|
'Query unregistered successfully'
|
|
233
278
|
);
|
|
234
279
|
} catch (err) {
|
|
235
280
|
this.logger.error(
|
|
236
|
-
{ err, queryHash, Category: '
|
|
281
|
+
{ err, queryHash, Category: 'sp00ky-client::CacheModule::unregisterQuery' },
|
|
237
282
|
'Failed to unregister query'
|
|
238
283
|
);
|
|
239
284
|
}
|
|
@@ -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
|
|