@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.
Files changed (47) hide show
  1. package/README.md +21 -0
  2. package/dist/index.d.ts +590 -0
  3. package/dist/index.js +3082 -0
  4. package/package.json +46 -0
  5. package/src/events/events.test.ts +242 -0
  6. package/src/events/index.ts +261 -0
  7. package/src/index.ts +3 -0
  8. package/src/modules/auth/events/index.ts +18 -0
  9. package/src/modules/auth/index.ts +267 -0
  10. package/src/modules/cache/index.ts +241 -0
  11. package/src/modules/cache/types.ts +19 -0
  12. package/src/modules/data/data.test.ts +58 -0
  13. package/src/modules/data/index.ts +777 -0
  14. package/src/modules/devtools/index.ts +364 -0
  15. package/src/modules/sync/engine.ts +163 -0
  16. package/src/modules/sync/events/index.ts +77 -0
  17. package/src/modules/sync/index.ts +3 -0
  18. package/src/modules/sync/queue/index.ts +2 -0
  19. package/src/modules/sync/queue/queue-down.ts +89 -0
  20. package/src/modules/sync/queue/queue-up.ts +223 -0
  21. package/src/modules/sync/scheduler.ts +84 -0
  22. package/src/modules/sync/sync.ts +407 -0
  23. package/src/modules/sync/utils.test.ts +311 -0
  24. package/src/modules/sync/utils.ts +171 -0
  25. package/src/services/database/database.ts +108 -0
  26. package/src/services/database/events/index.ts +32 -0
  27. package/src/services/database/index.ts +5 -0
  28. package/src/services/database/local-migrator.ts +203 -0
  29. package/src/services/database/local.ts +99 -0
  30. package/src/services/database/remote.ts +110 -0
  31. package/src/services/logger/index.ts +118 -0
  32. package/src/services/persistence/localstorage.ts +26 -0
  33. package/src/services/persistence/surrealdb.ts +62 -0
  34. package/src/services/stream-processor/index.ts +364 -0
  35. package/src/services/stream-processor/stream-processor.test.ts +140 -0
  36. package/src/services/stream-processor/wasm-types.ts +31 -0
  37. package/src/spooky.ts +346 -0
  38. package/src/types.ts +237 -0
  39. package/src/utils/error-classification.ts +28 -0
  40. package/src/utils/index.ts +172 -0
  41. package/src/utils/parser.test.ts +125 -0
  42. package/src/utils/parser.ts +46 -0
  43. package/src/utils/surql.ts +182 -0
  44. package/src/utils/utils.test.ts +152 -0
  45. package/src/utils/withRetry.test.ts +153 -0
  46. package/tsconfig.json +14 -0
  47. package/tsdown.config.ts +9 -0
