@spooky-sync/core 0.0.0-canary.1
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/README.md +21 -0
- package/dist/index.d.ts +590 -0
- package/dist/index.js +3082 -0
- package/package.json +46 -0
- package/src/events/events.test.ts +242 -0
- package/src/events/index.ts +261 -0
- package/src/index.ts +3 -0
- package/src/modules/auth/events/index.ts +18 -0
- package/src/modules/auth/index.ts +267 -0
- package/src/modules/cache/index.ts +241 -0
- package/src/modules/cache/types.ts +19 -0
- package/src/modules/data/data.test.ts +58 -0
- package/src/modules/data/index.ts +777 -0
- package/src/modules/devtools/index.ts +364 -0
- package/src/modules/sync/engine.ts +163 -0
- package/src/modules/sync/events/index.ts +77 -0
- package/src/modules/sync/index.ts +3 -0
- package/src/modules/sync/queue/index.ts +2 -0
- package/src/modules/sync/queue/queue-down.ts +89 -0
- package/src/modules/sync/queue/queue-up.ts +223 -0
- package/src/modules/sync/scheduler.ts +84 -0
- package/src/modules/sync/sync.ts +407 -0
- package/src/modules/sync/utils.test.ts +311 -0
- package/src/modules/sync/utils.ts +171 -0
- package/src/services/database/database.ts +108 -0
- package/src/services/database/events/index.ts +32 -0
- package/src/services/database/index.ts +5 -0
- package/src/services/database/local-migrator.ts +203 -0
- package/src/services/database/local.ts +99 -0
- package/src/services/database/remote.ts +110 -0
- package/src/services/logger/index.ts +118 -0
- package/src/services/persistence/localstorage.ts +26 -0
- package/src/services/persistence/surrealdb.ts +62 -0
- package/src/services/stream-processor/index.ts +364 -0
- package/src/services/stream-processor/stream-processor.test.ts +140 -0
- package/src/services/stream-processor/wasm-types.ts +31 -0
- package/src/spooky.ts +346 -0
- package/src/types.ts +237 -0
- package/src/utils/error-classification.ts +28 -0
- package/src/utils/index.ts +172 -0
- package/src/utils/parser.test.ts +125 -0
- package/src/utils/parser.ts +46 -0
- package/src/utils/surql.ts +182 -0
- package/src/utils/utils.test.ts +152 -0
- package/src/utils/withRetry.test.ts +153 -0
- package/tsconfig.json +14 -0
- package/tsdown.config.ts +9 -0
package/src/spooky.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { DataModule } from './modules/data/index';
|
|
2
|
+
import {
|
|
3
|
+
SpookyConfig,
|
|
4
|
+
QueryTimeToLive,
|
|
5
|
+
SpookyQueryResultPromise,
|
|
6
|
+
PersistenceClient,
|
|
7
|
+
MutationEvent,
|
|
8
|
+
UpdateOptions,
|
|
9
|
+
RunOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
import {
|
|
12
|
+
LocalDatabaseService,
|
|
13
|
+
LocalMigrator,
|
|
14
|
+
RemoteDatabaseService,
|
|
15
|
+
} from './services/database/index';
|
|
16
|
+
import { Surreal } from 'surrealdb';
|
|
17
|
+
import { SpookySync, UpEvent } from './modules/sync/index';
|
|
18
|
+
import {
|
|
19
|
+
GetTable,
|
|
20
|
+
InnerQuery,
|
|
21
|
+
QueryBuilder,
|
|
22
|
+
QueryOptions,
|
|
23
|
+
SchemaStructure,
|
|
24
|
+
TableModel,
|
|
25
|
+
TableNames,
|
|
26
|
+
BackendNames,
|
|
27
|
+
BackendRoutes,
|
|
28
|
+
RoutePayload,
|
|
29
|
+
} from '@spooky-sync/query-builder';
|
|
30
|
+
|
|
31
|
+
import { DevToolsService } from './modules/devtools/index';
|
|
32
|
+
import { createLogger } from './services/logger/index';
|
|
33
|
+
import { AuthService } from './modules/auth/index';
|
|
34
|
+
import { StreamProcessorService } from './services/stream-processor/index';
|
|
35
|
+
import { EventSystem } from './events/index';
|
|
36
|
+
import { CacheModule } from './modules/cache/index';
|
|
37
|
+
import { LocalStoragePersistenceClient } from './services/persistence/localstorage';
|
|
38
|
+
import { generateId, parseParams } from './utils/index';
|
|
39
|
+
import { SurrealDBPersistenceClient } from './services/persistence/surrealdb';
|
|
40
|
+
|
|
41
|
+
export class SpookyClient<S extends SchemaStructure> {
|
|
42
|
+
private local: LocalDatabaseService;
|
|
43
|
+
private remote: RemoteDatabaseService;
|
|
44
|
+
private persistenceClient: PersistenceClient;
|
|
45
|
+
|
|
46
|
+
private migrator: LocalMigrator;
|
|
47
|
+
private cache: CacheModule;
|
|
48
|
+
private dataModule: DataModule<S>;
|
|
49
|
+
private sync: SpookySync<S>;
|
|
50
|
+
private devTools: DevToolsService;
|
|
51
|
+
|
|
52
|
+
private logger: ReturnType<typeof createLogger>;
|
|
53
|
+
public auth: AuthService<S>;
|
|
54
|
+
public streamProcessor: StreamProcessorService;
|
|
55
|
+
|
|
56
|
+
get remoteClient() {
|
|
57
|
+
return this.remote.getClient();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get localClient() {
|
|
61
|
+
return this.local.getClient();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get pendingMutationCount(): number {
|
|
65
|
+
return this.sync.pendingMutationCount;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
subscribeToPendingMutations(cb: (count: number) => void): () => void {
|
|
69
|
+
return this.sync.subscribeToPendingMutations(cb);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
constructor(private config: SpookyConfig<S>) {
|
|
73
|
+
const logger = createLogger(config.logLevel ?? 'info', config.otelEndpoint);
|
|
74
|
+
this.logger = logger.child({ service: 'SpookyClient' });
|
|
75
|
+
|
|
76
|
+
this.logger.info(
|
|
77
|
+
{
|
|
78
|
+
config: { ...config, schema: '[SchemaStructure]' },
|
|
79
|
+
Category: 'spooky-client::SpookyClient::constructor',
|
|
80
|
+
},
|
|
81
|
+
'SpookyClient initialized'
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
this.local = new LocalDatabaseService(this.config.database, logger);
|
|
85
|
+
this.remote = new RemoteDatabaseService(this.config.database, logger);
|
|
86
|
+
|
|
87
|
+
if (config.persistenceClient === 'surrealdb') {
|
|
88
|
+
this.persistenceClient = new SurrealDBPersistenceClient(this.local, logger);
|
|
89
|
+
} else if (config.persistenceClient === 'localstorage' || !config.persistenceClient) {
|
|
90
|
+
this.persistenceClient = new LocalStoragePersistenceClient(logger);
|
|
91
|
+
} else {
|
|
92
|
+
this.persistenceClient = config.persistenceClient;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.streamProcessor = new StreamProcessorService(
|
|
96
|
+
new EventSystem(['stream_update']),
|
|
97
|
+
this.local,
|
|
98
|
+
this.persistenceClient,
|
|
99
|
+
logger
|
|
100
|
+
);
|
|
101
|
+
this.migrator = new LocalMigrator(this.local, logger);
|
|
102
|
+
|
|
103
|
+
this.cache = new CacheModule(
|
|
104
|
+
this.local,
|
|
105
|
+
this.streamProcessor,
|
|
106
|
+
(update) => {
|
|
107
|
+
// Direct callback from cache to data module
|
|
108
|
+
this.dataModule.onStreamUpdate(update);
|
|
109
|
+
},
|
|
110
|
+
logger
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
this.dataModule = new DataModule(
|
|
114
|
+
this.cache,
|
|
115
|
+
this.local,
|
|
116
|
+
this.config.schema,
|
|
117
|
+
logger,
|
|
118
|
+
this.config.streamDebounceTime
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Initialize Auth
|
|
122
|
+
this.auth = new AuthService(this.config.schema, this.remote, this.persistenceClient, logger);
|
|
123
|
+
|
|
124
|
+
// Initialize Sync
|
|
125
|
+
this.sync = new SpookySync(this.local, this.remote, this.cache, this.dataModule, this.config.schema, this.logger);
|
|
126
|
+
|
|
127
|
+
// Initialize DevTools
|
|
128
|
+
this.devTools = new DevToolsService(
|
|
129
|
+
this.local,
|
|
130
|
+
this.remote,
|
|
131
|
+
logger,
|
|
132
|
+
this.config.schema,
|
|
133
|
+
this.auth,
|
|
134
|
+
this.dataModule
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Register DevTools as a receiver for stream updates
|
|
138
|
+
this.streamProcessor.addReceiver(this.devTools);
|
|
139
|
+
|
|
140
|
+
// Wire up callbacks instead of events
|
|
141
|
+
this.setupCallbacks();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Setup direct callbacks instead of event subscriptions
|
|
146
|
+
*/
|
|
147
|
+
private setupCallbacks() {
|
|
148
|
+
// Mutation callback for sync
|
|
149
|
+
this.dataModule.onMutation((mutations: UpEvent[]) => {
|
|
150
|
+
// Notify DevTools
|
|
151
|
+
this.devTools.onMutation(mutations);
|
|
152
|
+
|
|
153
|
+
// Enqueue in Sync
|
|
154
|
+
if (mutations.length > 0) {
|
|
155
|
+
this.sync.enqueueMutation(mutations);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Sync events for incoming updates
|
|
160
|
+
this.sync.events.subscribe('SYNC_QUERY_UPDATED', (event: any) => {
|
|
161
|
+
this.devTools.logEvent('SYNC_QUERY_UPDATED', event.payload);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Database events for DevTools
|
|
165
|
+
this.local.getEvents().subscribe('DATABASE_LOCAL_QUERY', (event: any) => {
|
|
166
|
+
this.devTools.logEvent('LOCAL_QUERY', event.payload);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
this.remote.getEvents().subscribe('DATABASE_REMOTE_QUERY', (event: any) => {
|
|
170
|
+
this.devTools.logEvent('REMOTE_QUERY', event.payload);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async init() {
|
|
175
|
+
this.logger.info(
|
|
176
|
+
{ Category: 'spooky-client::SpookyClient::init' },
|
|
177
|
+
'SpookyClient initialization started'
|
|
178
|
+
);
|
|
179
|
+
try {
|
|
180
|
+
const clientId = this.config.clientId ?? (await this.loadOrGenerateClientId());
|
|
181
|
+
this.persistClientId(clientId);
|
|
182
|
+
this.logger.debug(
|
|
183
|
+
{ clientId, Category: 'spooky-client::SpookyClient::init' },
|
|
184
|
+
'Client ID loaded'
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
await this.local.connect();
|
|
188
|
+
this.logger.debug(
|
|
189
|
+
{ Category: 'spooky-client::SpookyClient::init' },
|
|
190
|
+
'Local database connected'
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await this.migrator.provision(this.config.schemaSurql);
|
|
194
|
+
this.logger.debug({ Category: 'spooky-client::SpookyClient::init' }, 'Schema provisioned');
|
|
195
|
+
|
|
196
|
+
await this.remote.connect();
|
|
197
|
+
this.logger.debug(
|
|
198
|
+
{ Category: 'spooky-client::SpookyClient::init' },
|
|
199
|
+
'Remote database connected'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
await this.streamProcessor.init();
|
|
203
|
+
this.logger.debug(
|
|
204
|
+
{ Category: 'spooky-client::SpookyClient::init' },
|
|
205
|
+
'StreamProcessor initialized'
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
await this.auth.init();
|
|
209
|
+
this.logger.debug({ Category: 'spooky-client::SpookyClient::init' }, 'Auth initialized');
|
|
210
|
+
|
|
211
|
+
await this.dataModule.init();
|
|
212
|
+
this.logger.debug(
|
|
213
|
+
{ Category: 'spooky-client::SpookyClient::init' },
|
|
214
|
+
'DataModule initialized'
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
await this.sync.init(clientId);
|
|
218
|
+
this.logger.debug({ Category: 'spooky-client::SpookyClient::init' }, 'Sync initialized');
|
|
219
|
+
|
|
220
|
+
this.logger.info(
|
|
221
|
+
{ Category: 'spooky-client::SpookyClient::init' },
|
|
222
|
+
'SpookyClient initialization completed successfully'
|
|
223
|
+
);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
this.logger.error(
|
|
226
|
+
{ error: e, Category: 'spooky-client::SpookyClient::init' },
|
|
227
|
+
'SpookyClient initialization failed'
|
|
228
|
+
);
|
|
229
|
+
throw e;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async close() {
|
|
234
|
+
await this.local.close();
|
|
235
|
+
await this.remote.close();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
authenticate(token: string) {
|
|
239
|
+
return this.remote.getClient().authenticate(token);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
deauthenticate() {
|
|
243
|
+
return this.remote.getClient().invalidate();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
query<Table extends TableNames<S>>(
|
|
247
|
+
table: Table,
|
|
248
|
+
options: QueryOptions<TableModel<GetTable<S, Table>>, false>,
|
|
249
|
+
ttl: QueryTimeToLive = '10m'
|
|
250
|
+
): QueryBuilder<S, Table, SpookyQueryResultPromise> {
|
|
251
|
+
return new QueryBuilder<S, Table, SpookyQueryResultPromise>(
|
|
252
|
+
this.config.schema,
|
|
253
|
+
table,
|
|
254
|
+
async (q) => ({
|
|
255
|
+
hash: await this.initQuery(table, q, ttl),
|
|
256
|
+
}),
|
|
257
|
+
options
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private async initQuery<Table extends TableNames<S>>(
|
|
262
|
+
table: Table,
|
|
263
|
+
q: InnerQuery<any, any, any>,
|
|
264
|
+
ttl: QueryTimeToLive
|
|
265
|
+
) {
|
|
266
|
+
const tableSchema = this.config.schema.tables.find((t) => t.name === table);
|
|
267
|
+
if (!tableSchema) {
|
|
268
|
+
throw new Error(`Table ${table} not found`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const hash = await this.dataModule.query(
|
|
272
|
+
table,
|
|
273
|
+
q.selectQuery.query,
|
|
274
|
+
parseParams(tableSchema.columns, q.selectQuery.vars ?? {}),
|
|
275
|
+
ttl
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
await this.sync.enqueueDownEvent({
|
|
279
|
+
type: 'register',
|
|
280
|
+
payload: {
|
|
281
|
+
hash,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
return hash;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async queryRaw(sql: string, params: Record<string, any>, ttl: QueryTimeToLive) {
|
|
289
|
+
const tableName = sql.split('FROM ')[1].split(' ')[0];
|
|
290
|
+
return this.dataModule.query(tableName, sql, params, ttl);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async subscribe(
|
|
294
|
+
queryHash: string,
|
|
295
|
+
callback: (records: Record<string, any>[]) => void,
|
|
296
|
+
options?: { immediate?: boolean }
|
|
297
|
+
): Promise<() => void> {
|
|
298
|
+
return this.dataModule.subscribe(queryHash, callback, options);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
run<
|
|
302
|
+
B extends BackendNames<S>,
|
|
303
|
+
R extends BackendRoutes<S, B>,
|
|
304
|
+
>(backend: B, path: R, payload: RoutePayload<S, B, R>, options?: RunOptions) {
|
|
305
|
+
return this.dataModule.run(backend, path, payload, options);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
create(id: string, data: Record<string, unknown>) {
|
|
309
|
+
return this.dataModule.create(id, data);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
update(table: string, id: string, data: Record<string, unknown>, options?: UpdateOptions) {
|
|
313
|
+
return this.dataModule.update(table, id, data, options);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
delete(table: string, id: string) {
|
|
317
|
+
return this.dataModule.delete(table, id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async useRemote<T>(fn: (client: Surreal) => Promise<T> | T): Promise<T> {
|
|
321
|
+
return fn(this.remote.getClient());
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private persistClientId(id: string) {
|
|
325
|
+
try {
|
|
326
|
+
this.persistenceClient.set('spooky_client_id', id);
|
|
327
|
+
} catch (e) {
|
|
328
|
+
this.logger.warn(
|
|
329
|
+
{ error: e, Category: 'spooky-client::SpookyClient::persistClientId' },
|
|
330
|
+
'Failed to persist client ID'
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async loadOrGenerateClientId(): Promise<string> {
|
|
336
|
+
const clientId = await this.persistenceClient.get<string>('spooky_client_id');
|
|
337
|
+
|
|
338
|
+
if (clientId) {
|
|
339
|
+
return clientId;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const newId = generateId();
|
|
343
|
+
await this.persistClientId(newId);
|
|
344
|
+
return newId;
|
|
345
|
+
}
|
|
346
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { RecordId, SchemaStructure } from '@spooky-sync/query-builder';
|
|
2
|
+
import { Level } from 'pino';
|
|
3
|
+
import { PushEventOptions } from './events/index';
|
|
4
|
+
import { UpEvent } from './modules/sync/index';
|
|
5
|
+
|
|
6
|
+
export type { Level } from 'pino';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The type of storage backend to use for the local database.
|
|
10
|
+
* - 'memory': In-memory storage (transient).
|
|
11
|
+
* - 'indexeddb': IndexedDB storage (persistent).
|
|
12
|
+
*/
|
|
13
|
+
export type StoreType = 'memory' | 'indexeddb';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interface for a custom persistence client.
|
|
17
|
+
* Allows providing a custom storage mechanism for the local database.
|
|
18
|
+
*/
|
|
19
|
+
export interface PersistenceClient {
|
|
20
|
+
/**
|
|
21
|
+
* Sets a value in the storage.
|
|
22
|
+
* @param key The key to set.
|
|
23
|
+
* @param value The value to store.
|
|
24
|
+
*/
|
|
25
|
+
set<T>(key: string, value: T): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Gets a value from the storage.
|
|
28
|
+
* @param key The key to retrieve.
|
|
29
|
+
* @returns The stored value or null if not found.
|
|
30
|
+
*/
|
|
31
|
+
get<T>(key: string): Promise<T | null>;
|
|
32
|
+
/**
|
|
33
|
+
* Removes a value from the storage.
|
|
34
|
+
* @param key The key to remove.
|
|
35
|
+
*/
|
|
36
|
+
remove(key: string): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Supported Time-To-Live (TTL) values for cached queries.
|
|
41
|
+
* Format: number + unit (m=minutes, h=hours, d=days).
|
|
42
|
+
*/
|
|
43
|
+
export type QueryTimeToLive =
|
|
44
|
+
| '1m'
|
|
45
|
+
| '5m'
|
|
46
|
+
| '10m'
|
|
47
|
+
| '15m'
|
|
48
|
+
| '20m'
|
|
49
|
+
| '25m'
|
|
50
|
+
| '30m'
|
|
51
|
+
| '1h'
|
|
52
|
+
| '2h'
|
|
53
|
+
| '3h'
|
|
54
|
+
| '4h'
|
|
55
|
+
| '5h'
|
|
56
|
+
| '6h'
|
|
57
|
+
| '7h'
|
|
58
|
+
| '8h'
|
|
59
|
+
| '9h'
|
|
60
|
+
| '10h'
|
|
61
|
+
| '11h'
|
|
62
|
+
| '12h'
|
|
63
|
+
| '1d';
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Result object returned when a query is registered or executed.
|
|
67
|
+
*/
|
|
68
|
+
export interface SpookyQueryResult {
|
|
69
|
+
/** The unique hash identifier for the query. */
|
|
70
|
+
hash: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type SpookyQueryResultPromise = Promise<SpookyQueryResult>;
|
|
74
|
+
|
|
75
|
+
export interface EventSubscriptionOptions {
|
|
76
|
+
priority?: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Configuration options for the Spooky client.
|
|
81
|
+
* @template S The schema structure type.
|
|
82
|
+
*/
|
|
83
|
+
export interface SpookyConfig<S extends SchemaStructure> {
|
|
84
|
+
/** Database connection configuration. */
|
|
85
|
+
database: {
|
|
86
|
+
/** The SurrealDB endpoint URL. */
|
|
87
|
+
endpoint?: string;
|
|
88
|
+
/** The namespace to use. */
|
|
89
|
+
namespace: string;
|
|
90
|
+
/** The database name. */
|
|
91
|
+
database: string;
|
|
92
|
+
/** The local store type implementation. */
|
|
93
|
+
store?: StoreType;
|
|
94
|
+
/** Authentication token. */
|
|
95
|
+
token?: string;
|
|
96
|
+
};
|
|
97
|
+
/** Unique client identifier. If not provided, one will be generated. */
|
|
98
|
+
clientId?: string;
|
|
99
|
+
/** The schema definition. */
|
|
100
|
+
schema: S;
|
|
101
|
+
/** The compiled SURQL schema string. */
|
|
102
|
+
schemaSurql: string;
|
|
103
|
+
/** Logging level. */
|
|
104
|
+
logLevel: Level;
|
|
105
|
+
/**
|
|
106
|
+
* Persistence client to use.
|
|
107
|
+
* Can be a custom implementation, 'surrealdb' (default), or 'localstorage'.
|
|
108
|
+
*/
|
|
109
|
+
persistenceClient?: PersistenceClient | 'surrealdb' | 'localstorage';
|
|
110
|
+
/** OpenTelemetry collector endpoint for telemetry data. */
|
|
111
|
+
otelEndpoint?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Debounce time in milliseconds for stream updates.
|
|
114
|
+
* Defaults to 100ms.
|
|
115
|
+
*/
|
|
116
|
+
streamDebounceTime?: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type QueryHash = string;
|
|
120
|
+
|
|
121
|
+
// Flat array format: [[record-id, version], [record-id, version], ...]
|
|
122
|
+
export type RecordVersionArray = Array<[string, number]>;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Represents the difference between two record version sets.
|
|
126
|
+
* Used for synchronizing local and remote states.
|
|
127
|
+
*/
|
|
128
|
+
export interface RecordVersionDiff {
|
|
129
|
+
/** List of records added. */
|
|
130
|
+
added: Array<{ id: RecordId<string>; version: number }>;
|
|
131
|
+
/** List of records updated. */
|
|
132
|
+
updated: Array<{ id: RecordId<string>; version: number }>;
|
|
133
|
+
/** List of record IDs removed. */
|
|
134
|
+
removed: RecordId<string>[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Configuration for a specific query instance.
|
|
139
|
+
* Stores metadata about the query's state, parameters, and versioning.
|
|
140
|
+
*/
|
|
141
|
+
export interface QueryConfig {
|
|
142
|
+
/** The unique ID of the query config record. */
|
|
143
|
+
id: RecordId<string>;
|
|
144
|
+
/** The SURQL query string. */
|
|
145
|
+
surql: string;
|
|
146
|
+
/** Parameters used in the query. */
|
|
147
|
+
params: Record<string, any>;
|
|
148
|
+
/** The version array representing the local state of results. */
|
|
149
|
+
localArray: RecordVersionArray;
|
|
150
|
+
/** The version array representing the remote (server) state of results. */
|
|
151
|
+
remoteArray: RecordVersionArray;
|
|
152
|
+
/** Time-To-Live for this query. */
|
|
153
|
+
ttl: QueryTimeToLive;
|
|
154
|
+
/** Timestamp when the query was last accessed/active. */
|
|
155
|
+
lastActiveAt: Date;
|
|
156
|
+
/** The name of the table this query targets (if applicable). */
|
|
157
|
+
tableName: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type QueryConfigRecord = QueryConfig & { id: string };
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Internal state of a live query.
|
|
164
|
+
*/
|
|
165
|
+
export interface QueryState {
|
|
166
|
+
/** The configuration for this query. */
|
|
167
|
+
config: QueryConfig;
|
|
168
|
+
/** The current cached records for this query. */
|
|
169
|
+
records: Record<string, any>[];
|
|
170
|
+
/** Timer for TTL expiration. */
|
|
171
|
+
ttlTimer: NodeJS.Timeout | null;
|
|
172
|
+
/** TTL duration in milliseconds. */
|
|
173
|
+
ttlDurationMs: number;
|
|
174
|
+
/** Number of times the query has been updated. */
|
|
175
|
+
updateCount: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Callback types
|
|
179
|
+
export type QueryUpdateCallback = (records: Record<string, any>[]) => void;
|
|
180
|
+
export type MutationCallback = (mutations: UpEvent[]) => void;
|
|
181
|
+
|
|
182
|
+
export type MutationEventType = 'create' | 'update' | 'delete';
|
|
183
|
+
|
|
184
|
+
// Mutation event for sync
|
|
185
|
+
/**
|
|
186
|
+
* Represents a mutation event (create, update, delete) to be synchronized.
|
|
187
|
+
*/
|
|
188
|
+
export interface MutationEvent {
|
|
189
|
+
/** Example: 'create', 'update', or 'delete'. */
|
|
190
|
+
type: MutationEventType;
|
|
191
|
+
/** unique id of the mutation */
|
|
192
|
+
mutation_id: RecordId<string>;
|
|
193
|
+
/** The ID of the record being mutated. */
|
|
194
|
+
record_id: RecordId<string>;
|
|
195
|
+
/** The data payload for create/update operations. */
|
|
196
|
+
data?: any;
|
|
197
|
+
/** The full record data (optional context). */
|
|
198
|
+
record?: any;
|
|
199
|
+
/** Options for the mutation event (e.g., debounce settings). */
|
|
200
|
+
options?: PushEventOptions;
|
|
201
|
+
/** Timestamp when the event was created. */
|
|
202
|
+
createdAt: Date;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Options for run operations.
|
|
207
|
+
*/
|
|
208
|
+
export interface RunOptions {
|
|
209
|
+
assignedTo?: string;
|
|
210
|
+
max_retries?: number;
|
|
211
|
+
retry_strategy?: 'linear' | 'exponential';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Options for update operations.
|
|
216
|
+
*/
|
|
217
|
+
export interface UpdateOptions {
|
|
218
|
+
/**
|
|
219
|
+
* Debounce configuration for the update.
|
|
220
|
+
* If boolean, enables default debounce behavior.
|
|
221
|
+
*/
|
|
222
|
+
debounced?: boolean | DebounceOptions;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Configuration options for debouncing updates.
|
|
227
|
+
*/
|
|
228
|
+
export interface DebounceOptions {
|
|
229
|
+
/**
|
|
230
|
+
* The key to use for debouncing.
|
|
231
|
+
* - 'recordId': Debounce based on the specific record ID. WARNING: IT WILL ONLY ACCEPT THE LATEST CHANGE AND DOES *NOT* MERGE THE PREVIOUS ONCES. IF YOU ARE UNSURE JUST USE 'recordId_x_fields'.
|
|
232
|
+
* - 'recordId_x_fields': Debounce based on record ID and specific fields.
|
|
233
|
+
*/
|
|
234
|
+
key?: 'recordId' | 'recordId_x_fields';
|
|
235
|
+
/** The debounce delay in milliseconds. */
|
|
236
|
+
delay?: number;
|
|
237
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const NETWORK_ERROR_PATTERNS = [
|
|
2
|
+
'connection',
|
|
3
|
+
'timeout',
|
|
4
|
+
'timed out',
|
|
5
|
+
'websocket',
|
|
6
|
+
'fetch failed',
|
|
7
|
+
'disconnected',
|
|
8
|
+
'socket',
|
|
9
|
+
'network',
|
|
10
|
+
'econnrefused',
|
|
11
|
+
'econnreset',
|
|
12
|
+
'enotfound',
|
|
13
|
+
'epipe',
|
|
14
|
+
'abort',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function classifySyncError(error: unknown): 'network' | 'application' {
|
|
18
|
+
const message =
|
|
19
|
+
error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
20
|
+
|
|
21
|
+
for (const pattern of NETWORK_ERROR_PATTERNS) {
|
|
22
|
+
if (message.includes(pattern)) {
|
|
23
|
+
return 'network';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return 'application';
|
|
28
|
+
}
|