@syncular/client-react 0.0.1 → 0.0.2-126
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 +30 -0
- package/dist/createSyncularReact.d.ts +6 -3
- package/dist/createSyncularReact.d.ts.map +1 -1
- package/dist/createSyncularReact.js +55 -13
- package/dist/createSyncularReact.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +35 -12
- package/src/__tests__/SyncEngine.test.ts +709 -30
- package/src/__tests__/SyncProvider.strictmode.test.tsx +3 -3
- package/src/__tests__/fingerprint.test.ts +4 -4
- package/src/__tests__/hooks/useMutation.test.tsx +3 -3
- package/src/__tests__/hooks.test.tsx +98 -3
- package/src/__tests__/integration/provider-reconfig.test.ts +17 -29
- package/src/__tests__/integration/push-flow.test.ts +20 -19
- package/src/__tests__/integration/test-setup.ts +95 -46
- package/src/__tests__/test-utils.ts +35 -23
- package/src/__tests__/useMutations.test.tsx +3 -3
- package/src/createSyncularReact.tsx +70 -15
- package/src/index.ts +27 -0
|
@@ -21,7 +21,7 @@ import React from 'react';
|
|
|
21
21
|
import { createSyncularReact } from '../index';
|
|
22
22
|
import {
|
|
23
23
|
createMockDb,
|
|
24
|
-
|
|
24
|
+
createMockHandlerRegistry,
|
|
25
25
|
createMockTransport,
|
|
26
26
|
} from './test-utils';
|
|
27
27
|
|
|
@@ -66,14 +66,14 @@ describe('SyncProvider (StrictMode)', () => {
|
|
|
66
66
|
|
|
67
67
|
function renderWithProvider(node: React.ReactNode) {
|
|
68
68
|
const transport = createMockTransport();
|
|
69
|
-
const
|
|
69
|
+
const handlers = createMockHandlerRegistry();
|
|
70
70
|
|
|
71
71
|
return render(
|
|
72
72
|
<React.StrictMode>
|
|
73
73
|
<SyncProvider
|
|
74
74
|
db={db}
|
|
75
75
|
transport={transport}
|
|
76
|
-
|
|
76
|
+
handlers={handlers}
|
|
77
77
|
actorId="test-actor"
|
|
78
78
|
clientId="test-client"
|
|
79
79
|
subscriptions={[]}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import type { Kysely } from 'kysely';
|
|
13
13
|
import {
|
|
14
14
|
createMockDb,
|
|
15
|
-
|
|
15
|
+
createMockHandlerRegistry,
|
|
16
16
|
createMockTransport,
|
|
17
17
|
} from './test-utils';
|
|
18
18
|
|
|
@@ -32,7 +32,7 @@ describe('fingerprint utilities', () => {
|
|
|
32
32
|
const config: SyncEngineConfig = {
|
|
33
33
|
db,
|
|
34
34
|
transport: createMockTransport(),
|
|
35
|
-
|
|
35
|
+
handlers: createMockHandlerRegistry(),
|
|
36
36
|
actorId: 'test-actor',
|
|
37
37
|
clientId: 'test-client',
|
|
38
38
|
subscriptions: [],
|
|
@@ -108,11 +108,11 @@ describe('fingerprint utilities', () => {
|
|
|
108
108
|
await engine.start();
|
|
109
109
|
|
|
110
110
|
// Simulate a mutation by calling applyLocalMutation
|
|
111
|
-
// This requires the
|
|
111
|
+
// This requires the table handler to exist, so we'll test getMutationTimestamp directly
|
|
112
112
|
const beforeMutation = engine.getMutationTimestamp('tasks', 'abc');
|
|
113
113
|
expect(beforeMutation).toBe(0);
|
|
114
114
|
|
|
115
|
-
// We can't easily test the full mutation flow without proper
|
|
115
|
+
// We can't easily test the full mutation flow without proper handler setup,
|
|
116
116
|
// but we can verify the fingerprint changes when timestamps change
|
|
117
117
|
});
|
|
118
118
|
|
|
@@ -20,7 +20,7 @@ import type { ReactNode } from 'react';
|
|
|
20
20
|
import { createSyncularReact } from '../../index';
|
|
21
21
|
import {
|
|
22
22
|
createMockDb,
|
|
23
|
-
|
|
23
|
+
createMockHandlerRegistry,
|
|
24
24
|
createMockTransport,
|
|
25
25
|
} from '../test-utils';
|
|
26
26
|
|
|
@@ -59,13 +59,13 @@ describe('useMutation', () => {
|
|
|
59
59
|
|
|
60
60
|
function createWrapper() {
|
|
61
61
|
const transport = createMockTransport();
|
|
62
|
-
const
|
|
62
|
+
const handlers = createMockHandlerRegistry<TestDb>();
|
|
63
63
|
|
|
64
64
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
65
65
|
<SyncProvider
|
|
66
66
|
db={db}
|
|
67
67
|
transport={transport}
|
|
68
|
-
|
|
68
|
+
handlers={handlers}
|
|
69
69
|
actorId="test-actor"
|
|
70
70
|
clientId="test-client"
|
|
71
71
|
subscriptions={[]}
|
|
@@ -13,14 +13,16 @@ import type { ReactNode } from 'react';
|
|
|
13
13
|
import { createSyncularReact } from '../index';
|
|
14
14
|
import {
|
|
15
15
|
createMockDb,
|
|
16
|
-
|
|
16
|
+
createMockHandlerRegistry,
|
|
17
17
|
createMockTransport,
|
|
18
18
|
} from './test-utils';
|
|
19
19
|
|
|
20
20
|
const {
|
|
21
21
|
SyncProvider,
|
|
22
22
|
useConflicts,
|
|
23
|
+
useEngine,
|
|
23
24
|
useOutbox,
|
|
25
|
+
usePresenceWithJoin,
|
|
24
26
|
useResolveConflict,
|
|
25
27
|
useSyncConnection,
|
|
26
28
|
useSyncEngine,
|
|
@@ -36,13 +38,13 @@ describe('React Hooks', () => {
|
|
|
36
38
|
|
|
37
39
|
function createWrapper(options?: { autoStart?: boolean }) {
|
|
38
40
|
const transport = createMockTransport();
|
|
39
|
-
const
|
|
41
|
+
const handlers = createMockHandlerRegistry();
|
|
40
42
|
|
|
41
43
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
42
44
|
<SyncProvider
|
|
43
45
|
db={db}
|
|
44
46
|
transport={transport}
|
|
45
|
-
|
|
47
|
+
handlers={handlers}
|
|
46
48
|
actorId="test-actor"
|
|
47
49
|
clientId="test-client"
|
|
48
50
|
subscriptions={[]}
|
|
@@ -151,6 +153,99 @@ describe('React Hooks', () => {
|
|
|
151
153
|
// These hook tests verify the React binding works
|
|
152
154
|
});
|
|
153
155
|
|
|
156
|
+
describe('usePresenceWithJoin', () => {
|
|
157
|
+
it('does not re-join when metadata object identity changes with equal values', async () => {
|
|
158
|
+
let joinCalls = 0;
|
|
159
|
+
let leaveCalls = 0;
|
|
160
|
+
|
|
161
|
+
const { result, rerender } = renderHook(
|
|
162
|
+
({ metadata }: { metadata: { displayName: string } }) => {
|
|
163
|
+
const engine = useEngine();
|
|
164
|
+
const presence = usePresenceWithJoin('room:1', {
|
|
165
|
+
metadata,
|
|
166
|
+
autoJoin: true,
|
|
167
|
+
});
|
|
168
|
+
return { engine, presence };
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
wrapper: createWrapper(),
|
|
172
|
+
initialProps: { metadata: { displayName: 'Alice' } },
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(result.current.presence.isJoined).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const originalJoin = result.current.engine.joinPresence.bind(
|
|
181
|
+
result.current.engine
|
|
182
|
+
);
|
|
183
|
+
const originalLeave = result.current.engine.leavePresence.bind(
|
|
184
|
+
result.current.engine
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
result.current.engine.joinPresence = (scopeKey, metadata) => {
|
|
188
|
+
joinCalls += 1;
|
|
189
|
+
originalJoin(scopeKey, metadata);
|
|
190
|
+
};
|
|
191
|
+
result.current.engine.leavePresence = (scopeKey) => {
|
|
192
|
+
leaveCalls += 1;
|
|
193
|
+
originalLeave(scopeKey);
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
rerender({ metadata: { displayName: 'Alice' } });
|
|
197
|
+
await act(async () => {
|
|
198
|
+
await Promise.resolve();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(joinCalls).toBe(0);
|
|
202
|
+
expect(leaveCalls).toBe(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('updates metadata when auto-join metadata changes', async () => {
|
|
206
|
+
let updateCalls = 0;
|
|
207
|
+
|
|
208
|
+
const { result, rerender } = renderHook(
|
|
209
|
+
({ metadata }: { metadata: { displayName: string } }) => {
|
|
210
|
+
const engine = useEngine();
|
|
211
|
+
const presence = usePresenceWithJoin('room:2', {
|
|
212
|
+
metadata,
|
|
213
|
+
autoJoin: true,
|
|
214
|
+
});
|
|
215
|
+
return { engine, presence };
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
wrapper: createWrapper(),
|
|
219
|
+
initialProps: { metadata: { displayName: 'Alice' } },
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(result.current.presence.isJoined).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const originalUpdate = result.current.engine.updatePresenceMetadata.bind(
|
|
228
|
+
result.current.engine
|
|
229
|
+
);
|
|
230
|
+
result.current.engine.updatePresenceMetadata = (scopeKey, metadata) => {
|
|
231
|
+
updateCalls += 1;
|
|
232
|
+
originalUpdate(scopeKey, metadata);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
rerender({ metadata: { displayName: 'Bob' } });
|
|
236
|
+
|
|
237
|
+
await waitFor(() => {
|
|
238
|
+
expect(updateCalls).toBe(1);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
await waitFor(() => {
|
|
242
|
+
expect(result.current.presence.presence[0]?.metadata).toMatchObject({
|
|
243
|
+
displayName: 'Bob',
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
154
249
|
describe('useOutbox', () => {
|
|
155
250
|
it('should return outbox stats', async () => {
|
|
156
251
|
const { result } = renderHook(() => useOutbox(), {
|
|
@@ -46,12 +46,12 @@ function createMockClientTableRegistry(): ClientTableRegistry<ClientDb> {
|
|
|
46
46
|
describe('SyncProvider Reconfiguration', () => {
|
|
47
47
|
let server: TestServer;
|
|
48
48
|
let db: Kysely<ClientDb>;
|
|
49
|
-
let
|
|
49
|
+
let mockHandlers: ClientTableRegistry<ClientDb>;
|
|
50
50
|
|
|
51
51
|
beforeEach(async () => {
|
|
52
52
|
server = await createTestServer();
|
|
53
53
|
db = createBunSqliteDb<ClientDb>({ path: ':memory:' });
|
|
54
|
-
|
|
54
|
+
mockHandlers = createMockClientTableRegistry();
|
|
55
55
|
|
|
56
56
|
await ensureClientSyncSchema(db);
|
|
57
57
|
|
|
@@ -77,14 +77,8 @@ describe('SyncProvider Reconfiguration', () => {
|
|
|
77
77
|
it('SyncEngine recreates when actorId changes', async () => {
|
|
78
78
|
// Create a simple transport for testing
|
|
79
79
|
const transport = {
|
|
80
|
-
async
|
|
81
|
-
return {
|
|
82
|
-
ok: true as const,
|
|
83
|
-
subscriptions: [],
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
async push() {
|
|
87
|
-
return { ok: true as const, status: 'applied' as const, results: [] };
|
|
80
|
+
async sync() {
|
|
81
|
+
return { ok: true as const };
|
|
88
82
|
},
|
|
89
83
|
async fetchSnapshotChunk() {
|
|
90
84
|
return new Uint8Array();
|
|
@@ -95,7 +89,7 @@ describe('SyncProvider Reconfiguration', () => {
|
|
|
95
89
|
const engine1 = new SyncEngine({
|
|
96
90
|
db,
|
|
97
91
|
transport,
|
|
98
|
-
|
|
92
|
+
handlers: mockHandlers,
|
|
99
93
|
actorId: 'user-1',
|
|
100
94
|
clientId: 'client-1',
|
|
101
95
|
subscriptions: [],
|
|
@@ -110,7 +104,7 @@ describe('SyncProvider Reconfiguration', () => {
|
|
|
110
104
|
const engine2 = new SyncEngine({
|
|
111
105
|
db,
|
|
112
106
|
transport,
|
|
113
|
-
|
|
107
|
+
handlers: mockHandlers,
|
|
114
108
|
actorId: 'user-2',
|
|
115
109
|
clientId: 'client-1',
|
|
116
110
|
subscriptions: [],
|
|
@@ -163,11 +157,8 @@ describe('SyncProvider Reconfiguration', () => {
|
|
|
163
157
|
|
|
164
158
|
it('engine config is immutable after creation', () => {
|
|
165
159
|
const transport = {
|
|
166
|
-
async
|
|
167
|
-
return { ok: true as const
|
|
168
|
-
},
|
|
169
|
-
async push() {
|
|
170
|
-
return { ok: true as const, status: 'applied' as const, results: [] };
|
|
160
|
+
async sync() {
|
|
161
|
+
return { ok: true as const };
|
|
171
162
|
},
|
|
172
163
|
async fetchSnapshotChunk() {
|
|
173
164
|
return new Uint8Array();
|
|
@@ -177,7 +168,7 @@ describe('SyncProvider Reconfiguration', () => {
|
|
|
177
168
|
const engine = new SyncEngine({
|
|
178
169
|
db,
|
|
179
170
|
transport,
|
|
180
|
-
|
|
171
|
+
handlers: mockHandlers,
|
|
181
172
|
actorId: 'user-1',
|
|
182
173
|
clientId: 'client-1',
|
|
183
174
|
subscriptions: [],
|
|
@@ -197,13 +188,10 @@ describe('SyncProvider Reconfiguration', () => {
|
|
|
197
188
|
|
|
198
189
|
describe('SyncProvider React render tests', () => {
|
|
199
190
|
let db: Kysely<ClientDb>;
|
|
200
|
-
let
|
|
191
|
+
let mockHandlers: ClientTableRegistry<ClientDb>;
|
|
201
192
|
const mockTransport = {
|
|
202
|
-
async
|
|
203
|
-
return { ok: true as const
|
|
204
|
-
},
|
|
205
|
-
async push() {
|
|
206
|
-
return { ok: true as const, status: 'applied' as const, results: [] };
|
|
193
|
+
async sync() {
|
|
194
|
+
return { ok: true as const };
|
|
207
195
|
},
|
|
208
196
|
async fetchSnapshotChunk() {
|
|
209
197
|
return new Uint8Array();
|
|
@@ -212,7 +200,7 @@ describe('SyncProvider React render tests', () => {
|
|
|
212
200
|
|
|
213
201
|
beforeEach(async () => {
|
|
214
202
|
db = createBunSqliteDb<ClientDb>({ path: ':memory:' });
|
|
215
|
-
|
|
203
|
+
mockHandlers = new ClientTableRegistry<ClientDb>();
|
|
216
204
|
await ensureClientSyncSchema(db);
|
|
217
205
|
});
|
|
218
206
|
|
|
@@ -229,7 +217,7 @@ describe('SyncProvider React render tests', () => {
|
|
|
229
217
|
createElement(SyncProvider, {
|
|
230
218
|
db,
|
|
231
219
|
transport: mockTransport,
|
|
232
|
-
|
|
220
|
+
handlers: mockHandlers,
|
|
233
221
|
actorId: 'user-1',
|
|
234
222
|
clientId: 'client-1',
|
|
235
223
|
autoStart: false, // Disable auto-start for faster test
|
|
@@ -244,7 +232,7 @@ describe('SyncProvider React render tests', () => {
|
|
|
244
232
|
createElement(SyncProvider, {
|
|
245
233
|
db,
|
|
246
234
|
transport: mockTransport,
|
|
247
|
-
|
|
235
|
+
handlers: mockHandlers,
|
|
248
236
|
actorId: 'user-2', // Changed!
|
|
249
237
|
clientId: 'client-1',
|
|
250
238
|
autoStart: false,
|
|
@@ -261,7 +249,7 @@ describe('SyncProvider React render tests', () => {
|
|
|
261
249
|
createElement(SyncProvider, {
|
|
262
250
|
db,
|
|
263
251
|
transport: mockTransport,
|
|
264
|
-
|
|
252
|
+
handlers: mockHandlers,
|
|
265
253
|
actorId: 'user-1',
|
|
266
254
|
clientId: 'client-1',
|
|
267
255
|
autoStart: false,
|
|
@@ -277,7 +265,7 @@ describe('SyncProvider React render tests', () => {
|
|
|
277
265
|
createElement(SyncProvider, {
|
|
278
266
|
db,
|
|
279
267
|
transport: mockTransport,
|
|
280
|
-
|
|
268
|
+
handlers: mockHandlers,
|
|
281
269
|
actorId: 'user-1',
|
|
282
270
|
clientId: 'client-1',
|
|
283
271
|
autoStart: false,
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
enqueueOutboxCommit,
|
|
10
10
|
getNextSendableOutboxCommit,
|
|
11
11
|
} from '@syncular/client';
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
import {
|
|
14
14
|
createTestClient,
|
|
15
15
|
createTestServer,
|
|
@@ -109,8 +109,8 @@ describe('Push Flow', () => {
|
|
|
109
109
|
|
|
110
110
|
it('sync updates local row with server version after push', async () => {
|
|
111
111
|
// First, apply the mutation locally (this is what useMutation does)
|
|
112
|
-
const
|
|
113
|
-
const handler =
|
|
112
|
+
const handlers = client.handlers;
|
|
113
|
+
const handler = handlers.get('tasks');
|
|
114
114
|
|
|
115
115
|
await client.db.transaction().execute(async (trx) => {
|
|
116
116
|
await handler!.applyChange(
|
|
@@ -243,31 +243,32 @@ describe('Push Flow', () => {
|
|
|
243
243
|
clientId: 'retriable-client',
|
|
244
244
|
});
|
|
245
245
|
|
|
246
|
-
// Override the transport to return retriable errors
|
|
247
|
-
const
|
|
246
|
+
// Override the transport to return retriable errors on push
|
|
247
|
+
const originalSync = retriableClient.transport.sync.bind(
|
|
248
248
|
retriableClient.transport
|
|
249
249
|
);
|
|
250
250
|
let retriableErrorCount = 0;
|
|
251
|
-
retriableClient.transport.
|
|
252
|
-
request
|
|
253
|
-
|
|
254
|
-
if (retriableErrorCount < 2) {
|
|
255
|
-
// Return retriable error for first two attempts
|
|
251
|
+
retriableClient.transport.sync = async (request) => {
|
|
252
|
+
if (request.push && retriableErrorCount < 2) {
|
|
253
|
+
// Return retriable error for first two push attempts
|
|
256
254
|
retriableErrorCount++;
|
|
257
255
|
return {
|
|
258
256
|
ok: true as const,
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
257
|
+
push: {
|
|
258
|
+
ok: true as const,
|
|
259
|
+
status: 'rejected' as const,
|
|
260
|
+
results: request.push.operations.map((_, i) => ({
|
|
261
|
+
opIndex: i,
|
|
262
|
+
status: 'error' as const,
|
|
263
|
+
error: 'TEMPORARY_FAILURE',
|
|
264
|
+
code: 'TEMPORARY',
|
|
265
|
+
retriable: true,
|
|
266
|
+
})),
|
|
267
|
+
},
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
270
|
// After that, use the real transport
|
|
270
|
-
return
|
|
271
|
+
return originalSync(request);
|
|
271
272
|
};
|
|
272
273
|
|
|
273
274
|
await retriableClient.engine.start();
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
import type {
|
|
9
9
|
SyncClientDb,
|
|
10
10
|
SyncClientPlugin,
|
|
11
|
-
SyncPullRequest,
|
|
12
|
-
SyncPushRequest,
|
|
13
11
|
SyncTransport,
|
|
14
12
|
} from '@syncular/client';
|
|
15
13
|
import {
|
|
@@ -27,6 +25,7 @@ import {
|
|
|
27
25
|
ensureSyncSchema,
|
|
28
26
|
pull,
|
|
29
27
|
pushCommit,
|
|
28
|
+
readSnapshotChunk,
|
|
30
29
|
recordClientCursor,
|
|
31
30
|
type ServerSyncDialect,
|
|
32
31
|
type ServerTableHandler,
|
|
@@ -73,7 +72,7 @@ export interface TestServer {
|
|
|
73
72
|
/** Full database instance with app tables (also includes sync tables) */
|
|
74
73
|
db: Kysely<ServerDb>;
|
|
75
74
|
dialect: ServerSyncDialect;
|
|
76
|
-
|
|
75
|
+
handlers: TableRegistry<ServerDb>;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
@@ -84,14 +83,14 @@ export interface TestClient {
|
|
|
84
83
|
db: Kysely<ClientDb>;
|
|
85
84
|
engine: SyncEngine<ClientDb>;
|
|
86
85
|
transport: SyncTransport;
|
|
87
|
-
/** Client
|
|
88
|
-
|
|
86
|
+
/** Client handler registry */
|
|
87
|
+
handlers: ClientTableRegistry<ClientDb>;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
/**
|
|
92
|
-
* Server-side tasks
|
|
91
|
+
* Server-side tasks table handler for tests
|
|
93
92
|
*/
|
|
94
|
-
const
|
|
93
|
+
const tasksServerHandler: ServerTableHandler<ServerDb> = {
|
|
95
94
|
table: 'tasks',
|
|
96
95
|
scopePatterns: ['user:{user_id}'],
|
|
97
96
|
|
|
@@ -346,14 +345,14 @@ export async function createTestServer(): Promise<TestServer> {
|
|
|
346
345
|
.addColumn('server_version', 'integer', (col) => col.notNull().defaultTo(1))
|
|
347
346
|
.execute();
|
|
348
347
|
|
|
349
|
-
// Register
|
|
350
|
-
const
|
|
351
|
-
|
|
348
|
+
// Register handlers
|
|
349
|
+
const handlers = new TableRegistry<ServerDb>();
|
|
350
|
+
handlers.register(tasksServerHandler);
|
|
352
351
|
|
|
353
352
|
return {
|
|
354
353
|
db,
|
|
355
354
|
dialect,
|
|
356
|
-
|
|
355
|
+
handlers,
|
|
357
356
|
};
|
|
358
357
|
}
|
|
359
358
|
|
|
@@ -366,39 +365,89 @@ function createInProcessTransport(
|
|
|
366
365
|
): SyncTransport {
|
|
367
366
|
const syncDb = server.db;
|
|
368
367
|
|
|
368
|
+
async function streamToBytes(
|
|
369
|
+
stream: ReadableStream<Uint8Array>
|
|
370
|
+
): Promise<Uint8Array> {
|
|
371
|
+
const reader = stream.getReader();
|
|
372
|
+
try {
|
|
373
|
+
const chunks: Uint8Array[] = [];
|
|
374
|
+
let total = 0;
|
|
375
|
+
while (true) {
|
|
376
|
+
const { done, value } = await reader.read();
|
|
377
|
+
if (done) break;
|
|
378
|
+
if (!value) continue;
|
|
379
|
+
chunks.push(value);
|
|
380
|
+
total += value.length;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const merged = new Uint8Array(total);
|
|
384
|
+
let offset = 0;
|
|
385
|
+
for (const chunk of chunks) {
|
|
386
|
+
merged.set(chunk, offset);
|
|
387
|
+
offset += chunk.length;
|
|
388
|
+
}
|
|
389
|
+
return merged;
|
|
390
|
+
} finally {
|
|
391
|
+
reader.releaseLock();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
369
395
|
return {
|
|
370
|
-
async
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
396
|
+
async sync(request) {
|
|
397
|
+
const result: { ok: true; push?: any; pull?: any } = { ok: true };
|
|
398
|
+
|
|
399
|
+
if (request.push) {
|
|
400
|
+
const pushed = await pushCommit({
|
|
401
|
+
db: syncDb,
|
|
402
|
+
dialect: server.dialect,
|
|
403
|
+
handlers: server.handlers,
|
|
404
|
+
actorId,
|
|
405
|
+
request: {
|
|
406
|
+
clientId: request.clientId,
|
|
407
|
+
clientCommitId: request.push.clientCommitId,
|
|
408
|
+
operations: request.push.operations,
|
|
409
|
+
schemaVersion: request.push.schemaVersion,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
result.push = pushed.response;
|
|
413
|
+
}
|
|
388
414
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
415
|
+
if (request.pull) {
|
|
416
|
+
const pulled = await pull({
|
|
417
|
+
db: syncDb,
|
|
418
|
+
dialect: server.dialect,
|
|
419
|
+
handlers: server.handlers,
|
|
420
|
+
actorId,
|
|
421
|
+
request: {
|
|
422
|
+
clientId: request.clientId,
|
|
423
|
+
...request.pull,
|
|
424
|
+
},
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
recordClientCursor(syncDb, server.dialect, {
|
|
428
|
+
clientId: request.clientId,
|
|
429
|
+
actorId,
|
|
430
|
+
cursor: pulled.clientCursor,
|
|
431
|
+
effectiveScopes: pulled.effectiveScopes,
|
|
432
|
+
}).catch(() => {});
|
|
433
|
+
|
|
434
|
+
result.pull = pulled.response;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return result;
|
|
398
438
|
},
|
|
399
439
|
|
|
400
|
-
async fetchSnapshotChunk() {
|
|
401
|
-
|
|
440
|
+
async fetchSnapshotChunk(request) {
|
|
441
|
+
const chunk = await readSnapshotChunk(syncDb, request.chunkId);
|
|
442
|
+
if (!chunk) {
|
|
443
|
+
throw new Error(`Snapshot chunk not found: ${request.chunkId}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (chunk.body instanceof Uint8Array) {
|
|
447
|
+
return new Uint8Array(chunk.body);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return streamToBytes(chunk.body);
|
|
402
451
|
},
|
|
403
452
|
};
|
|
404
453
|
}
|
|
@@ -429,9 +478,9 @@ export async function createTestClient(
|
|
|
429
478
|
.addColumn('server_version', 'integer', (col) => col.notNull().defaultTo(0))
|
|
430
479
|
.execute();
|
|
431
480
|
|
|
432
|
-
// Create client
|
|
433
|
-
const
|
|
434
|
-
|
|
481
|
+
// Create client handler registry
|
|
482
|
+
const handlers = new ClientTableRegistry<ClientDb>();
|
|
483
|
+
handlers.register({
|
|
435
484
|
table: 'tasks',
|
|
436
485
|
|
|
437
486
|
async applySnapshot(ctx, snapshot) {
|
|
@@ -509,11 +558,11 @@ export async function createTestClient(
|
|
|
509
558
|
const config: SyncEngineConfig<ClientDb> = {
|
|
510
559
|
db,
|
|
511
560
|
transport,
|
|
512
|
-
|
|
561
|
+
handlers,
|
|
513
562
|
actorId: options.actorId,
|
|
514
563
|
clientId: options.clientId,
|
|
515
564
|
subscriptions: [
|
|
516
|
-
{ id: 'my-tasks',
|
|
565
|
+
{ id: 'my-tasks', table: 'tasks', scopes: { user_id: options.actorId } },
|
|
517
566
|
],
|
|
518
567
|
pollIntervalMs: 999999, // Disable polling for tests
|
|
519
568
|
realtimeEnabled: false,
|
|
@@ -522,7 +571,7 @@ export async function createTestClient(
|
|
|
522
571
|
|
|
523
572
|
const engine = new SyncEngine<ClientDb>(config);
|
|
524
573
|
|
|
525
|
-
return { db, engine, transport,
|
|
574
|
+
return { db, engine, transport, handlers };
|
|
526
575
|
}
|
|
527
576
|
|
|
528
577
|
/**
|