@syncular/client-react 0.0.1-60
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/dist/createSyncularReact.d.ts +222 -0
- package/dist/createSyncularReact.d.ts.map +1 -0
- package/dist/createSyncularReact.js +775 -0
- package/dist/createSyncularReact.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
- package/src/__tests__/SyncEngine.test.ts +1332 -0
- package/src/__tests__/SyncProvider.strictmode.test.tsx +117 -0
- package/src/__tests__/fingerprint.test.ts +181 -0
- package/src/__tests__/hooks/useMutation.test.tsx +468 -0
- package/src/__tests__/hooks.test.tsx +384 -0
- package/src/__tests__/integration/conflict-resolution.test.ts +439 -0
- package/src/__tests__/integration/provider-reconfig.test.ts +279 -0
- package/src/__tests__/integration/push-flow.test.ts +321 -0
- package/src/__tests__/integration/realtime-sync.test.ts +222 -0
- package/src/__tests__/integration/self-conflict.test.ts +91 -0
- package/src/__tests__/integration/test-setup.ts +550 -0
- package/src/__tests__/integration/two-client-sync.test.ts +373 -0
- package/src/__tests__/setup.ts +7 -0
- package/src/__tests__/test-utils.ts +199 -0
- package/src/__tests__/useMutations.test.tsx +198 -0
- package/src/createSyncularReact.tsx +1346 -0
- package/src/index.ts +36 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useMutations hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { beforeEach, describe, expect, it } from 'bun:test';
|
|
6
|
+
import type { SyncClientDb } from '@syncular/client';
|
|
7
|
+
import { act, renderHook } from '@testing-library/react';
|
|
8
|
+
import type { Kysely } from 'kysely';
|
|
9
|
+
import type { ReactNode } from 'react';
|
|
10
|
+
import { createSyncularReact } from '../index';
|
|
11
|
+
import {
|
|
12
|
+
createMockDb,
|
|
13
|
+
createMockShapeRegistry,
|
|
14
|
+
createMockTransport,
|
|
15
|
+
} from './test-utils';
|
|
16
|
+
|
|
17
|
+
// DB schema for tests
|
|
18
|
+
// server_version has a DB default so it's optional for inserts but present on selects
|
|
19
|
+
interface TestDbTasks {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
completed: number;
|
|
23
|
+
user_id: string;
|
|
24
|
+
server_version?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// TestDb includes app tables + sync tables (created by createMockDb)
|
|
28
|
+
interface TestDb extends SyncClientDb {
|
|
29
|
+
tasks: TestDbTasks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { SyncProvider, useEngine, useMutations } = createSyncularReact<TestDb>();
|
|
33
|
+
|
|
34
|
+
describe('useMutations', () => {
|
|
35
|
+
let db: Kysely<TestDb>;
|
|
36
|
+
|
|
37
|
+
beforeEach(async () => {
|
|
38
|
+
db = await createMockDb<TestDb>();
|
|
39
|
+
|
|
40
|
+
// App table used in tests
|
|
41
|
+
await db.schema
|
|
42
|
+
.createTable('tasks')
|
|
43
|
+
.ifNotExists()
|
|
44
|
+
.addColumn('id', 'text', (col) => col.primaryKey())
|
|
45
|
+
.addColumn('title', 'text', (col) => col.notNull())
|
|
46
|
+
.addColumn('completed', 'integer', (col) => col.notNull().defaultTo(0))
|
|
47
|
+
.addColumn('user_id', 'text', (col) => col.notNull())
|
|
48
|
+
.addColumn('server_version', 'integer', (col) =>
|
|
49
|
+
col.notNull().defaultTo(0)
|
|
50
|
+
)
|
|
51
|
+
.execute();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
function createWrapper() {
|
|
55
|
+
const transport = createMockTransport();
|
|
56
|
+
const shapes = createMockShapeRegistry<TestDb>();
|
|
57
|
+
|
|
58
|
+
const Wrapper = ({ children }: { children: ReactNode }) => (
|
|
59
|
+
<SyncProvider
|
|
60
|
+
db={db}
|
|
61
|
+
transport={transport}
|
|
62
|
+
shapes={shapes}
|
|
63
|
+
actorId="test-actor"
|
|
64
|
+
clientId="test-client"
|
|
65
|
+
subscriptions={[]}
|
|
66
|
+
pollIntervalMs={999999}
|
|
67
|
+
autoStart={false}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</SyncProvider>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return Wrapper;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
it('insert() generates id, writes local row, and enqueues one outbox commit', async () => {
|
|
77
|
+
const { result } = renderHook(
|
|
78
|
+
() => ({
|
|
79
|
+
api: useMutations({ sync: false }),
|
|
80
|
+
engine: useEngine(),
|
|
81
|
+
}),
|
|
82
|
+
{ wrapper: createWrapper() }
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
let insertedId = '';
|
|
86
|
+
await act(async () => {
|
|
87
|
+
const res = await result.current.api.tasks.insert({
|
|
88
|
+
title: 'Hello',
|
|
89
|
+
completed: 0,
|
|
90
|
+
user_id: 'test-actor',
|
|
91
|
+
});
|
|
92
|
+
insertedId = res.id;
|
|
93
|
+
expect(res.commitId).toBeTruthy();
|
|
94
|
+
expect(res.clientCommitId).toBeTruthy();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const row = await db
|
|
98
|
+
.selectFrom('tasks')
|
|
99
|
+
.selectAll()
|
|
100
|
+
.where('id', '=', insertedId)
|
|
101
|
+
.executeTakeFirstOrThrow();
|
|
102
|
+
expect(row.title).toBe('Hello');
|
|
103
|
+
|
|
104
|
+
const outbox = await db
|
|
105
|
+
.selectFrom('sync_outbox_commits')
|
|
106
|
+
.select(['id', 'operations_json'])
|
|
107
|
+
.execute();
|
|
108
|
+
expect(outbox.length).toBe(1);
|
|
109
|
+
|
|
110
|
+
const ops = JSON.parse(outbox[0]!.operations_json);
|
|
111
|
+
expect(ops.length).toBe(1);
|
|
112
|
+
expect(ops[0].op).toBe('upsert');
|
|
113
|
+
expect(ops[0].table).toBe('tasks');
|
|
114
|
+
expect(ops[0].row_id).toBe(insertedId);
|
|
115
|
+
expect(ops[0].payload.id).toBeUndefined();
|
|
116
|
+
expect(ops[0].payload.server_version).toBeUndefined();
|
|
117
|
+
|
|
118
|
+
// Fingerprinting: local mutation timestamps updated
|
|
119
|
+
expect(
|
|
120
|
+
result.current.engine.getMutationTimestamp('tasks', insertedId)
|
|
121
|
+
).toBeGreaterThan(0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('$commit() batches multiple ops into a single outbox commit', async () => {
|
|
125
|
+
const { result } = renderHook(() => useMutations({ sync: false }), {
|
|
126
|
+
wrapper: createWrapper(),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let ids: string[] = [];
|
|
130
|
+
await act(async () => {
|
|
131
|
+
const res = await result.current.$commit(async (tx) => {
|
|
132
|
+
const a = await tx.tasks.insert({
|
|
133
|
+
title: 'A',
|
|
134
|
+
completed: 0,
|
|
135
|
+
user_id: 'test-actor',
|
|
136
|
+
});
|
|
137
|
+
const b = await tx.tasks.insert({
|
|
138
|
+
title: 'B',
|
|
139
|
+
completed: 0,
|
|
140
|
+
user_id: 'test-actor',
|
|
141
|
+
});
|
|
142
|
+
return [a, b];
|
|
143
|
+
});
|
|
144
|
+
ids = res.result;
|
|
145
|
+
expect(res.commit.commitId).toBeTruthy();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const outbox = await db
|
|
149
|
+
.selectFrom('sync_outbox_commits')
|
|
150
|
+
.select(['operations_json'])
|
|
151
|
+
.execute();
|
|
152
|
+
expect(outbox.length).toBe(1);
|
|
153
|
+
|
|
154
|
+
const ops: { row_id: string }[] = JSON.parse(outbox[0]!.operations_json);
|
|
155
|
+
expect(ops.length).toBe(2);
|
|
156
|
+
expect(new Set(ops.map((o) => o.row_id))).toEqual(new Set(ids));
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('update() patches only provided columns and auto-reads base_version from server_version', async () => {
|
|
160
|
+
const { result } = renderHook(() => useMutations({ sync: false }), {
|
|
161
|
+
wrapper: createWrapper(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Seed a row with server_version=7
|
|
165
|
+
await db
|
|
166
|
+
.insertInto('tasks')
|
|
167
|
+
.values({
|
|
168
|
+
id: 't1',
|
|
169
|
+
title: 'Keep',
|
|
170
|
+
completed: 0,
|
|
171
|
+
user_id: 'test-actor',
|
|
172
|
+
server_version: 7,
|
|
173
|
+
})
|
|
174
|
+
.execute();
|
|
175
|
+
|
|
176
|
+
await act(async () => {
|
|
177
|
+
await result.current.tasks.update('t1', { completed: 1 });
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const row = await db
|
|
181
|
+
.selectFrom('tasks')
|
|
182
|
+
.selectAll()
|
|
183
|
+
.where('id', '=', 't1')
|
|
184
|
+
.executeTakeFirstOrThrow();
|
|
185
|
+
expect(row.title).toBe('Keep');
|
|
186
|
+
expect(row.completed).toBe(1);
|
|
187
|
+
expect(row.server_version).toBe(7);
|
|
188
|
+
|
|
189
|
+
const outbox = await db
|
|
190
|
+
.selectFrom('sync_outbox_commits')
|
|
191
|
+
.select(['operations_json'])
|
|
192
|
+
.executeTakeFirstOrThrow();
|
|
193
|
+
|
|
194
|
+
const ops = JSON.parse(outbox.operations_json);
|
|
195
|
+
expect(ops.length).toBe(1);
|
|
196
|
+
expect(ops[0].base_version).toBe(7);
|
|
197
|
+
});
|
|
198
|
+
});
|