@spooky-sync/core 0.0.1-canary.1 → 0.0.1-canary.100

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 (66) hide show
  1. package/AGENTS.md +56 -0
  2. package/dist/index.d.ts +938 -339
  3. package/dist/index.js +2978 -423
  4. package/dist/otel/index.d.ts +21 -0
  5. package/dist/otel/index.js +86 -0
  6. package/dist/types.d.ts +548 -0
  7. package/package.json +39 -9
  8. package/skills/sp00ky-core/SKILL.md +258 -0
  9. package/skills/sp00ky-core/references/auth.md +98 -0
  10. package/skills/sp00ky-core/references/config.md +76 -0
  11. package/src/build-globals.d.ts +12 -0
  12. package/src/events/events.test.ts +2 -1
  13. package/src/events/index.ts +3 -0
  14. package/src/index.ts +9 -1
  15. package/src/modules/auth/events/index.ts +2 -1
  16. package/src/modules/auth/index.ts +59 -20
  17. package/src/modules/cache/index.ts +41 -29
  18. package/src/modules/cache/types.ts +2 -2
  19. package/src/modules/crdt/crdt-field.ts +281 -0
  20. package/src/modules/crdt/crdt-hydration.test.ts +206 -0
  21. package/src/modules/crdt/index.ts +352 -0
  22. package/src/modules/data/data.status.test.ts +108 -0
  23. package/src/modules/data/index.ts +848 -134
  24. package/src/modules/data/window-query.test.ts +52 -0
  25. package/src/modules/data/window-query.ts +130 -0
  26. package/src/modules/devtools/index.ts +115 -21
  27. package/src/modules/devtools/versions.test.ts +74 -0
  28. package/src/modules/devtools/versions.ts +81 -0
  29. package/src/modules/feature-flag/index.test.ts +120 -0
  30. package/src/modules/feature-flag/index.ts +209 -0
  31. package/src/modules/ref-tables.test.ts +66 -0
  32. package/src/modules/ref-tables.ts +69 -0
  33. package/src/modules/sync/engine.ts +101 -37
  34. package/src/modules/sync/events/index.ts +9 -2
  35. package/src/modules/sync/queue/queue-down.ts +5 -4
  36. package/src/modules/sync/queue/queue-up.ts +14 -13
  37. package/src/modules/sync/scheduler.ts +40 -3
  38. package/src/modules/sync/sync.ts +896 -59
  39. package/src/modules/sync/utils.test.ts +269 -2
  40. package/src/modules/sync/utils.ts +182 -17
  41. package/src/otel/index.ts +127 -0
  42. package/src/services/database/database.ts +11 -11
  43. package/src/services/database/events/index.ts +2 -1
  44. package/src/services/database/local-migrator.ts +21 -21
  45. package/src/services/database/local.test.ts +33 -0
  46. package/src/services/database/local.ts +192 -32
  47. package/src/services/database/remote.ts +13 -13
  48. package/src/services/logger/index.ts +6 -101
  49. package/src/services/persistence/localstorage.ts +2 -2
  50. package/src/services/persistence/resilient.ts +41 -0
  51. package/src/services/persistence/surrealdb.ts +9 -9
  52. package/src/services/stream-processor/index.ts +248 -37
  53. package/src/services/stream-processor/permissions.test.ts +47 -0
  54. package/src/services/stream-processor/permissions.ts +53 -0
  55. package/src/services/stream-processor/stream-processor.batch.test.ts +136 -0
  56. package/src/services/stream-processor/stream-processor.test.ts +1 -1
  57. package/src/services/stream-processor/wasm-types.ts +18 -2
  58. package/src/sp00ky.auth-order.test.ts +65 -0
  59. package/src/sp00ky.ts +648 -0
  60. package/src/types.ts +197 -15
  61. package/src/utils/index.ts +42 -13
  62. package/src/utils/parser.ts +3 -2
  63. package/src/utils/surql.ts +24 -15
  64. package/src/utils/withRetry.test.ts +1 -1
  65. package/tsdown.config.ts +55 -1
  66. package/src/spooky.ts +0 -346
package/src/spooky.ts DELETED
@@ -1,346 +0,0 @@
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
- }