@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { withRetry } from './index';
|
|
3
|
+
|
|
4
|
+
function createMockLogger() {
|
|
5
|
+
return {
|
|
6
|
+
warn: vi.fn(),
|
|
7
|
+
info: vi.fn(),
|
|
8
|
+
debug: vi.fn(),
|
|
9
|
+
error: vi.fn(),
|
|
10
|
+
fatal: vi.fn(),
|
|
11
|
+
trace: vi.fn(),
|
|
12
|
+
child: vi.fn(),
|
|
13
|
+
} as any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('withRetry', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.useFakeTimers();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
vi.useRealTimers();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('succeeds on first try and returns result', async () => {
|
|
26
|
+
const logger = createMockLogger();
|
|
27
|
+
const operation = vi.fn().mockResolvedValue('ok');
|
|
28
|
+
|
|
29
|
+
const result = await withRetry(logger, operation);
|
|
30
|
+
|
|
31
|
+
expect(result).toBe('ok');
|
|
32
|
+
expect(operation).toHaveBeenCalledOnce();
|
|
33
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('retries on transaction conflict and returns result on success', async () => {
|
|
37
|
+
const logger = createMockLogger();
|
|
38
|
+
const operation = vi
|
|
39
|
+
.fn()
|
|
40
|
+
.mockRejectedValueOnce(new Error('Can not open transaction'))
|
|
41
|
+
.mockResolvedValueOnce('ok');
|
|
42
|
+
|
|
43
|
+
const promise = withRetry(logger, operation, 3, 100);
|
|
44
|
+
|
|
45
|
+
// First call fails, triggers setTimeout(100 * 1)
|
|
46
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
47
|
+
|
|
48
|
+
const result = await promise;
|
|
49
|
+
expect(result).toBe('ok');
|
|
50
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
51
|
+
expect(logger.warn).toHaveBeenCalledOnce();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('retries on "Database is busy" error', async () => {
|
|
55
|
+
const logger = createMockLogger();
|
|
56
|
+
const operation = vi
|
|
57
|
+
.fn()
|
|
58
|
+
.mockRejectedValueOnce(new Error('Database is busy'))
|
|
59
|
+
.mockResolvedValueOnce('ok');
|
|
60
|
+
|
|
61
|
+
const promise = withRetry(logger, operation, 3, 100);
|
|
62
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
63
|
+
|
|
64
|
+
const result = await promise;
|
|
65
|
+
expect(result).toBe('ok');
|
|
66
|
+
expect(operation).toHaveBeenCalledTimes(2);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('retries on generic "transaction" error', async () => {
|
|
70
|
+
const logger = createMockLogger();
|
|
71
|
+
const operation = vi
|
|
72
|
+
.fn()
|
|
73
|
+
.mockRejectedValueOnce(new Error('Some transaction error'))
|
|
74
|
+
.mockResolvedValueOnce('ok');
|
|
75
|
+
|
|
76
|
+
const promise = withRetry(logger, operation, 3, 100);
|
|
77
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
78
|
+
|
|
79
|
+
const result = await promise;
|
|
80
|
+
expect(result).toBe('ok');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('throws immediately on non-retryable error', async () => {
|
|
84
|
+
const logger = createMockLogger();
|
|
85
|
+
const operation = vi.fn().mockRejectedValue(new Error('Not found'));
|
|
86
|
+
|
|
87
|
+
await expect(withRetry(logger, operation)).rejects.toThrow('Not found');
|
|
88
|
+
expect(operation).toHaveBeenCalledOnce();
|
|
89
|
+
expect(logger.warn).not.toHaveBeenCalled();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('exhausts all retries and throws last error', async () => {
|
|
93
|
+
// Use real timers for this test to avoid unhandled rejection timing issues
|
|
94
|
+
vi.useRealTimers();
|
|
95
|
+
const logger = createMockLogger();
|
|
96
|
+
const operation = vi
|
|
97
|
+
.fn()
|
|
98
|
+
.mockRejectedValueOnce(new Error('Can not open transaction'))
|
|
99
|
+
.mockRejectedValueOnce(new Error('Can not open transaction'))
|
|
100
|
+
.mockRejectedValueOnce(new Error('Can not open transaction'));
|
|
101
|
+
|
|
102
|
+
await expect(withRetry(logger, operation, 3, 1)).rejects.toThrow(
|
|
103
|
+
'Can not open transaction'
|
|
104
|
+
);
|
|
105
|
+
expect(operation).toHaveBeenCalledTimes(3);
|
|
106
|
+
expect(logger.warn).toHaveBeenCalledTimes(3);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('uses exponential backoff (delay * attempt)', async () => {
|
|
110
|
+
const logger = createMockLogger();
|
|
111
|
+
let callCount = 0;
|
|
112
|
+
const operation = vi.fn().mockImplementation(() => {
|
|
113
|
+
callCount++;
|
|
114
|
+
if (callCount < 3) {
|
|
115
|
+
return Promise.reject(new Error('transaction conflict'));
|
|
116
|
+
}
|
|
117
|
+
return Promise.resolve('ok');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const promise = withRetry(logger, operation, 3, 100);
|
|
121
|
+
|
|
122
|
+
// First retry: delay = 100 * 1 = 100
|
|
123
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
124
|
+
// Second retry: delay = 100 * 2 = 200
|
|
125
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
126
|
+
|
|
127
|
+
const result = await promise;
|
|
128
|
+
expect(result).toBe('ok');
|
|
129
|
+
expect(operation).toHaveBeenCalledTimes(3);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('logger.warn is called with retry details', async () => {
|
|
133
|
+
const logger = createMockLogger();
|
|
134
|
+
const operation = vi
|
|
135
|
+
.fn()
|
|
136
|
+
.mockRejectedValueOnce(new Error('Can not open transaction'))
|
|
137
|
+
.mockResolvedValueOnce('ok');
|
|
138
|
+
|
|
139
|
+
const promise = withRetry(logger, operation, 3, 100);
|
|
140
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
141
|
+
await promise;
|
|
142
|
+
|
|
143
|
+
expect(logger.warn).toHaveBeenCalledWith(
|
|
144
|
+
expect.objectContaining({
|
|
145
|
+
attempt: 1,
|
|
146
|
+
retries: 3,
|
|
147
|
+
error: 'Can not open transaction',
|
|
148
|
+
Category: 'spooky-client::utils::withRetry',
|
|
149
|
+
}),
|
|
150
|
+
'Retrying DB operation'
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|