@trainstar/synchro-react-native 0.1.0
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/LICENSE +20 -0
- package/README.md +122 -0
- package/SynchroReactNative.podspec +22 -0
- package/android/build.gradle +81 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/kotlin/com/trainstar/synchro/rn/SynchroModule.kt +808 -0
- package/android/src/main/kotlin/com/trainstar/synchro/rn/SynchroPackage.kt +32 -0
- package/ios/SynchroModule.mm +368 -0
- package/ios/SynchroModule.swift +970 -0
- package/lib/module/NativeSynchro.js +5 -0
- package/lib/module/NativeSynchro.js.map +1 -0
- package/lib/module/SynchroClient.js +295 -0
- package/lib/module/SynchroClient.js.map +1 -0
- package/lib/module/errors.js +150 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/hooks/index.js +6 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/usePendingChanges.js +25 -0
- package/lib/module/hooks/usePendingChanges.js.map +1 -0
- package/lib/module/hooks/useQuery.js +74 -0
- package/lib/module/hooks/useQuery.js.map +1 -0
- package/lib/module/hooks/useSyncStatus.js +15 -0
- package/lib/module/hooks/useSyncStatus.js.map +1 -0
- package/lib/module/index.js +8 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeSynchro.d.ts +84 -0
- package/lib/typescript/src/NativeSynchro.d.ts.map +1 -0
- package/lib/typescript/src/SynchroClient.d.ts +30 -0
- package/lib/typescript/src/SynchroClient.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +61 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +4 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePendingChanges.d.ts +3 -0
- package/lib/typescript/src/hooks/usePendingChanges.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useQuery.d.ts +12 -0
- package/lib/typescript/src/hooks/useQuery.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useSyncStatus.d.ts +4 -0
- package/lib/typescript/src/hooks/useSyncStatus.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +55 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +146 -0
- package/src/NativeSynchro.ts +114 -0
- package/src/SynchroClient.ts +424 -0
- package/src/errors.ts +205 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/usePendingChanges.ts +32 -0
- package/src/hooks/useQuery.ts +93 -0
- package/src/hooks/useSyncStatus.ts +17 -0
- package/src/index.ts +41 -0
- package/src/types.ts +71 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import type { EventSubscription } from 'react-native';
|
|
3
|
+
import NativeSynchro from './NativeSynchro';
|
|
4
|
+
import { mapNativeError } from './errors';
|
|
5
|
+
import type {
|
|
6
|
+
Row,
|
|
7
|
+
ExecResult,
|
|
8
|
+
BatchResult,
|
|
9
|
+
SQLStatement,
|
|
10
|
+
ColumnDef,
|
|
11
|
+
TableOptions,
|
|
12
|
+
Transaction,
|
|
13
|
+
CheckpointMode,
|
|
14
|
+
SyncStatus,
|
|
15
|
+
SyncStatusType,
|
|
16
|
+
ConflictEvent,
|
|
17
|
+
SynchroConfig,
|
|
18
|
+
Unsubscribe,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
let observerCounter = 0;
|
|
22
|
+
function nextObserverID(): string {
|
|
23
|
+
return `obs_${++observerCounter}_${Date.now()}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SynchroClient {
|
|
27
|
+
private readonly native = NativeSynchro;
|
|
28
|
+
private readonly config: SynchroConfig;
|
|
29
|
+
private authSubscription: EventSubscription | null = null;
|
|
30
|
+
|
|
31
|
+
constructor(config: SynchroConfig) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
|
|
34
|
+
// Wire auth callback using Codegen EventEmitter pattern
|
|
35
|
+
this.authSubscription = this.native.onAuthRequest(
|
|
36
|
+
(event: { requestID: string }) => {
|
|
37
|
+
this.config
|
|
38
|
+
.authProvider()
|
|
39
|
+
.then((token) => {
|
|
40
|
+
this.native.resolveAuthRequest(event.requestID, token);
|
|
41
|
+
})
|
|
42
|
+
.catch((err) => {
|
|
43
|
+
this.native.rejectAuthRequest(
|
|
44
|
+
event.requestID,
|
|
45
|
+
err instanceof Error ? err.message : String(err)
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async initialize(): Promise<void> {
|
|
53
|
+
try {
|
|
54
|
+
await this.native.initialize({
|
|
55
|
+
dbPath: this.config.dbPath,
|
|
56
|
+
serverURL: this.config.serverURL,
|
|
57
|
+
clientID: this.config.clientID,
|
|
58
|
+
platform: this.config.platform ?? Platform.OS,
|
|
59
|
+
appVersion: this.config.appVersion,
|
|
60
|
+
syncInterval: this.config.syncInterval ?? 30,
|
|
61
|
+
pushDebounce: this.config.pushDebounce ?? 0.5,
|
|
62
|
+
maxRetryAttempts: this.config.maxRetryAttempts ?? 5,
|
|
63
|
+
pullPageSize: this.config.pullPageSize ?? 100,
|
|
64
|
+
pushBatchSize: this.config.pushBatchSize ?? 100,
|
|
65
|
+
snapshotPageSize: this.config.snapshotPageSize ?? 100,
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
throw mapNativeError(error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -- Core SQL --
|
|
73
|
+
|
|
74
|
+
async query(sql: string, params?: unknown[]): Promise<Row[]> {
|
|
75
|
+
try {
|
|
76
|
+
const json = await this.native.query(
|
|
77
|
+
sql,
|
|
78
|
+
JSON.stringify(params ?? [])
|
|
79
|
+
);
|
|
80
|
+
return JSON.parse(json) as Row[];
|
|
81
|
+
} catch (error) {
|
|
82
|
+
throw mapNativeError(error);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async queryOne(sql: string, params?: unknown[]): Promise<Row | null> {
|
|
87
|
+
try {
|
|
88
|
+
const json = await this.native.queryOne(
|
|
89
|
+
sql,
|
|
90
|
+
JSON.stringify(params ?? [])
|
|
91
|
+
);
|
|
92
|
+
return json ? (JSON.parse(json) as Row) : null;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw mapNativeError(error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async execute(sql: string, params?: unknown[]): Promise<ExecResult> {
|
|
99
|
+
try {
|
|
100
|
+
return await this.native.execute(
|
|
101
|
+
sql,
|
|
102
|
+
JSON.stringify(params ?? [])
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
throw mapNativeError(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async executeBatch(statements: SQLStatement[]): Promise<BatchResult> {
|
|
110
|
+
try {
|
|
111
|
+
const payload = statements.map((s) => ({
|
|
112
|
+
sql: s.sql,
|
|
113
|
+
params: s.params ?? [],
|
|
114
|
+
}));
|
|
115
|
+
return await this.native.executeBatch(JSON.stringify(payload));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
throw mapNativeError(error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// -- Transactions --
|
|
122
|
+
|
|
123
|
+
async writeTransaction<T>(
|
|
124
|
+
callback: (tx: Transaction) => Promise<T>
|
|
125
|
+
): Promise<T> {
|
|
126
|
+
let txID: string;
|
|
127
|
+
try {
|
|
128
|
+
txID = await this.native.beginWriteTransaction();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw mapNativeError(error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const tx: Transaction = {
|
|
135
|
+
query: async (sql, params) => {
|
|
136
|
+
const json = await this.native.txQuery(
|
|
137
|
+
txID,
|
|
138
|
+
sql,
|
|
139
|
+
JSON.stringify(params ?? [])
|
|
140
|
+
);
|
|
141
|
+
return JSON.parse(json) as Row[];
|
|
142
|
+
},
|
|
143
|
+
queryOne: async (sql, params) => {
|
|
144
|
+
const json = await this.native.txQueryOne(
|
|
145
|
+
txID,
|
|
146
|
+
sql,
|
|
147
|
+
JSON.stringify(params ?? [])
|
|
148
|
+
);
|
|
149
|
+
return json ? (JSON.parse(json) as Row) : null;
|
|
150
|
+
},
|
|
151
|
+
execute: async (sql, params) => {
|
|
152
|
+
return await this.native.txExecute(
|
|
153
|
+
txID,
|
|
154
|
+
sql,
|
|
155
|
+
JSON.stringify(params ?? [])
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const result = await callback(tx);
|
|
160
|
+
await this.native.commitTransaction(txID);
|
|
161
|
+
return result;
|
|
162
|
+
} catch (error) {
|
|
163
|
+
try {
|
|
164
|
+
await this.native.rollbackTransaction(txID);
|
|
165
|
+
} catch {
|
|
166
|
+
// rollback best-effort
|
|
167
|
+
}
|
|
168
|
+
throw mapNativeError(error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async readTransaction<T>(
|
|
173
|
+
callback: (tx: Transaction) => Promise<T>
|
|
174
|
+
): Promise<T> {
|
|
175
|
+
let txID: string;
|
|
176
|
+
try {
|
|
177
|
+
txID = await this.native.beginReadTransaction();
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw mapNativeError(error);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const tx: Transaction = {
|
|
184
|
+
query: async (sql, params) => {
|
|
185
|
+
const json = await this.native.txQuery(
|
|
186
|
+
txID,
|
|
187
|
+
sql,
|
|
188
|
+
JSON.stringify(params ?? [])
|
|
189
|
+
);
|
|
190
|
+
return JSON.parse(json) as Row[];
|
|
191
|
+
},
|
|
192
|
+
queryOne: async (sql, params) => {
|
|
193
|
+
const json = await this.native.txQueryOne(
|
|
194
|
+
txID,
|
|
195
|
+
sql,
|
|
196
|
+
JSON.stringify(params ?? [])
|
|
197
|
+
);
|
|
198
|
+
return json ? (JSON.parse(json) as Row) : null;
|
|
199
|
+
},
|
|
200
|
+
execute: async (sql, params) => {
|
|
201
|
+
return await this.native.txExecute(
|
|
202
|
+
txID,
|
|
203
|
+
sql,
|
|
204
|
+
JSON.stringify(params ?? [])
|
|
205
|
+
);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
const result = await callback(tx);
|
|
209
|
+
await this.native.commitTransaction(txID);
|
|
210
|
+
return result;
|
|
211
|
+
} catch (error) {
|
|
212
|
+
try {
|
|
213
|
+
await this.native.rollbackTransaction(txID);
|
|
214
|
+
} catch {
|
|
215
|
+
// rollback best-effort
|
|
216
|
+
}
|
|
217
|
+
throw mapNativeError(error);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// -- Schema (local-only) --
|
|
222
|
+
|
|
223
|
+
async createTable(
|
|
224
|
+
name: string,
|
|
225
|
+
columns: ColumnDef[],
|
|
226
|
+
options?: TableOptions
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
try {
|
|
229
|
+
await this.native.createTable(
|
|
230
|
+
name,
|
|
231
|
+
JSON.stringify(columns),
|
|
232
|
+
options ? JSON.stringify(options) : null
|
|
233
|
+
);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw mapNativeError(error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async alterTable(name: string, addColumns: ColumnDef[]): Promise<void> {
|
|
240
|
+
try {
|
|
241
|
+
await this.native.alterTable(name, JSON.stringify(addColumns));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw mapNativeError(error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async createIndex(
|
|
248
|
+
table: string,
|
|
249
|
+
columns: string[],
|
|
250
|
+
unique = false
|
|
251
|
+
): Promise<void> {
|
|
252
|
+
try {
|
|
253
|
+
await this.native.createIndex(table, columns, unique);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
throw mapNativeError(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// -- Observation --
|
|
260
|
+
|
|
261
|
+
onChange(tables: string[], callback: () => void): Unsubscribe {
|
|
262
|
+
const observerID = nextObserverID();
|
|
263
|
+
|
|
264
|
+
const subscription = this.native.onChange(
|
|
265
|
+
(event: { observerID: string }) => {
|
|
266
|
+
if (event.observerID === observerID) {
|
|
267
|
+
callback();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
this.native.addChangeObserver(observerID, tables);
|
|
273
|
+
|
|
274
|
+
return () => {
|
|
275
|
+
subscription.remove();
|
|
276
|
+
this.native.removeObserver(observerID);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
watch(
|
|
281
|
+
sql: string,
|
|
282
|
+
params: unknown[] | undefined,
|
|
283
|
+
tables: string[],
|
|
284
|
+
callback: (rows: Row[]) => void
|
|
285
|
+
): Unsubscribe {
|
|
286
|
+
const observerID = nextObserverID();
|
|
287
|
+
|
|
288
|
+
const subscription = this.native.onQueryResult(
|
|
289
|
+
(event: { observerID: string; rowsJson: string }) => {
|
|
290
|
+
if (event.observerID === observerID) {
|
|
291
|
+
callback(JSON.parse(event.rowsJson) as Row[]);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
this.native.addQueryObserver(
|
|
297
|
+
observerID,
|
|
298
|
+
sql,
|
|
299
|
+
JSON.stringify(params ?? []),
|
|
300
|
+
tables
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
return () => {
|
|
304
|
+
subscription.remove();
|
|
305
|
+
this.native.removeObserver(observerID);
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// -- WAL --
|
|
310
|
+
|
|
311
|
+
async checkpoint(mode: CheckpointMode = 'passive'): Promise<void> {
|
|
312
|
+
try {
|
|
313
|
+
await this.native.checkpoint(mode);
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw mapNativeError(error);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// -- Lifecycle --
|
|
320
|
+
|
|
321
|
+
async close(): Promise<void> {
|
|
322
|
+
try {
|
|
323
|
+
await this.native.close();
|
|
324
|
+
} catch (error) {
|
|
325
|
+
throw mapNativeError(error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async getPath(): Promise<string> {
|
|
330
|
+
try {
|
|
331
|
+
return await this.native.getPath();
|
|
332
|
+
} catch (error) {
|
|
333
|
+
throw mapNativeError(error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async pendingChangeCount(): Promise<number> {
|
|
338
|
+
try {
|
|
339
|
+
return await this.native.pendingChangeCount();
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw mapNativeError(error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// -- Sync --
|
|
346
|
+
|
|
347
|
+
async start(): Promise<void> {
|
|
348
|
+
try {
|
|
349
|
+
await this.native.start();
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw mapNativeError(error);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async stop(): Promise<void> {
|
|
356
|
+
try {
|
|
357
|
+
await this.native.stop();
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw mapNativeError(error);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
async syncNow(): Promise<void> {
|
|
364
|
+
try {
|
|
365
|
+
await this.native.syncNow();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
throw mapNativeError(error);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// -- Status & Events --
|
|
372
|
+
|
|
373
|
+
onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe {
|
|
374
|
+
const subscription = this.native.onStatusChange(
|
|
375
|
+
(event: { status: string; retryAt: string | null }) => {
|
|
376
|
+
callback({
|
|
377
|
+
status: event.status as SyncStatusType,
|
|
378
|
+
retryAt: event.retryAt ? new Date(event.retryAt) : null,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
return () => subscription.remove();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
onConflict(callback: (event: ConflictEvent) => void): Unsubscribe {
|
|
386
|
+
const subscription = this.native.onConflict(
|
|
387
|
+
(event: {
|
|
388
|
+
table: string;
|
|
389
|
+
recordID: string;
|
|
390
|
+
clientDataJson: string | null;
|
|
391
|
+
serverDataJson: string | null;
|
|
392
|
+
}) => {
|
|
393
|
+
callback({
|
|
394
|
+
table: event.table,
|
|
395
|
+
recordID: event.recordID,
|
|
396
|
+
clientData: event.clientDataJson
|
|
397
|
+
? JSON.parse(event.clientDataJson)
|
|
398
|
+
: null,
|
|
399
|
+
serverData: event.serverDataJson
|
|
400
|
+
? JSON.parse(event.serverDataJson)
|
|
401
|
+
: null,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
return () => subscription.remove();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
onSnapshotRequired(
|
|
409
|
+
handler: () => Promise<boolean>
|
|
410
|
+
): Unsubscribe {
|
|
411
|
+
const subscription = this.native.onSnapshotRequired(
|
|
412
|
+
(event: { requestID: string }) => {
|
|
413
|
+
handler()
|
|
414
|
+
.then((approved) => {
|
|
415
|
+
this.native.resolveSnapshotRequest(event.requestID, approved);
|
|
416
|
+
})
|
|
417
|
+
.catch(() => {
|
|
418
|
+
this.native.resolveSnapshotRequest(event.requestID, false);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
return () => subscription.remove();
|
|
423
|
+
}
|
|
424
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export class SynchroError extends Error {
|
|
2
|
+
readonly code: string;
|
|
3
|
+
|
|
4
|
+
constructor(code: string, message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'SynchroError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class NotConnectedError extends SynchroError {
|
|
12
|
+
constructor() {
|
|
13
|
+
super('NOT_CONNECTED', 'Not connected to sync server');
|
|
14
|
+
this.name = 'NotConnectedError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SchemaNotLoadedError extends SynchroError {
|
|
19
|
+
constructor() {
|
|
20
|
+
super('SCHEMA_NOT_LOADED', 'Schema has not been loaded from server');
|
|
21
|
+
this.name = 'SchemaNotLoadedError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class TableNotSyncedError extends SynchroError {
|
|
26
|
+
readonly table: string;
|
|
27
|
+
|
|
28
|
+
constructor(table: string) {
|
|
29
|
+
super('TABLE_NOT_SYNCED', `Table '${table}' is not a synced table`);
|
|
30
|
+
this.name = 'TableNotSyncedError';
|
|
31
|
+
this.table = table;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class UpgradeRequiredError extends SynchroError {
|
|
36
|
+
readonly currentVersion: string;
|
|
37
|
+
readonly minimumVersion: string;
|
|
38
|
+
|
|
39
|
+
constructor(currentVersion: string, minimumVersion: string) {
|
|
40
|
+
super(
|
|
41
|
+
'UPGRADE_REQUIRED',
|
|
42
|
+
`App version ${currentVersion} is below minimum ${minimumVersion}`
|
|
43
|
+
);
|
|
44
|
+
this.name = 'UpgradeRequiredError';
|
|
45
|
+
this.currentVersion = currentVersion;
|
|
46
|
+
this.minimumVersion = minimumVersion;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class SchemaMismatchError extends SynchroError {
|
|
51
|
+
readonly serverVersion: number;
|
|
52
|
+
readonly serverHash: string;
|
|
53
|
+
|
|
54
|
+
constructor(serverVersion: number, serverHash: string) {
|
|
55
|
+
super(
|
|
56
|
+
'SCHEMA_MISMATCH',
|
|
57
|
+
`Schema mismatch: server version ${serverVersion}, hash ${serverHash}`
|
|
58
|
+
);
|
|
59
|
+
this.name = 'SchemaMismatchError';
|
|
60
|
+
this.serverVersion = serverVersion;
|
|
61
|
+
this.serverHash = serverHash;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class SnapshotRequiredError extends SynchroError {
|
|
66
|
+
constructor() {
|
|
67
|
+
super('SNAPSHOT_REQUIRED', 'Full snapshot required');
|
|
68
|
+
this.name = 'SnapshotRequiredError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface PushResultItem {
|
|
73
|
+
recordID: string;
|
|
74
|
+
table: string;
|
|
75
|
+
status: string;
|
|
76
|
+
message?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class PushRejectedError extends SynchroError {
|
|
80
|
+
readonly results: PushResultItem[];
|
|
81
|
+
|
|
82
|
+
constructor(results: PushResultItem[]) {
|
|
83
|
+
super('PUSH_REJECTED', `Push rejected: ${results.length} record(s)`);
|
|
84
|
+
this.name = 'PushRejectedError';
|
|
85
|
+
this.results = results;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class NetworkError extends SynchroError {
|
|
90
|
+
constructor(message: string) {
|
|
91
|
+
super('NETWORK_ERROR', `Network error: ${message}`);
|
|
92
|
+
this.name = 'NetworkError';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class ServerError extends SynchroError {
|
|
97
|
+
readonly status: number;
|
|
98
|
+
|
|
99
|
+
constructor(status: number, message: string) {
|
|
100
|
+
super('SERVER_ERROR', `Server error ${status}: ${message}`);
|
|
101
|
+
this.name = 'ServerError';
|
|
102
|
+
this.status = status;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class DatabaseError extends SynchroError {
|
|
107
|
+
constructor(message: string) {
|
|
108
|
+
super('DATABASE_ERROR', `Database error: ${message}`);
|
|
109
|
+
this.name = 'DatabaseError';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class InvalidResponseError extends SynchroError {
|
|
114
|
+
constructor(message: string) {
|
|
115
|
+
super('INVALID_RESPONSE', `Invalid response: ${message}`);
|
|
116
|
+
this.name = 'InvalidResponseError';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export class AlreadyStartedError extends SynchroError {
|
|
121
|
+
constructor() {
|
|
122
|
+
super('ALREADY_STARTED', 'Sync has already been started');
|
|
123
|
+
this.name = 'AlreadyStartedError';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class NotStartedError extends SynchroError {
|
|
128
|
+
constructor() {
|
|
129
|
+
super('NOT_STARTED', 'Sync has not been started');
|
|
130
|
+
this.name = 'NotStartedError';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export class TransactionTimeoutError extends SynchroError {
|
|
135
|
+
constructor() {
|
|
136
|
+
super('TRANSACTION_TIMEOUT', 'Transaction timed out due to inactivity');
|
|
137
|
+
this.name = 'TransactionTimeoutError';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface NativeErrorLike {
|
|
142
|
+
code?: string;
|
|
143
|
+
message?: string;
|
|
144
|
+
userInfo?: Record<string, string>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function mapNativeError(error: unknown): SynchroError {
|
|
148
|
+
if (error instanceof SynchroError) {
|
|
149
|
+
return error;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const nativeError = error as NativeErrorLike;
|
|
153
|
+
const code = nativeError?.code;
|
|
154
|
+
const message = nativeError?.message ?? 'Unknown error';
|
|
155
|
+
const userInfo = nativeError?.userInfo ?? {};
|
|
156
|
+
|
|
157
|
+
switch (code) {
|
|
158
|
+
case 'NOT_CONNECTED':
|
|
159
|
+
return new NotConnectedError();
|
|
160
|
+
case 'SCHEMA_NOT_LOADED':
|
|
161
|
+
return new SchemaNotLoadedError();
|
|
162
|
+
case 'TABLE_NOT_SYNCED':
|
|
163
|
+
return new TableNotSyncedError(userInfo.table ?? '');
|
|
164
|
+
case 'UPGRADE_REQUIRED':
|
|
165
|
+
return new UpgradeRequiredError(
|
|
166
|
+
userInfo.currentVersion ?? '',
|
|
167
|
+
userInfo.minimumVersion ?? ''
|
|
168
|
+
);
|
|
169
|
+
case 'SCHEMA_MISMATCH':
|
|
170
|
+
return new SchemaMismatchError(
|
|
171
|
+
parseInt(userInfo.serverVersion ?? '0', 10),
|
|
172
|
+
userInfo.serverHash ?? ''
|
|
173
|
+
);
|
|
174
|
+
case 'SNAPSHOT_REQUIRED':
|
|
175
|
+
return new SnapshotRequiredError();
|
|
176
|
+
case 'PUSH_REJECTED': {
|
|
177
|
+
let results: PushResultItem[] = [];
|
|
178
|
+
try {
|
|
179
|
+
results = JSON.parse(userInfo.results ?? '[]');
|
|
180
|
+
} catch {
|
|
181
|
+
// leave empty
|
|
182
|
+
}
|
|
183
|
+
return new PushRejectedError(results);
|
|
184
|
+
}
|
|
185
|
+
case 'NETWORK_ERROR':
|
|
186
|
+
return new NetworkError(userInfo.message ?? message);
|
|
187
|
+
case 'SERVER_ERROR':
|
|
188
|
+
return new ServerError(
|
|
189
|
+
parseInt(userInfo.status ?? '0', 10),
|
|
190
|
+
userInfo.message ?? message
|
|
191
|
+
);
|
|
192
|
+
case 'DATABASE_ERROR':
|
|
193
|
+
return new DatabaseError(userInfo.message ?? message);
|
|
194
|
+
case 'INVALID_RESPONSE':
|
|
195
|
+
return new InvalidResponseError(userInfo.message ?? message);
|
|
196
|
+
case 'ALREADY_STARTED':
|
|
197
|
+
return new AlreadyStartedError();
|
|
198
|
+
case 'NOT_STARTED':
|
|
199
|
+
return new NotStartedError();
|
|
200
|
+
case 'TRANSACTION_TIMEOUT':
|
|
201
|
+
return new TransactionTimeoutError();
|
|
202
|
+
default:
|
|
203
|
+
return new SynchroError(code ?? 'UNKNOWN', message);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import type { SynchroClient } from '../SynchroClient';
|
|
3
|
+
|
|
4
|
+
export function usePendingChanges(
|
|
5
|
+
client: SynchroClient,
|
|
6
|
+
pollInterval = 2000
|
|
7
|
+
): number {
|
|
8
|
+
const [count, setCount] = useState(0);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let active = true;
|
|
12
|
+
|
|
13
|
+
const poll = async () => {
|
|
14
|
+
try {
|
|
15
|
+
const c = await client.pendingChangeCount();
|
|
16
|
+
if (active) setCount(c);
|
|
17
|
+
} catch {
|
|
18
|
+
// ignore poll errors
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
poll();
|
|
23
|
+
const interval = setInterval(poll, pollInterval);
|
|
24
|
+
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
clearInterval(interval);
|
|
28
|
+
};
|
|
29
|
+
}, [client, pollInterval]);
|
|
30
|
+
|
|
31
|
+
return count;
|
|
32
|
+
}
|