@syncular/client-react 0.0.6-171 → 0.0.6-178

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/client-react",
3
- "version": "0.0.6-171",
3
+ "version": "0.0.6-178",
4
4
  "description": "React hooks and bindings for the Syncular client",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -44,7 +44,7 @@
44
44
  "release": "bunx syncular-publish"
45
45
  },
46
46
  "dependencies": {
47
- "@syncular/client": "0.0.6-171"
47
+ "@syncular/client": "0.0.6-178"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "kysely": "^0.28.0",
@@ -53,9 +53,9 @@
53
53
  "devDependencies": {
54
54
  "@happy-dom/global-registrator": "^20.7.0",
55
55
  "@syncular/config": "0.0.0",
56
- "@syncular/core": "0.0.6-171",
57
- "@syncular/dialect-bun-sqlite": "0.0.6-171",
58
- "@syncular/testkit": "0.0.6-171",
56
+ "@syncular/core": "0.0.6-178",
57
+ "@syncular/dialect-bun-sqlite": "0.0.6-178",
58
+ "@syncular/testkit": "0.0.6-178",
59
59
  "@testing-library/react": "^16.3.2",
60
60
  "@types/react": "^19.2.14",
61
61
  "happy-dom": "^20.7.0",
@@ -9,6 +9,7 @@ import { beforeEach, describe, expect, it } from 'bun:test';
9
9
  import type { SyncClientDb } from '@syncular/client';
10
10
  import { act, renderHook, waitFor } from '@testing-library/react';
11
11
  import type { Kysely } from 'kysely';
12
+ import { sql } from 'kysely';
12
13
  import type { ReactNode } from 'react';
13
14
  import { createSyncularReact } from '../index';
14
15
  import {
@@ -506,6 +507,153 @@ describe('React Hooks', () => {
506
507
  }
507
508
  );
508
509
  });
510
+
511
+ it('tracks joined tables and refreshes when joined data changes', async () => {
512
+ await db.schema
513
+ .createTable('users')
514
+ .ifNotExists()
515
+ .addColumn('id', 'text', (col) => col.primaryKey())
516
+ .addColumn('name', 'text', (col) => col.notNull())
517
+ .execute();
518
+
519
+ await db.schema
520
+ .createTable('tasks')
521
+ .ifNotExists()
522
+ .addColumn('id', 'text', (col) => col.primaryKey())
523
+ .addColumn('user_id', 'text', (col) => col.notNull())
524
+ .addColumn('title', 'text', (col) => col.notNull())
525
+ .execute();
526
+
527
+ await sql`
528
+ insert into ${sql.table('users')} (${sql.ref('id')}, ${sql.ref('name')})
529
+ values (${sql.val('user-1')}, ${sql.val('Alice')})
530
+ `.execute(db);
531
+
532
+ await sql`
533
+ insert into ${sql.table('tasks')} (
534
+ ${sql.ref('id')},
535
+ ${sql.ref('user_id')},
536
+ ${sql.ref('title')}
537
+ )
538
+ values (
539
+ ${sql.val('task-1')},
540
+ ${sql.val('user-1')},
541
+ ${sql.val('First task')}
542
+ )
543
+ `.execute(db);
544
+
545
+ const { result } = renderHook(
546
+ () => {
547
+ const engine = useEngine();
548
+ const query = useSyncQuery(({ selectFrom }) =>
549
+ selectFrom('tasks')
550
+ .innerJoin('users', 'users.id', 'tasks.user_id')
551
+ .select([
552
+ 'tasks.id as id',
553
+ 'tasks.title as title',
554
+ 'users.name as ownerName',
555
+ ])
556
+ .orderBy('tasks.id', 'asc')
557
+ );
558
+
559
+ return { engine, query };
560
+ },
561
+ { wrapper: createWrapper() }
562
+ );
563
+
564
+ await waitFor(() => {
565
+ expect(result.current.query.isLoading).toBe(false);
566
+ expect(result.current.query.data?.[0]).toMatchObject({
567
+ id: 'task-1',
568
+ ownerName: 'Alice',
569
+ });
570
+ });
571
+
572
+ await act(async () => {
573
+ await sql`
574
+ update ${sql.table('users')}
575
+ set ${sql.ref('name')} = ${sql.val('Bob')}
576
+ where ${sql.ref('id')} = ${sql.val('user-1')}
577
+ `.execute(db);
578
+
579
+ result.current.engine.recordLocalMutations([
580
+ {
581
+ table: 'users',
582
+ rowId: 'user-1',
583
+ op: 'upsert',
584
+ },
585
+ ]);
586
+ });
587
+
588
+ await waitFor(() => {
589
+ expect(result.current.query.data?.[0]).toMatchObject({
590
+ id: 'task-1',
591
+ ownerName: 'Bob',
592
+ });
593
+ });
594
+ });
595
+
596
+ it('preserves unchanged row references across keyed array refreshes', async () => {
597
+ await db.schema
598
+ .createTable('tasks')
599
+ .ifNotExists()
600
+ .addColumn('id', 'text', (col) => col.primaryKey())
601
+ .addColumn('title', 'text', (col) => col.notNull())
602
+ .execute();
603
+
604
+ await sql`
605
+ insert into ${sql.table('tasks')} (${sql.ref('id')}, ${sql.ref('title')})
606
+ values
607
+ (${sql.val('task-1')}, ${sql.val('First task')}),
608
+ (${sql.val('task-2')}, ${sql.val('Second task')})
609
+ `.execute(db);
610
+
611
+ const { result } = renderHook(
612
+ () => {
613
+ const engine = useEngine();
614
+ const query = useSyncQuery(({ selectFrom }) =>
615
+ selectFrom('tasks').selectAll().orderBy('id', 'asc')
616
+ );
617
+
618
+ return { engine, query };
619
+ },
620
+ { wrapper: createWrapper() }
621
+ );
622
+
623
+ await waitFor(() => {
624
+ expect(result.current.query.isLoading).toBe(false);
625
+ expect(result.current.query.data?.length).toBe(2);
626
+ });
627
+
628
+ const firstRow = result.current.query.data?.[0];
629
+ const secondRow = result.current.query.data?.[1];
630
+
631
+ await act(async () => {
632
+ await sql`
633
+ update ${sql.table('tasks')}
634
+ set ${sql.ref('title')} = ${sql.val('Second task updated')}
635
+ where ${sql.ref('id')} = ${sql.val('task-2')}
636
+ `.execute(db);
637
+
638
+ result.current.engine.recordLocalMutations([
639
+ {
640
+ table: 'tasks',
641
+ rowId: 'task-2',
642
+ op: 'upsert',
643
+ },
644
+ ]);
645
+ });
646
+
647
+ await waitFor(() => {
648
+ expect(result.current.query.data?.[1]).toMatchObject({
649
+ id: 'task-2',
650
+ title: 'Second task updated',
651
+ });
652
+ });
653
+
654
+ expect(result.current.query.data?.[0]).toBe(firstRow);
655
+ expect(result.current.query.data?.[1]).not.toBe(secondRow);
656
+ });
509
657
  });