@@ -0,0 +1,311 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { RecordId } from 'surrealdb';
3
+ import {
4
+ diffRecordVersionArray,
5
+ applyRecordVersionDiff,
6
+ createDiffFromDbOp,
7
+ ArraySyncer,
8
+ } from './utils';
9
+ import { RecordVersionArray, RecordVersionDiff } from '../../types';
10
+ import { encodeRecordId } from '../../utils/index';
11
+
12
+ function rid(table: string, id: string): RecordId<string> {
13
+ return new RecordId(table, id);
14
+ }
15
+
16
+ describe('diffRecordVersionArray', () => {
17
+ it('detects added records (in remote, not local)', () => {
18
+ const local: RecordVersionArray = [['user:1', 1]];
19
+ const remote: RecordVersionArray = [
20
+ ['user:1', 1],
21
+ ['user:2', 1],
22
+ ];
23
+ const diff = diffRecordVersionArray(local, remote);
24
+
25
+ expect(diff.added).toHaveLength(1);
26
+ expect(encodeRecordId(diff.added[0].id)).toBe('user:2');
27
+ expect(diff.added[0].version).toBe(1);
28
+ expect(diff.updated).toHaveLength(0);
29
+ expect(diff.removed).toHaveLength(0);
30
+ });
31
+
32
+ it('detects updated records (remote version > local version)', () => {
33
+ const local: RecordVersionArray = [['user:1', 1]];
34
+ const remote: RecordVersionArray = [['user:1', 3]];
35
+ const diff = diffRecordVersionArray(local, remote);
36
+
37
+ expect(diff.updated).toHaveLength(1);
38
+ expect(encodeRecordId(diff.updated[0].id)).toBe('user:1');
39
+ expect(diff.updated[0].version).toBe(3);
40
+ expect(diff.added).toHaveLength(0);
41
+ expect(diff.removed).toHaveLength(0);
42
+ });
43
+
44
+ it('detects removed records (in local, not remote)', () => {
45
+ const local: RecordVersionArray = [
46
+ ['user:1', 1],
47
+ ['user:2', 1],
48
+ ];
49
+ const remote: RecordVersionArray = [['user:1', 1]];
50
+ const diff = diffRecordVersionArray(local, remote);
51
+
52
+ expect(diff.removed).toHaveLength(1);
53
+ expect(encodeRecordId(diff.removed[0])).toBe('user:2');
54
+ expect(diff.added).toHaveLength(0);
55
+ expect(diff.updated).toHaveLength(0);
56
+ });
57
+
58
+ it('handles null arrays', () => {
59
+ const diff = diffRecordVersionArray(null, null);
60
+ expect(diff.added).toHaveLength(0);
61
+ expect(diff.updated).toHaveLength(0);
62
+ expect(diff.removed).toHaveLength(0);
63
+ });
64
+
65
+ it('handles empty arrays', () => {
66
+ const diff = diffRecordVersionArray([], []);
67
+ expect(diff.added).toHaveLength(0);
68
+ expect(diff.updated).toHaveLength(0);
69
+ expect(diff.removed).toHaveLength(0);
70
+ });
71
+
72
+ it('no diff when arrays match', () => {
73
+ const arr: RecordVersionArray = [
74
+ ['user:1', 1],
75
+ ['user:2', 2],
76
+ ];
77
+ const diff = diffRecordVersionArray(arr, arr);
78
+ expect(diff.added).toHaveLength(0);
79
+ expect(diff.updated).toHaveLength(0);
80
+ expect(diff.removed).toHaveLength(0);
81
+ });
82
+
83
+ it('handles mixed adds/updates/removes', () => {
84
+ const local: RecordVersionArray = [
85
+ ['user:1', 1],
86
+ ['user:2', 1],
87
+ ['user:3', 1],
88
+ ];
89
+ const remote: RecordVersionArray = [
90
+ ['user:1', 1], // same
91
+ ['user:2', 3], // updated
92
+ // user:3 removed
93
+ ['user:4', 1], // added
94
+ ];
95
+ const diff = diffRecordVersionArray(local, remote);
96
+
97
+ expect(diff.added).toHaveLength(1);
98
+ expect(encodeRecordId(diff.added[0].id)).toBe('user:4');
99
+ expect(diff.updated).toHaveLength(1);
100
+ expect(encodeRecordId(diff.updated[0].id)).toBe('user:2');
101
+ expect(diff.removed).toHaveLength(1);
102
+ expect(encodeRecordId(diff.removed[0])).toBe('user:3');
103
+ });
104
+ });
105
+
106
+ describe('applyRecordVersionDiff', () => {
107
+ it('applies additions', () => {
108
+ const current: RecordVersionArray = [['user:1', 1]];
109
+ const diff: RecordVersionDiff = {
110
+ added: [{ id: rid('user', '2'), version: 1 }],
111
+ updated: [],
112
+ removed: [],
113
+ };
114
+ const result = applyRecordVersionDiff(current, diff);
115
+ expect(result).toEqual([
116
+ ['user:1', 1],
117
+ ['user:2', 1],
118
+ ]);
119
+ });
120
+
121
+ it('applies updates', () => {
122
+ const current: RecordVersionArray = [['user:1', 1]];
123
+ const diff: RecordVersionDiff = {
124
+ added: [],
125
+ updated: [{ id: rid('user', '1'), version: 5 }],
126
+ removed: [],
127
+ };
128
+ const result = applyRecordVersionDiff(current, diff);
129
+ expect(result).toEqual([['user:1', 5]]);
130
+ });
131
+
132
+ it('applies removals', () => {
133
+ const current: RecordVersionArray = [
134
+ ['user:1', 1],
135
+ ['user:2', 2],
136
+ ];
137
+ const diff: RecordVersionDiff = {
138
+ added: [],
139
+ updated: [],
140
+ removed: [rid('user', '1')],
141
+ };
142
+ const result = applyRecordVersionDiff(current, diff);
143
+ expect(result).toEqual([['user:2', 2]]);
144
+ });
145
+
146
+ it('result is sorted by record ID', () => {
147
+ const current: RecordVersionArray = [['user:c', 1]];
148
+ const diff: RecordVersionDiff = {
149
+ added: [
150
+ { id: rid('user', 'a'), version: 1 },
151
+ { id: rid('user', 'z'), version: 1 },
152
+ ],
153
+ updated: [],
154
+ removed: [],
155
+ };
156
+ const result = applyRecordVersionDiff(current, diff);
157
+ expect(result).toEqual([
158
+ ['user:a', 1],
159
+ ['user:c', 1],
160
+ ['user:z', 1],
161
+ ]);
162
+ });
163
+
164
+ it('empty diff returns original (sorted)', () => {
165
+ const current: RecordVersionArray = [
166
+ ['user:b', 2],
167
+ ['user:a', 1],
168
+ ];
169
+ const diff: RecordVersionDiff = { added: [], updated: [], removed: [] };
170
+ const result = applyRecordVersionDiff(current, diff);
171
+ expect(result).toEqual([
172
+ ['user:a', 1],
173
+ ['user:b', 2],
174
+ ]);
175
+ });
176
+ });
177
+
178
+ describe('createDiffFromDbOp', () => {
179
+ it('CREATE populates added array', () => {
180
+ const recordId = rid('user', '1');
181
+ const diff = createDiffFromDbOp('CREATE', recordId, 1);
182
+ expect(diff.added).toHaveLength(1);
183
+ expect(diff.added[0].id).toBe(recordId);
184
+ expect(diff.added[0].version).toBe(1);
185
+ expect(diff.updated).toHaveLength(0);
186
+ expect(diff.removed).toHaveLength(0);
187
+ });
188
+
189
+ it('UPDATE populates updated array', () => {
190
+ const recordId = rid('user', '1');
191
+ const diff = createDiffFromDbOp('UPDATE', recordId, 2);
192
+ expect(diff.updated).toHaveLength(1);
193
+ expect(diff.updated[0].id).toBe(recordId);
194
+ expect(diff.updated[0].version).toBe(2);
195
+ expect(diff.added).toHaveLength(0);
196
+ expect(diff.removed).toHaveLength(0);
197
+ });
198
+
199
+ it('DELETE populates removed array', () => {
200
+ const recordId = rid('user', '1');
201
+ const diff = createDiffFromDbOp('DELETE', recordId, 1);
202
+ expect(diff.removed).toHaveLength(1);
203
+ expect(diff.removed[0]).toBe(recordId);
204
+ expect(diff.added).toHaveLength(0);
205
+ expect(diff.updated).toHaveLength(0);
206
+ });
207
+
208
+ it('skips if existing version >= new version', () => {
209
+ const recordId = rid('user', '1');
210
+ const versions: RecordVersionArray = [['user:1', 5]];
211
+ const diff = createDiffFromDbOp('UPDATE', recordId, 3, versions);
212
+ expect(diff.added).toHaveLength(0);
213
+ expect(diff.updated).toHaveLength(0);
214
+ expect(diff.removed).toHaveLength(0);
215
+ });
216
+
217
+ it('applies if existing version < new version', () => {
218
+ const recordId = rid('user', '1');
219
+ const versions: RecordVersionArray = [['user:1', 2]];
220
+ const diff = createDiffFromDbOp('UPDATE', recordId, 5, versions);
221
+ expect(diff.updated).toHaveLength(1);
222
+ expect(diff.updated[0].version).toBe(5);
223
+ });
224
+ });
225
+
226
+ describe('ArraySyncer', () => {
227
+ it('insert adds to local array', () => {
228
+ const syncer = new ArraySyncer(
229
+ [['user:1', 1]],
230
+ [['user:1', 1]]
231
+ );
232
+
233
+ syncer.insert('user:2', 1);
234
+ const diff = syncer.nextSet();
235
+ expect(diff).not.toBeNull();
236
+ // local now has user:2 which remote does not → user:2 is in local removed from remote perspective
237
+ // Actually: local=[user:1, user:2], remote=[user:1] → user:2 is "removed" (in local, not remote)
238
+ expect(diff!.removed).toHaveLength(1);
239
+ expect(encodeRecordId(diff!.removed[0])).toBe('user:2');
240
+ });
241
+
242
+ it('update modifies version in local array', () => {
243
+ const syncer = new ArraySyncer(
244
+ [['user:1', 1]],
245
+ [['user:1', 1]]
246
+ );
247
+
248
+ syncer.update('user:1', 5);
249
+ const diff = syncer.nextSet();
250
+ // local version (5) > remote version (1), so no "updated" from diff perspective
251
+ // diff finds remote additions/updates relative to local; local version higher means no remote update
252
+ expect(diff).not.toBeNull();
253
+ expect(diff!.added).toHaveLength(0);
254
+ expect(diff!.updated).toHaveLength(0);
255
+ expect(diff!.removed).toHaveLength(0);
256
+ });
257
+
258
+ it('delete removes from local array', () => {
259
+ const syncer = new ArraySyncer(
260
+ [
261
+ ['user:1', 1],
262
+ ['user:2', 1],
263
+ ],
264
+ [
265
+ ['user:1', 1],
266
+ ['user:2', 1],
267
+ ]
268
+ );
269
+
270
+ syncer.delete('user:2');
271
+ const diff = syncer.nextSet();
272
+ expect(diff).not.toBeNull();
273
+ // remote has user:2, local does not → added from remote perspective
274
+ expect(diff!.added).toHaveLength(1);
275
+ expect(encodeRecordId(diff!.added[0].id)).toBe('user:2');
276
+ });
277
+
278
+ it('nextSet returns correct diff against remote', () => {
279
+ const syncer = new ArraySyncer(
280
+ [['user:1', 1]],
281
+ [
282
+ ['user:1', 1],
283
+ ['user:2', 1],
284
+ ]
285
+ );
286
+
287
+ const diff = syncer.nextSet();
288
+ expect(diff).not.toBeNull();
289
+ expect(diff!.added).toHaveLength(1);
290
+ expect(encodeRecordId(diff!.added[0].id)).toBe('user:2');
291
+ });
292
+
293
+ it('maintains sorting after mutations', () => {
294
+ const syncer = new ArraySyncer(
295
+ [['user:c', 1]],
296
+ []
297
+ );
298
+
299
+ syncer.insert('user:a', 1);
300
+ syncer.insert('user:z', 1);
301
+
302
+ // nextSet triggers sort
303
+ const diff = syncer.nextSet();
304
+ // All 3 are in local but not remote → 3 removed items
305
+ expect(diff!.removed).toHaveLength(3);
306
+ // Check they come in sorted order
307
+ const removedIds = diff!.removed.map((r) => encodeRecordId(r));
308
+ const sorted = [...removedIds].sort();
309
+ expect(removedIds).toEqual(sorted);
310
+ });
311
+ });
@@ -0,0 +1,171 @@
1
+ import { RecordId } from 'surrealdb';
2
+ import { RecordVersionArray, RecordVersionDiff } from '../../types';
3
+ import { parseRecordIdString, encodeRecordId } from '../../utils/index';
4
+
5
+ export class ArraySyncer {
6
+ private localArray: RecordVersionArray;
7
+ private remoteArray: RecordVersionArray;
8
+ private needsSort = false;
9
+
10
+ constructor(localArray: RecordVersionArray, remoteArray: RecordVersionArray) {
11
+ this.remoteArray = remoteArray.sort((a, b) => a[0].localeCompare(b[0]));
12
+ this.localArray = localArray.sort((a, b) => a[0].localeCompare(b[0]));
13
+ }
14
+
15
+ /**
16
+ * Inserts an item into the local array
17
+ */
18
+ insert(recordId: string, version: number) {
19
+ this.localArray.push([recordId, version]);
20
+ this.needsSort = true;
21
+ }
22
+
23
+ /**
24
+ * Updates the current local RecordVersionArray state.
25
+ */
26
+ update(recordId: string, version: number) {
27
+ this.localArray = this.localArray.map((record) => {
28
+ if (record[0] === recordId) {
29
+ this.needsSort = true;
30
+ return [recordId, version];
31
+ }
32
+ return record;
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Deletes an item from the local array
38
+ */
39
+ delete(recordId: string) {
40
+ this.localArray = this.localArray.filter((record) => record[0] !== recordId);
41
+ }
42
+
43
+ /**
44
+ * Returns the difference between the local and remote arrays.
45
+ * Includes sets of added, updated, and removed records.
46
+ */
47
+ nextSet(): RecordVersionDiff | null {
48
+ if (this.needsSort) {
49
+ this.localArray.sort((a, b) => a[0].localeCompare(b[0]));
50
+ this.needsSort = false;
51
+ }
52
+ console.log('xxxx555', this.localArray, this.remoteArray);
53
+ const diff = diffRecordVersionArray(this.localArray, this.remoteArray);
54
+ return diff;
55
+ }
56
+ }
57
+
58
+ export function diffRecordVersionArray(
59
+ local: RecordVersionArray | null,
60
+ remote: RecordVersionArray | null
61
+ ): RecordVersionDiff {
62
+ const localArray = local || [];
63
+ const remoteArray = remote || [];
64
+
65
+ // Convert arrays to Maps for O(1) lookup
66
+ const localMap = new Map<string, number>(localArray);
67
+ const remoteMap = new Map<string, number>(remoteArray);
68
+
69
+ const added: string[] = [];
70
+ const updated: string[] = [];
71
+ const removed: string[] = [];
72
+
73
+ // Find added and updated records
74
+ for (const [recordId, remoteVersion] of remoteMap) {
75
+ const localVersion = localMap.get(recordId);
76
+
77
+ if (localVersion === undefined) {
78
+ // Record exists in remote but not in local
79
+ added.push(recordId);
80
+ } else if (localVersion < remoteVersion) {
81
+ // Record exists in both but remote has newer version
82
+ updated.push(recordId);
83
+ }
84
+ }
85
+
86
+ // Find removed records
87
+ for (const [recordId] of localMap) {
88
+ if (!remoteMap.has(recordId)) {
89
+ removed.push(recordId);
90
+ }
91
+ }
92
+
93
+ return {
94
+ added: added.map((id) => ({
95
+ id: parseRecordIdString(id),
96
+ version: remoteMap.get(id)!,
97
+ })),
98
+ updated: updated.map((id) => ({
99
+ id: parseRecordIdString(id),
100
+ version: remoteMap.get(id)!,
101
+ })),
102
+ removed: removed.map(parseRecordIdString),
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Applies a RecordVersionDiff to a RecordVersionArray and returns a new sorted array.
108
+ */
109
+ export function applyRecordVersionDiff(
110
+ current: RecordVersionArray,
111
+ diff: RecordVersionDiff
112
+ ): RecordVersionArray {
113
+ const currentMap = new Map(current);
114
+
115
+ // Apply removals
116
+ for (const id of diff.removed) {
117
+ currentMap.delete(encodeRecordId(id));
118
+ }
119
+
120
+ // Apply additions
121
+ for (const item of diff.added) {
122
+ currentMap.set(encodeRecordId(item.id), item.version);
123
+ }
124
+
125
+ // Apply updates
126
+ for (const item of diff.updated) {
127
+ currentMap.set(encodeRecordId(item.id), item.version);
128
+ }
129
+
130
+ return Array.from(currentMap).sort((a, b) => a[0].localeCompare(b[0]));
131
+ }
132
+
133
+ export function createDiffFromDbOp(
134
+ op: 'CREATE' | 'UPDATE' | 'DELETE',
135
+ recordId: RecordId,
136
+ version: number,
137
+ versions?: RecordVersionArray
138
+ ): RecordVersionDiff {
139
+ // Version guard: skip stale CREATE/UPDATE, but always process DELETE
140
+ if (op !== 'DELETE') {
141
+ const old = versions?.find((record) => record[0] === encodeRecordId(recordId));
142
+
143
+ if (old && old[1] >= version) {
144
+ return {
145
+ added: [],
146
+ updated: [],
147
+ removed: [],
148
+ };
149
+ }
150
+ }
151
+
152
+ if (op === 'CREATE') {
153
+ return {
154
+ added: [{ id: recordId, version }],
155
+ updated: [],
156
+ removed: [],
157
+ };
158
+ } else if (op === 'UPDATE') {
159
+ return {
160
+ added: [],
161
+ updated: [{ id: recordId, version }],
162
+ removed: [],
163
+ };
164
+ } else {
165
+ return {
166
+ added: [],
167
+ updated: [],
168
+ removed: [recordId],
169
+ };
170
+ }
171
+ }
@@ -0,0 +1,108 @@
1
+ import { Surreal, SurrealTransaction } from 'surrealdb';
2
+ import { createLogger, Logger } from '../logger/index';
3
+ import {
4
+ DatabaseEventSystem,
5
+ DatabaseEventTypes,
6
+ DatabaseQueryEventPayload,
7
+ } from './events/index';
8
+ import { SealedQuery } from '../../utils/surql';
9
+
10
+ export abstract class AbstractDatabaseService {
11
+ protected client: Surreal;
12
+ protected logger: Logger;
13
+ protected events: DatabaseEventSystem;
14
+ protected abstract eventType:
15
+ | typeof DatabaseEventTypes.LocalQuery
16
+ | typeof DatabaseEventTypes.RemoteQuery;
17
+
18
+ constructor(client: Surreal, logger: Logger, events: DatabaseEventSystem) {
19
+ this.client = client;
20
+ this.logger = logger.child({ service: 'Database' });
21
+ this.events = events;
22
+ }
23
+
24
+ abstract connect(): Promise<void>;
25
+
26
+ getClient(): Surreal {
27
+ return this.client;
28
+ }
29
+
30
+ getEvents(): DatabaseEventSystem {
31
+ return this.events;
32
+ }
33
+
34
+ tx(): Promise<SurrealTransaction> {
35
+ return this.client.beginTransaction();
36
+ }
37
+
38
+ private queryQueue: Promise<void> = Promise.resolve();
39
+
40
+ /**
41
+ * Execute a query with serialized execution to prevent WASM transaction issues.
42
+ */
43
+ async query<T extends unknown[]>(query: string, vars?: Record<string, unknown>): Promise<T> {
44
+ return new Promise((resolve, reject) => {
45
+ this.queryQueue = this.queryQueue
46
+ .then(async () => {
47
+ const startTime = performance.now();
48
+ try {
49
+ this.logger.debug(
50
+ { query, vars, Category: 'spooky-client::Database::query' },
51
+ 'Executing query'
52
+ );
53
+ const pending = this.client.query(query, vars);
54
+ // In SurrealDB 2.0, .query() collects results by default.
55
+ // We cast to T directly as proper typing depends on the caller knowing the return structure.
56
+ const result = (await pending) as unknown as T;
57
+ const duration = performance.now() - startTime;
58
+
59
+ // Emit query event
60
+ this.events.emit(this.eventType, {
61
+ query,
62
+ vars,
63
+ duration,
64
+ success: true,
65
+ timestamp: Date.now(),
66
+ });
67
+
68
+ resolve(result);
69
+ this.logger.trace(
70
+ { query, result, Category: 'spooky-client::Database::query' },
71
+ 'Query executed successfully'
72
+ );
73
+ } catch (err) {
74
+ const duration = performance.now() - startTime;
75
+
76
+ // Emit query event with error
77
+ this.events.emit(this.eventType, {
78
+ query,
79
+ vars,
80
+ duration,
81
+ success: false,
82
+ error: err instanceof Error ? err.message : String(err),
83
+ timestamp: Date.now(),
84
+ });
85
+
86
+ this.logger.error(
87
+ { query, vars, err, Category: 'spooky-client::Database::query' },
88
+ 'Query execution failed'
89
+ );
90
+ reject(err);
91
+ }
92
+ })
93
+ .catch(() => {
94
+ // Ignore queue errors to keep the chain alive; the specific promise was rejected above.
95
+ });
96
+ });
97
+ }
98
+
99
+ async execute<T>(query: SealedQuery<T>, vars?: Record<string, unknown>): Promise<T> {
100
+ const raw = await this.query<unknown[]>(query.sql, vars);
101
+ return query.extract(raw);
102
+ }
103
+
104
+ async close(): Promise<void> {
105
+ this.logger.info({ Category: 'spooky-client::Database::close' }, 'Closing database connection');
106
+ await this.client.close();
107
+ }
108
+ }
@@ -0,0 +1,32 @@
1
+ import { createEventSystem, EventDefinition, EventSystem } from '../../../events/index';
2
+
3
+ export const DatabaseEventTypes = {
4
+ LocalQuery: 'DATABASE_LOCAL_QUERY',
5
+ RemoteQuery: 'DATABASE_REMOTE_QUERY',
6
+ } as const;
7
+
8
+ export interface DatabaseQueryEventPayload {
9
+ query: string;
10
+ vars?: Record<string, unknown>;
11
+ duration: number;
12
+ success: boolean;
13
+ error?: string;
14
+ timestamp: number;
15
+ }
16
+
17
+ export type DatabaseEventTypeMap = {
18
+ [DatabaseEventTypes.LocalQuery]: EventDefinition<
19
+ typeof DatabaseEventTypes.LocalQuery,
20
+ DatabaseQueryEventPayload
21
+ >;
22
+ [DatabaseEventTypes.RemoteQuery]: EventDefinition<
23
+ typeof DatabaseEventTypes.RemoteQuery,
24
+ DatabaseQueryEventPayload
25
+ >;
26
+ };
27
+
28
+ export type DatabaseEventSystem = EventSystem<DatabaseEventTypeMap>;
29
+
30
+ export function createDatabaseEventSystem(): DatabaseEventSystem {
31
+ return createEventSystem([DatabaseEventTypes.LocalQuery, DatabaseEventTypes.RemoteQuery]);
32
+ }
@@ -0,0 +1,5 @@
1
+ export * from './database';
2
+ export * from './local';
3
+ export * from './remote';
4
+ export * from './local-migrator';
5
+ export * from './events/index';