@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.
@@ -24,37 +24,49 @@ export function createMockTransport(
24
24
  } = {}
25
25
  ): SyncTransport {
26
26
  return {
27
- async pull(request: SyncPullRequest): Promise<SyncPullResponse> {
28
- options.onPull?.(request);
29
- return {
30
- ok: true,
31
- subscriptions: [],
32
- ...options.pullResponse,
33
- };
34
- },
35
- async push(request: SyncPushRequest): Promise<SyncPushResponse> {
36
- options.onPush?.(request);
37
- return {
38
- ok: true,
39
- status: 'applied',
40
- results: request.operations.map((_, i) => ({
41
- opIndex: i,
42
- status: 'applied' as const,
43
- })),
44
- ...options.pushResponse,
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 shape registry
67
+ * Create a mock handler registry
56
68
  */
57
- export function createMockShapeRegistry<
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('shape', 'text', (col) => col.notNull())
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
- createMockShapeRegistry,
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 shapes = createMockShapeRegistry<TestDb>();
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
- shapes={shapes}
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
- shapes: ClientTableRegistry<DB>;
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
- shapes: ClientTableRegistry<DB>;
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
- shapes,
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 = false,
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
- shapes,
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
- shapes,
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
- shapes,
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 (shapes !== initialProps.shapes) changedProps.push('shapes');
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, shapes, initialProps]);
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
- shapes,
466
+ handlers,
445
467
  }),
446
- [engine, db, transport, shapes]
468
+ [engine, db, transport, handlers]
447
469
  );
448
470
 
449
- if (!isReady && !renderWhileStarting) {
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
- join(initialMetadata);
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
- // eslint-disable-next-line react-hooks/exhaustive-deps
1311
- }, [autoJoin, initialMetadata, join, leave]);
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';