@syncular/client-react 0.0.1 → 0.0.2-127
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
|
@@ -24,37 +24,49 @@ export function createMockTransport(
|
|
|
24
24
|
} = {}
|
|
25
25
|
): SyncTransport {
|
|
26
26
|
return {
|
|
27
|
-
async
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
async sync(request) {
|
|
28
|
+
const result: { ok: true; push?: any; pull?: any } = { ok: true };
|
|
29
|
+
|
|
30
|
+
if (request.push) {
|
|
31
|
+
options.onPush?.({
|
|
32
|
+
clientId: request.clientId,
|
|
33
|
+
...request.push,
|
|
34
|
+
} as SyncPushRequest);
|
|
35
|
+
result.push = {
|
|
36
|
+
ok: true,
|
|
37
|
+
status: 'applied',
|
|
38
|
+
results: request.push.operations.map((_, i) => ({
|
|
39
|
+
opIndex: i,
|
|
40
|
+
status: 'applied' as const,
|
|
41
|
+
})),
|
|
42
|
+
...options.pushResponse,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (request.pull) {
|
|
47
|
+
options.onPull?.({
|
|
48
|
+
clientId: request.clientId,
|
|
49
|
+
...request.pull,
|
|
50
|
+
} as SyncPullRequest);
|
|
51
|
+
result.pull = {
|
|
52
|
+
ok: true,
|
|
53
|
+
subscriptions: [],
|
|
54
|
+
...options.pullResponse,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
46
59
|
},
|
|
47
60
|
async fetchSnapshotChunk(): Promise<Uint8Array> {
|
|
48
|
-
// Return empty gzipped NDJSON (empty array)
|
|
49
61
|
return new Uint8Array();
|
|
50
62
|
},
|
|
51
63
|
};
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
|
-
* Create a mock
|
|
67
|
+
* Create a mock handler registry
|
|
56
68
|
*/
|
|
57
|
-
export function
|
|
69
|
+
export function createMockHandlerRegistry<
|
|
58
70
|
DB extends SyncClientDb = SyncClientDb,
|
|
59
71
|
>(): ClientTableRegistry<DB> {
|
|
60
72
|
return new ClientTableRegistry<DB>();
|
|
@@ -83,7 +95,7 @@ export async function createMockDb<
|
|
|
83
95
|
.ifNotExists()
|
|
84
96
|
.addColumn('state_id', 'text', (col) => col.notNull())
|
|
85
97
|
.addColumn('subscription_id', 'text', (col) => col.notNull())
|
|
86
|
-
.addColumn('
|
|
98
|
+
.addColumn('table', 'text', (col) => col.notNull())
|
|
87
99
|
.addColumn('scopes_json', 'text', (col) => col.notNull())
|
|
88
100
|
.addColumn('params_json', 'text', (col) => col.notNull())
|
|
89
101
|
.addColumn('cursor', 'integer', (col) => col.notNull())
|
|
@@ -10,7 +10,7 @@ import type { ReactNode } from 'react';
|
|
|
10
10
|
import { createSyncularReact } from '../index';
|
|
11
11
|
import {
|
|
12
12
|
createMockDb,
|
|
13
|
-
|
|
13
|
+
createMockHandlerRegistry,
|
|
14
14
|
createMockTransport,
|
|
15
15
|
} from './test-utils';
|
|
16
16
|
|
|
@@ -53,13 +53,13 @@ describe('useMutations', () => {
|
|
|
53
53
|
|
|
54
54
|
function createWrapper() {
|
|
55
55
|
const transport = createMockTransport();
|
|
56
|
-
const
|
|
56
|
+
const handlers = createMockHandlerRegistry<TestDb>();
|
|
57
57
|
|
|
58
58
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
59
59
|
<SyncProvider
|
|
60
60
|
db={db}
|
|
61
61
|
transport={transport}
|
|
62
|
-
|
|
62
|
+
handlers={handlers}
|
|
63
63
|
actorId="test-actor"
|
|
64
64
|
clientId="test-client"
|
|
65
65
|
subscriptions={[]}
|
|
@@ -68,17 +68,33 @@ function isExecutableQuery<TResult>(
|
|
|
68
68
|
return typeof value.execute === 'function';
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function isPresenceMetadataEqual(left: unknown, right: unknown): boolean {
|
|
72
|
+
if (Object.is(left, right)) return true;
|
|
73
|
+
if (!isRecord(left) || !isRecord(right)) return false;
|
|
74
|
+
|
|
75
|
+
const leftKeys = Object.keys(left);
|
|
76
|
+
const rightKeys = Object.keys(right);
|
|
77
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
78
|
+
|
|
79
|
+
for (const key of leftKeys) {
|
|
80
|
+
if (!(key in right)) return false;
|
|
81
|
+
if (!Object.is(left[key], right[key])) return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
export interface SyncContextValue<DB extends SyncClientDb> {
|
|
72
88
|
engine: SyncEngine<DB>;
|
|
73
89
|
db: Kysely<DB>;
|
|
74
90
|
transport: SyncTransport;
|
|
75
|
-
|
|
91
|
+
handlers: ClientTableRegistry<DB>;
|
|
76
92
|
}
|
|
77
93
|
|
|
78
94
|
export interface SyncProviderProps<DB extends SyncClientDb> {
|
|
79
95
|
db: Kysely<DB>;
|
|
80
96
|
transport: SyncTransport;
|
|
81
|
-
|
|
97
|
+
handlers: ClientTableRegistry<DB>;
|
|
82
98
|
actorId?: string | null;
|
|
83
99
|
clientId?: string | null;
|
|
84
100
|
subscriptions?: Array<Omit<SyncSubscriptionRequest, 'cursor'>>;
|
|
@@ -96,6 +112,8 @@ export interface SyncProviderProps<DB extends SyncClientDb> {
|
|
|
96
112
|
onConflict?: (conflict: ConflictInfo) => void;
|
|
97
113
|
onDataChange?: (scopes: string[]) => void;
|
|
98
114
|
plugins?: SyncClientPlugin[];
|
|
115
|
+
/** Custom SHA-256 hash function (for platforms without crypto.subtle, e.g. React Native) */
|
|
116
|
+
sha256?: (bytes: Uint8Array) => Promise<string>;
|
|
99
117
|
autoStart?: boolean;
|
|
100
118
|
renderWhileStarting?: boolean;
|
|
101
119
|
children: ReactNode;
|
|
@@ -107,6 +125,7 @@ export interface UseSyncEngineResult {
|
|
|
107
125
|
reconnect: () => void;
|
|
108
126
|
disconnect: () => void;
|
|
109
127
|
start: () => Promise<void>;
|
|
128
|
+
resetLocalState: () => void;
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
export interface SyncStatus {
|
|
@@ -297,7 +316,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
297
316
|
function SyncProvider({
|
|
298
317
|
db,
|
|
299
318
|
transport,
|
|
300
|
-
|
|
319
|
+
handlers,
|
|
301
320
|
actorId,
|
|
302
321
|
clientId,
|
|
303
322
|
subscriptions = [],
|
|
@@ -315,15 +334,16 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
315
334
|
onConflict,
|
|
316
335
|
onDataChange,
|
|
317
336
|
plugins,
|
|
337
|
+
sha256,
|
|
318
338
|
autoStart = true,
|
|
319
|
-
renderWhileStarting =
|
|
339
|
+
renderWhileStarting = true,
|
|
320
340
|
children,
|
|
321
341
|
}: SyncProviderProps<DB>): ReactNode {
|
|
322
342
|
const config = useMemo<SyncEngineConfig<DB>>(
|
|
323
343
|
() => ({
|
|
324
344
|
db,
|
|
325
345
|
transport,
|
|
326
|
-
|
|
346
|
+
handlers,
|
|
327
347
|
actorId,
|
|
328
348
|
clientId,
|
|
329
349
|
subscriptions,
|
|
@@ -341,11 +361,12 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
341
361
|
onConflict,
|
|
342
362
|
onDataChange,
|
|
343
363
|
plugins,
|
|
364
|
+
sha256,
|
|
344
365
|
}),
|
|
345
366
|
[
|
|
346
367
|
db,
|
|
347
368
|
transport,
|
|
348
|
-
|
|
369
|
+
handlers,
|
|
349
370
|
actorId,
|
|
350
371
|
clientId,
|
|
351
372
|
subscriptions,
|
|
@@ -363,6 +384,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
363
384
|
onConflict,
|
|
364
385
|
onDataChange,
|
|
365
386
|
plugins,
|
|
387
|
+
sha256,
|
|
366
388
|
]
|
|
367
389
|
);
|
|
368
390
|
|
|
@@ -373,7 +395,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
373
395
|
clientId,
|
|
374
396
|
db,
|
|
375
397
|
transport,
|
|
376
|
-
|
|
398
|
+
handlers,
|
|
377
399
|
}));
|
|
378
400
|
|
|
379
401
|
useEffect(() => {
|
|
@@ -382,7 +404,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
382
404
|
if (clientId !== initialProps.clientId) changedProps.push('clientId');
|
|
383
405
|
if (db !== initialProps.db) changedProps.push('db');
|
|
384
406
|
if (transport !== initialProps.transport) changedProps.push('transport');
|
|
385
|
-
if (
|
|
407
|
+
if (handlers !== initialProps.handlers) changedProps.push('handlers');
|
|
386
408
|
|
|
387
409
|
if (changedProps.length > 0) {
|
|
388
410
|
const message =
|
|
@@ -399,7 +421,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
399
421
|
);
|
|
400
422
|
}
|
|
401
423
|
}
|
|
402
|
-
}, [actorId, clientId, db, transport,
|
|
424
|
+
}, [actorId, clientId, db, transport, handlers, initialProps]);
|
|
403
425
|
|
|
404
426
|
const [isReady, setIsReady] = useState(false);
|
|
405
427
|
|
|
@@ -441,12 +463,12 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
441
463
|
engine,
|
|
442
464
|
db,
|
|
443
465
|
transport,
|
|
444
|
-
|
|
466
|
+
handlers,
|
|
445
467
|
}),
|
|
446
|
-
[engine, db, transport,
|
|
468
|
+
[engine, db, transport, handlers]
|
|
447
469
|
);
|
|
448
470
|
|
|
449
|
-
if (!isReady &&
|
|
471
|
+
if (!isReady && renderWhileStarting === false) {
|
|
450
472
|
return null;
|
|
451
473
|
}
|
|
452
474
|
|
|
@@ -480,6 +502,10 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
480
502
|
const reconnect = useCallback(() => engine.reconnect(), [engine]);
|
|
481
503
|
const disconnect = useCallback(() => engine.disconnect(), [engine]);
|
|
482
504
|
const start = useCallback(() => engine.start(), [engine]);
|
|
505
|
+
const resetLocalState = useCallback(
|
|
506
|
+
() => engine.resetLocalState(),
|
|
507
|
+
[engine]
|
|
508
|
+
);
|
|
483
509
|
|
|
484
510
|
return {
|
|
485
511
|
state,
|
|
@@ -487,6 +513,7 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
487
513
|
reconnect,
|
|
488
514
|
disconnect,
|
|
489
515
|
start,
|
|
516
|
+
resetLocalState,
|
|
490
517
|
};
|
|
491
518
|
}
|
|
492
519
|
|
|
@@ -1272,6 +1299,8 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
1272
1299
|
const engine = useEngine();
|
|
1273
1300
|
const { presence, isLoading } = usePresence<TMetadata>(scopeKey);
|
|
1274
1301
|
const [isJoined, setIsJoined] = useState(false);
|
|
1302
|
+
const previousMetadataRef = useRef<TMetadata | undefined>(initialMetadata);
|
|
1303
|
+
const autoJoinMetadataRef = useRef<TMetadata | undefined>(initialMetadata);
|
|
1275
1304
|
|
|
1276
1305
|
const join = useCallback(
|
|
1277
1306
|
(metadata?: TMetadata) => {
|
|
@@ -1299,16 +1328,42 @@ export function createSyncularReact<DB extends SyncClientDb>() {
|
|
|
1299
1328
|
[engine, scopeKey]
|
|
1300
1329
|
);
|
|
1301
1330
|
|
|
1331
|
+
useEffect(() => {
|
|
1332
|
+
autoJoinMetadataRef.current = initialMetadata;
|
|
1333
|
+
}, [initialMetadata]);
|
|
1334
|
+
|
|
1302
1335
|
useEffect(() => {
|
|
1303
1336
|
if (autoJoin) {
|
|
1304
|
-
|
|
1337
|
+
const metadata = autoJoinMetadataRef.current;
|
|
1338
|
+
join(metadata);
|
|
1339
|
+
previousMetadataRef.current = metadata;
|
|
1305
1340
|
}
|
|
1306
1341
|
|
|
1307
1342
|
return () => {
|
|
1308
1343
|
leave();
|
|
1309
1344
|
};
|
|
1310
|
-
|
|
1311
|
-
|
|
1345
|
+
}, [autoJoin, join, leave]);
|
|
1346
|
+
|
|
1347
|
+
useEffect(() => {
|
|
1348
|
+
if (!autoJoin || !isJoined) {
|
|
1349
|
+
previousMetadataRef.current = initialMetadata;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
if (initialMetadata === undefined) {
|
|
1354
|
+
previousMetadataRef.current = initialMetadata;
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
if (
|
|
1359
|
+
isPresenceMetadataEqual(previousMetadataRef.current, initialMetadata)
|
|
1360
|
+
) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
previousMetadataRef.current = initialMetadata;
|
|
1365
|
+
updateMetadata(initialMetadata);
|
|
1366
|
+
}, [autoJoin, initialMetadata, isJoined, updateMetadata]);
|
|
1312
1367
|
|
|
1313
1368
|
return {
|
|
1314
1369
|
presence,
|
package/src/index.ts
CHANGED
|
@@ -5,5 +5,32 @@
|
|
|
5
5
|
* typed to your application's DB schema.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
export type {
|
|
9
|
+
ConflictResolution,
|
|
10
|
+
FluentMutation,
|
|
11
|
+
MutationInput,
|
|
12
|
+
MutationResult,
|
|
13
|
+
MutationsHook,
|
|
14
|
+
OutboxCommit,
|
|
15
|
+
SyncContextValue,
|
|
16
|
+
SyncProviderProps,
|
|
17
|
+
SyncStatus,
|
|
18
|
+
UseConflictsResult,
|
|
19
|
+
UseMutationOptions,
|
|
20
|
+
UseMutationResult,
|
|
21
|
+
UseMutationsOptions,
|
|
22
|
+
UseOutboxResult,
|
|
23
|
+
UsePresenceResult,
|
|
24
|
+
UsePresenceWithJoinOptions,
|
|
25
|
+
UsePresenceWithJoinResult,
|
|
26
|
+
UseQueryOptions,
|
|
27
|
+
UseQueryResult,
|
|
28
|
+
UseResolveConflictOptions,
|
|
29
|
+
UseResolveConflictResult,
|
|
30
|
+
UseSyncConnectionResult,
|
|
31
|
+
UseSyncEngineResult,
|
|
32
|
+
UseSyncQueryOptions,
|
|
33
|
+
UseSyncQueryResult,
|
|
34
|
+
} from './createSyncularReact';
|
|
8
35
|
// Re-export core client types for convenience.
|
|
9
36
|
export { createSyncularReact } from './createSyncularReact';
|