510
658
 
511
659
  describe('usePresenceWithJoin', () => {
@@ -0,0 +1,155 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test';
2
+ import type { SyncClientDb } from '@syncular/client';
3
+ import { act, renderHook, waitFor } from '@testing-library/react';
4
+ import type { Kysely } from 'kysely';
5
+ import { sql } from 'kysely';
6
+ import type { ReactNode } from 'react';
7
+ import { createSyncularReact } from '../index';
8
+ import {
9
+ createMockDb,
10
+ createMockHandlerRegistry,
11
+ createMockSync,
12
+ createMockTransport,
13
+ } from './test-utils';
14
+
15
+ const { SyncProvider, useEngine, useSyncQuery } =
16
+ createSyncularReact<SyncClientDb>();
17
+
18
+ describe('useSyncQuery branching', () => {
19
+ let db: Kysely<SyncClientDb>;
20
+
21
+ beforeEach(async () => {
22
+ db = await createMockDb();
23
+ });
24
+
25
+ function createWrapper() {
26
+ const transport = createMockTransport();
27
+ const handlers = createMockHandlerRegistry();
28
+ const sync = createMockSync({ handlers });
29
+
30
+ const Wrapper = ({ children }: { children: ReactNode }) => (
31
+ <SyncProvider
32
+ db={db}
33
+ transport={transport}
34
+ sync={sync}
35
+ identity={{ actorId: 'test-actor' }}
36
+ clientId="test-client"
37
+ pollIntervalMs={999999}
38
+ autoStart={false}
39
+ >
40
+ {children}
41
+ </SyncProvider>
42
+ );
43
+
44
+ return Wrapper;
45
+ }
46
+
47
+ it('does not leak joined tables from an abandoned builder branch', async () => {
48
+ await db.schema
49
+ .createTable('users')
50
+ .ifNotExists()
51
+ .addColumn('id', 'text', (col) => col.primaryKey())
52
+ .addColumn('name', 'text', (col) => col.notNull())
53
+ .execute();
54
+
55
+ await db.schema
56
+ .createTable('tasks')
57
+ .ifNotExists()
58
+ .addColumn('id', 'text', (col) => col.primaryKey())
59
+ .addColumn('user_id', 'text', (col) => col.notNull())
60
+ .addColumn('title', 'text', (col) => col.notNull())
61
+ .execute();
62
+
63
+ await sql`
64
+ insert into ${sql.table('users')} (${sql.ref('id')}, ${sql.ref('name')})
65
+ values (${sql.val('user-1')}, ${sql.val('Alice')})
66
+ `.execute(db);
67
+
68
+ await sql`
69
+ insert into ${sql.table('tasks')} (
70
+ ${sql.ref('id')},
71
+ ${sql.ref('user_id')},
72
+ ${sql.ref('title')}
73
+ )
74
+ values (
75
+ ${sql.val('task-1')},
76
+ ${sql.val('user-1')},
77
+ ${sql.val('First task')}
78
+ )
79
+ `.execute(db);
80
+
81
+ let executions = 0;
82
+
83
+ const { result } = renderHook(
84
+ () => {
85
+ const engine = useEngine();
86
+ const query = useSyncQuery(({ selectFrom }) => {
87
+ executions += 1;
88
+
89
+ const base = selectFrom('tasks');
90
+ void base.innerJoin('users', 'users.id', 'tasks.user_id');
91
+
92
+ return base
93
+ .select(['tasks.id as id', 'tasks.title as title'])
94
+ .orderBy('tasks.id', 'asc');
95
+ });
96
+
97
+ return { engine, query };
98
+ },
99
+ { wrapper: createWrapper() }
100
+ );
101
+
102
+ await waitFor(() => {
103
+ expect(result.current.query.isLoading).toBe(false);
104
+ expect(result.current.query.data?.[0]).toMatchObject({
105
+ id: 'task-1',
106
+ title: 'First task',
107
+ });
108
+ });
109
+
110
+ const initialExecutions = executions;
111
+
112
+ await act(async () => {
113
+ await sql`
114
+ update ${sql.table('users')}
115
+ set ${sql.ref('name')} = ${sql.val('Bob')}
116
+ where ${sql.ref('id')} = ${sql.val('user-1')}
117
+ `.execute(db);
118
+
119
+ result.current.engine.recordLocalMutations([
120
+ {
121
+ table: 'users',
122
+ rowId: 'user-1',
123
+ op: 'upsert',
124
+ },
125
+ ]);
126
+ });
127
+
128
+ await new Promise((resolve) => setTimeout(resolve, 50));
129
+ expect(executions).toBe(initialExecutions);
130
+
131
+ await act(async () => {
132
+ await sql`
133
+ update ${sql.table('tasks')}
134
+ set ${sql.ref('title')} = ${sql.val('First task updated')}
135
+ where ${sql.ref('id')} = ${sql.val('task-1')}
136
+ `.execute(db);
137
+
138
+ result.current.engine.recordLocalMutations([
139
+ {
140
+ table: 'tasks',
141
+ rowId: 'task-1',
142
+ op: 'upsert',
143
+ },
144
+ ]);
145
+ });
146
+
147
+ await waitFor(() => {
148
+ expect(executions).toBeGreaterThan(initialExecutions);
149
+ expect(result.current.query.data?.[0]).toMatchObject({
150
+ id: 'task-1',
151
+ title: 'First task updated',
152
+ });
153
+ });
154
+ });
155
+ });
@@ -0,0 +1,134 @@
1
+ import { beforeEach, describe, expect, it } from 'bun:test';
2
+ import type { SyncClientDb } from '@syncular/client';
3
+ import { act, renderHook, waitFor } from '@testing-library/react';
4
+ import type { Kysely } from 'kysely';
5
+ import { sql } from 'kysely';
6
+ import type { ReactNode } from 'react';
7
+ import { createSyncularReact } from '../index';
8
+ import {
9
+ createMockDb,
10
+ createMockHandlerRegistry,
11
+ createMockSync,
12
+ createMockTransport,
13
+ } from './test-utils';
14
+
15
+ const { SyncProvider, useEngine, useSyncQuery } =
16
+ createSyncularReact<SyncClientDb>();
17
+
18
+ describe('useSyncQuery structural sharing', () => {
19
+ let db: Kysely<SyncClientDb>;
20
+
21
+ beforeEach(async () => {
22
+ db = await createMockDb();
23
+ });
24
+
25
+ function createWrapper() {
26
+ const transport = createMockTransport();
27
+ const handlers = createMockHandlerRegistry();
28
+ const sync = createMockSync({ handlers });
29
+
30
+ const Wrapper = ({ children }: { children: ReactNode }) => (
31
+ <SyncProvider
32
+ db={db}
33
+ transport={transport}
34
+ sync={sync}
35
+ identity={{ actorId: 'test-actor' }}
36
+ clientId="test-client"
37
+ pollIntervalMs={999999}
38
+ autoStart={false}
39
+ >
40
+ {children}
41
+ </SyncProvider>
42
+ );
43
+
44
+ return Wrapper;
45
+ }
46
+
47
+ it('preserves positional identity when joined rows share the same keyField', async () => {
48
+ await db.schema
49
+ .createTable('tasks')
50
+ .ifNotExists()
51
+ .addColumn('id', 'text', (col) => col.primaryKey())
52
+ .addColumn('title', 'text', (col) => col.notNull())
53
+ .execute();
54
+
55
+ await db.schema
56
+ .createTable('task_notes')
57
+ .ifNotExists()
58
+ .addColumn('id', 'text', (col) => col.primaryKey())
59
+ .addColumn('task_id', 'text', (col) => col.notNull())
60
+ .addColumn('note', 'text', (col) => col.notNull())
61
+ .execute();
62
+
63
+ await sql`
64
+ insert into ${sql.table('tasks')} (${sql.ref('id')}, ${sql.ref('title')})
65
+ values (${sql.val('task-1')}, ${sql.val('Task')})
66
+ `.execute(db);
67
+
68
+ await sql`
69
+ insert into ${sql.table('task_notes')} (
70
+ ${sql.ref('id')},
71
+ ${sql.ref('task_id')},
72
+ ${sql.ref('note')}
73
+ )
74
+ values
75
+ (${sql.val('note-1')}, ${sql.val('task-1')}, ${sql.val('First note')}),
76
+ (${sql.val('note-2')}, ${sql.val('task-1')}, ${sql.val('Second note')})
77
+ `.execute(db);
78
+
79
+ const { result } = renderHook(
80
+ () => {
81
+ const engine = useEngine();
82
+ const query = useSyncQuery(({ selectFrom }) =>
83
+ selectFrom('tasks')
84
+ .innerJoin('task_notes', 'task_notes.task_id', 'tasks.id')
85
+ .select([
86
+ 'tasks.id as id',
87
+ 'tasks.title as title',
88
+ 'task_notes.id as noteId',
89
+ 'task_notes.note as note',
90
+ ])
91
+ .orderBy('task_notes.id', 'asc')
92
+ );
93
+
94
+ return { engine, query };
95
+ },
96
+ { wrapper: createWrapper() }
97
+ );
98
+
99
+ await waitFor(() => {
100
+ expect(result.current.query.isLoading).toBe(false);
101
+ expect(result.current.query.data?.length).toBe(2);
102
+ });
103
+
104
+ const firstRow = result.current.query.data?.[0];
105
+ const secondRow = result.current.query.data?.[1];
106
+
107
+ await act(async () => {
108
+ await sql`
109
+ update ${sql.table('task_notes')}
110
+ set ${sql.ref('note')} = ${sql.val('Second note updated')}
111
+ where ${sql.ref('id')} = ${sql.val('note-2')}
112
+ `.execute(db);
113
+
114
+ result.current.engine.recordLocalMutations([
115
+ {
116
+ table: 'task_notes',
117
+ rowId: 'note-2',
118
+ op: 'upsert',
119
+ },
120
+ ]);
121
+ });
122
+
123
+ await waitFor(() => {
124
+ expect(result.current.query.data?.[1]).toMatchObject({
125
+ id: 'task-1',
126
+ noteId: 'note-2',
127
+ note: 'Second note updated',
128
+ });
129
+ });
130
+
131
+ expect(result.current.query.data?.[0]).toBe(firstRow);
132
+ expect(result.current.query.data?.[1]).not.toBe(secondRow);
133
+ });
134
+ });
@@ -0,0 +1,220 @@
1
+ import type { QueryContext, SyncClientDb } from '@syncular/client';
2
+ import { useMemo, useRef } from 'react';
3
+ import {
4
+ createSyncularReact as createSyncularReactBase,
5
+ type UseQueryOptions,
6
+ type UseQueryResult,
7
+ type UseSyncQueryOptions,
8
+ type UseSyncQueryResult,
9
+ } from './createSyncularReact';
10
+
11
+ type ExecutableQuery<TResult> = {
12
+ execute: () => Promise<TResult>;
13
+ };
14
+
15
+ type QueryFn<DB extends SyncClientDb, TResult> = (
16
+ ctx: QueryContext<DB>
17
+ ) => ExecutableQuery<TResult> | Promise<TResult>;
18
+
19
+ type SyncularReactBindings<DB extends SyncClientDb> = ReturnType<
20
+ typeof createSyncularReactBase<DB>
21
+ >;
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ function shallowEqualRecords(
28
+ left: Record<string, unknown>,
29
+ right: Record<string, unknown>
30
+ ): boolean {
31
+ if (left === right) return true;
32
+
33
+ const leftKeys = Object.keys(left);
34
+ const rightKeys = Object.keys(right);
35
+ if (leftKeys.length !== rightKeys.length) return false;
36
+
37
+ for (const key of leftKeys) {
38
+ if (!(key in right)) return false;
39
+ if (!Object.is(left[key], right[key])) return false;
40
+ }
41
+
42
+ return true;
43
+ }
44
+
45
+ function shallowEqualQueryValues(left: unknown, right: unknown): boolean {
46
+ if (Object.is(left, right)) return true;
47
+ if (!isRecord(left) || !isRecord(right)) return false;
48
+ return shallowEqualRecords(left, right);
49
+ }
50
+
51
+ function getKeyedQueryValueKey<T>(value: T, keyField: string): string | null {
52
+ if (!isRecord(value) || !(keyField in value)) return null;
53
+
54
+ const key = value[keyField];
55
+ if (key === null || key === undefined) return null;
56
+ return String(key);
57
+ }
58
+
59
+ function buildUniqueKeyMap<T>(
60
+ items: T[],
61
+ keyField: string
62
+ ): Map<string, T> | null {
63
+ const itemsByKey = new Map<string, T>();
64
+
65
+ for (const item of items) {
66
+ const key = getKeyedQueryValueKey(item, keyField);
67
+ if (key === null || itemsByKey.has(key)) {
68
+ return null;
69
+ }
70
+ itemsByKey.set(key, item);
71
+ }
72
+
73
+ return itemsByKey;
74
+ }
75
+
76
+ function shareArrayResult<T>(previous: T[], next: T[], keyField: string): T[] {
77
+ if (previous.length === 0 || next.length === 0) {
78
+ return next;
79
+ }
80
+
81
+ const previousByKey = buildUniqueKeyMap(previous, keyField);
82
+ const nextByKey = buildUniqueKeyMap(next, keyField);
83
+ const shared = next.slice();
84
+
85
+ if (previousByKey && nextByKey) {
86
+ for (const [index, item] of next.entries()) {
87
+ const key = getKeyedQueryValueKey(item, keyField);
88
+ if (key === null) {
89
+ return next;
90
+ }
91
+
92
+ const previousItem = previousByKey.get(key);
93
+ if (
94
+ previousItem !== undefined &&
95
+ shallowEqualQueryValues(previousItem, item)
96
+ ) {
97
+ shared[index] = previousItem;
98
+ }
99
+ }
100
+ } else {
101
+ const limit = Math.min(previous.length, next.length);
102
+ for (let index = 0; index < limit; index += 1) {
103
+ const previousItem = previous[index];
104
+ const nextItem = next[index];
105
+ if (
106
+ previousItem !== undefined &&
107
+ nextItem !== undefined &&
108
+ shallowEqualQueryValues(previousItem, nextItem)
109
+ ) {
110
+ shared[index] = previousItem;
111
+ }
112
+ }
113
+ }
114
+
115
+ if (shared.length !== previous.length) {
116
+ return shared;
117
+ }
118
+
119
+ for (let index = 0; index < shared.length; index += 1) {
120
+ if (!Object.is(shared[index], previous[index])) {
121
+ return shared;
122
+ }
123
+ }
124
+
125
+ return previous;
126
+ }
127
+
128
+ function shareQueryResult<TResult>(
129
+ previous: TResult | undefined,
130
+ next: TResult | undefined,
131
+ keyField: string,
132
+ enabled: boolean
133
+ ): TResult | undefined {
134
+ if (!enabled || previous === undefined || next === undefined) {
135
+ return next;
136
+ }
137
+
138
+ if (Array.isArray(previous) && Array.isArray(next)) {
139
+ return shareArrayResult(previous, next, keyField) as TResult;
140
+ }
141
+
142
+ if (
143
+ isRecord(previous) &&
144
+ isRecord(next) &&
145
+ shallowEqualRecords(previous, next)
146
+ ) {
147
+ return previous;
148
+ }
149
+
150
+ return next;
151
+ }
152
+
153
+ export function createSyncularReact<
154
+ DB extends SyncClientDb,
155
+ >(): SyncularReactBindings<DB> {
156
+ const base = createSyncularReactBase<DB>();
157
+
158
+ function useSyncQuery<TResult>(
159
+ queryFn: QueryFn<DB, TResult>,
160
+ options: UseSyncQueryOptions = {}
161
+ ): UseSyncQueryResult<TResult> {
162
+ const keyField = options.keyField ?? 'id';
163
+ const structuralSharing = options.structuralSharing !== false;
164
+ const sharedDataRef = useRef<TResult | undefined>(undefined);
165
+ const query = base.useSyncQuery(queryFn, {
166
+ ...options,
167
+ structuralSharing: false,
168
+ });
169
+
170
+ const data = useMemo(() => {
171
+ const shared = shareQueryResult(
172
+ sharedDataRef.current,
173
+ query.data,
174
+ keyField,
175
+ structuralSharing
176
+ );
177
+ sharedDataRef.current = shared;
178
+ return shared;
179
+ }, [query.data, keyField, structuralSharing]);
180
+
181
+ return useMemo(
182
+ () => ({
183
+ ...query,
184
+ data,
185
+ }),
186
+ [query, data]
187
+ );
188
+ }
189
+
190
+ function useQuery<TResult>(
191
+ queryFn: QueryFn<DB, TResult>,
192
+ options: UseQueryOptions = {}
193
+ ): UseQueryResult<TResult> {
194
+ const { enabled = true, deps = [], keyField = 'id' } = options;
195
+ const query = useSyncQuery(queryFn, {
196
+ enabled,
197
+ deps,
198
+ keyField,
199
+ refreshOnDataChange: false,
200
+ loadingOnRefresh: true,
201
+ transitionUpdates: false,
202
+ });
203
+
204
+ return useMemo(
205
+ () => ({
206
+ data: query.data,
207
+ isLoading: query.isLoading,
208
+ error: query.error,
209
+ refetch: query.refetch,
210
+ }),
211
+ [query.data, query.isLoading, query.error, query.refetch]
212
+ );
213
+ }
214
+
215
+ return {
216
+ ...base,
217
+ useQuery,
218
+ useSyncQuery,
219
+ } as SyncularReactBindings<DB>;
220
+ }