@trainstar/synchro-react-native 0.1.2 → 0.3.2
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 +103 -24
- package/SynchroReactNative.podspec +3 -2
- package/android/build.gradle +8 -3
- package/android/src/main/kotlin/com/trainstar/synchro/rn/SynchroModule.kt +1106 -238
- package/ios/SynchroModule.mm +178 -51
- package/ios/SynchroModule.swift +1382 -295
- package/lib/module/NativeSynchro.js +5 -0
- package/lib/module/NativeSynchro.js.map +1 -1
- package/lib/module/SynchroClient.js +637 -65
- package/lib/module/SynchroClient.js.map +1 -1
- package/lib/module/digest.js +82 -0
- package/lib/module/digest.js.map +1 -0
- package/lib/module/errors.js +165 -22
- package/lib/module/errors.js.map +1 -1
- package/lib/module/hooks/useQuery.js +37 -3
- package/lib/module/hooks/useQuery.js.map +1 -1
- package/lib/module/hooks/useSyncStatus.js +51 -6
- package/lib/module/hooks/useSyncStatus.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/inspection.js +121 -0
- package/lib/module/inspection.js.map +1 -0
- package/lib/module/sqliteValues.js +87 -0
- package/lib/module/sqliteValues.js.map +1 -0
- package/lib/module/types.js +10 -0
- package/lib/module/types.js.map +1 -1
- package/lib/typescript/example/e2e/corpus-harness.d.ts +34 -0
- package/lib/typescript/example/e2e/corpus-harness.d.ts.map +1 -0
- package/lib/typescript/example/src/conformance/runner.d.ts +125 -0
- package/lib/typescript/example/src/conformance/runner.d.ts.map +1 -0
- package/lib/typescript/example/src/conformance/types.d.ts +71 -0
- package/lib/typescript/example/src/conformance/types.d.ts.map +1 -0
- package/lib/typescript/example/src/timeouts.d.ts +2 -0
- package/lib/typescript/example/src/timeouts.d.ts.map +1 -0
- package/lib/typescript/src/NativeSynchro.d.ts +59 -17
- package/lib/typescript/src/NativeSynchro.d.ts.map +1 -1
- package/lib/typescript/src/SynchroClient.d.ts +27 -10
- package/lib/typescript/src/SynchroClient.d.ts.map +1 -1
- package/lib/typescript/src/digest.d.ts +3 -0
- package/lib/typescript/src/digest.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +37 -9
- package/lib/typescript/src/errors.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useQuery.d.ts +2 -2
- package/lib/typescript/src/hooks/useQuery.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useSyncStatus.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -3
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/inspection.d.ts +43 -0
- package/lib/typescript/src/inspection.d.ts.map +1 -0
- package/lib/typescript/src/sqliteValues.d.ts +8 -0
- package/lib/typescript/src/sqliteValues.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +203 -11
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +20 -8
- package/src/NativeSynchro.ts +93 -26
- package/src/SynchroClient.ts +900 -120
- package/src/digest.ts +93 -0
- package/src/errors.ts +273 -33
- package/src/hooks/useQuery.ts +59 -16
- package/src/hooks/useSyncStatus.ts +40 -7
- package/src/index.ts +44 -3
- package/src/inspection.ts +226 -0
- package/src/sqliteValues.ts +105 -0
- package/src/types.ts +318 -16
package/src/digest.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Return the SHA-256 digest of a string as lowercase hexadecimal. */
|
|
2
|
+
export function sha256Hex(value: string): string {
|
|
3
|
+
const bytes = utf8(value);
|
|
4
|
+
const bitLength = bytes.length * 8;
|
|
5
|
+
bytes.push(0x80);
|
|
6
|
+
while ((bytes.length + 8) % 64 !== 0) {
|
|
7
|
+
bytes.push(0);
|
|
8
|
+
}
|
|
9
|
+
for (let shift = 56; shift >= 0; shift -= 8) {
|
|
10
|
+
bytes.push(Math.floor(bitLength / 2 ** shift) & 0xff);
|
|
11
|
+
}
|
|
12
|
+
const hash = [
|
|
13
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
14
|
+
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
15
|
+
];
|
|
16
|
+
const constants = [
|
|
17
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
18
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
19
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
20
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
21
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
22
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
23
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
24
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
25
|
+
];
|
|
26
|
+
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
27
|
+
const words = new Array<number>(64).fill(0);
|
|
28
|
+
for (let index = 0; index < 16; index += 1) {
|
|
29
|
+
words[index] = ((bytes[offset + index * 4] << 24) | (bytes[offset + index * 4 + 1] << 16) | (bytes[offset + index * 4 + 2] << 8) | bytes[offset + index * 4 + 3]) >>> 0;
|
|
30
|
+
}
|
|
31
|
+
for (let index = 16; index < 64; index += 1) {
|
|
32
|
+
const a = words[index - 15];
|
|
33
|
+
const b = words[index - 2];
|
|
34
|
+
const s0 = rotateRight(a, 7) ^ rotateRight(a, 18) ^ (a >>> 3);
|
|
35
|
+
const s1 = rotateRight(b, 17) ^ rotateRight(b, 19) ^ (b >>> 10);
|
|
36
|
+
words[index] = (words[index - 16] + s0 + words[index - 7] + s1) >>> 0;
|
|
37
|
+
}
|
|
38
|
+
let [a, b, c, d, e, f, g, h] = hash;
|
|
39
|
+
for (let index = 0; index < 64; index += 1) {
|
|
40
|
+
const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
41
|
+
const choice = (e & f) ^ (~e & g);
|
|
42
|
+
const temp1 = (h + s1 + choice + constants[index] + words[index]) >>> 0;
|
|
43
|
+
const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
44
|
+
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
45
|
+
const temp2 = (s0 + majority) >>> 0;
|
|
46
|
+
h = g;
|
|
47
|
+
g = f;
|
|
48
|
+
f = e;
|
|
49
|
+
e = (d + temp1) >>> 0;
|
|
50
|
+
d = c;
|
|
51
|
+
c = b;
|
|
52
|
+
b = a;
|
|
53
|
+
a = (temp1 + temp2) >>> 0;
|
|
54
|
+
}
|
|
55
|
+
hash[0] = (hash[0] + a) >>> 0;
|
|
56
|
+
hash[1] = (hash[1] + b) >>> 0;
|
|
57
|
+
hash[2] = (hash[2] + c) >>> 0;
|
|
58
|
+
hash[3] = (hash[3] + d) >>> 0;
|
|
59
|
+
hash[4] = (hash[4] + e) >>> 0;
|
|
60
|
+
hash[5] = (hash[5] + f) >>> 0;
|
|
61
|
+
hash[6] = (hash[6] + g) >>> 0;
|
|
62
|
+
hash[7] = (hash[7] + h) >>> 0;
|
|
63
|
+
}
|
|
64
|
+
return hash.map((word) => word.toString(16).padStart(8, '0')).join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function rotateRight(value: number, shift: number): number {
|
|
68
|
+
return (value >>> shift) | (value << (32 - shift));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function utf8(value: string): number[] {
|
|
72
|
+
const bytes: number[] = [];
|
|
73
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
74
|
+
let code = value.charCodeAt(index);
|
|
75
|
+
if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
|
|
76
|
+
const low = value.charCodeAt(index + 1);
|
|
77
|
+
if (low >= 0xdc00 && low <= 0xdfff) {
|
|
78
|
+
code = 0x10000 + ((code - 0xd800) << 10) + low - 0xdc00;
|
|
79
|
+
index += 1;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (code < 0x80) {
|
|
83
|
+
bytes.push(code);
|
|
84
|
+
} else if (code < 0x800) {
|
|
85
|
+
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
86
|
+
} else if (code < 0x10000) {
|
|
87
|
+
bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
88
|
+
} else {
|
|
89
|
+
bytes.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return bytes;
|
|
93
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -1,7 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PROTOCOL_ERROR_CODES,
|
|
3
|
+
SYNC_FAILURE_CODES,
|
|
4
|
+
SYNC_OPERATION_KINDS,
|
|
5
|
+
SYNC_RECOVERY_ACTIONS,
|
|
6
|
+
SYNC_STATUS_TYPES,
|
|
7
|
+
} from './types';
|
|
8
|
+
import type {
|
|
9
|
+
ProtocolErrorCode,
|
|
10
|
+
Row,
|
|
11
|
+
SyncFailure,
|
|
12
|
+
SyncStatusType,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
export const NATIVE_ERROR_CODES = [
|
|
16
|
+
'NOT_CONNECTED',
|
|
17
|
+
'SCHEMA_NOT_LOADED',
|
|
18
|
+
'TABLE_NOT_SYNCED',
|
|
19
|
+
'UPGRADE_REQUIRED',
|
|
20
|
+
'SCHEMA_MISMATCH',
|
|
21
|
+
'PUSH_REJECTED',
|
|
22
|
+
'NETWORK_ERROR',
|
|
23
|
+
'SERVER_ERROR',
|
|
24
|
+
'PROTOCOL_ERROR',
|
|
25
|
+
'DATABASE_ERROR',
|
|
26
|
+
'INVALID_RESPONSE',
|
|
27
|
+
'INVALID_SEED',
|
|
28
|
+
'SYNC_BLOCKED',
|
|
29
|
+
'UNSUPPORTED_SCHEMA',
|
|
30
|
+
'INVALID_STATE_TRANSITION',
|
|
31
|
+
'ALREADY_STARTED',
|
|
32
|
+
'NOT_STARTED',
|
|
33
|
+
'TRANSACTION_TIMEOUT',
|
|
34
|
+
'INVALID_CONFIG',
|
|
35
|
+
'CLIENT_ALREADY_ACTIVE',
|
|
36
|
+
'UNKNOWN',
|
|
37
|
+
] as const;
|
|
38
|
+
|
|
39
|
+
export type NativeErrorCode = (typeof NATIVE_ERROR_CODES)[number];
|
|
40
|
+
|
|
1
41
|
export class SynchroError extends Error {
|
|
2
|
-
readonly code:
|
|
42
|
+
readonly code: NativeErrorCode;
|
|
3
43
|
|
|
4
|
-
constructor(code:
|
|
44
|
+
constructor(code: NativeErrorCode, message: string) {
|
|
5
45
|
super(message);
|
|
6
46
|
this.code = code;
|
|
7
47
|
this.name = 'SynchroError';
|
|
@@ -62,25 +102,22 @@ export class SchemaMismatchError extends SynchroError {
|
|
|
62
102
|
}
|
|
63
103
|
}
|
|
64
104
|
|
|
65
|
-
export
|
|
66
|
-
|
|
67
|
-
super('SNAPSHOT_REQUIRED', 'Full snapshot required');
|
|
68
|
-
this.name = 'SnapshotRequiredError';
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface PushResultItem {
|
|
73
|
-
recordID: string;
|
|
105
|
+
export interface PushRejectedMutation {
|
|
106
|
+
mutationID: string;
|
|
74
107
|
table: string;
|
|
108
|
+
pk: Row;
|
|
75
109
|
status: string;
|
|
110
|
+
code: string;
|
|
76
111
|
message?: string;
|
|
112
|
+
serverRow?: Row | null;
|
|
113
|
+
serverVersion?: string | null;
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
export class PushRejectedError extends SynchroError {
|
|
80
|
-
readonly results:
|
|
117
|
+
readonly results: PushRejectedMutation[];
|
|
81
118
|
|
|
82
|
-
constructor(results:
|
|
83
|
-
super('PUSH_REJECTED', `Push rejected: ${results.length}
|
|
119
|
+
constructor(results: PushRejectedMutation[]) {
|
|
120
|
+
super('PUSH_REJECTED', `Push rejected: ${results.length} mutation(s)`);
|
|
84
121
|
this.name = 'PushRejectedError';
|
|
85
122
|
this.results = results;
|
|
86
123
|
}
|
|
@@ -117,6 +154,64 @@ export class InvalidResponseError extends SynchroError {
|
|
|
117
154
|
}
|
|
118
155
|
}
|
|
119
156
|
|
|
157
|
+
export const SCHEMA_UNSUPPORTED_REASONS = [
|
|
158
|
+
'unknown_schema_lineage',
|
|
159
|
+
'incompatible_schema_transition',
|
|
160
|
+
] as const;
|
|
161
|
+
|
|
162
|
+
export type SchemaUnsupportedReason = (typeof SCHEMA_UNSUPPORTED_REASONS)[number];
|
|
163
|
+
|
|
164
|
+
export class ProtocolError extends SynchroError {
|
|
165
|
+
readonly status: number;
|
|
166
|
+
readonly protocolCode: ProtocolErrorCode;
|
|
167
|
+
|
|
168
|
+
constructor(status: number, protocolCode: ProtocolErrorCode, message: string) {
|
|
169
|
+
super('PROTOCOL_ERROR', `Protocol error ${status} ${protocolCode}: ${message}`);
|
|
170
|
+
this.name = 'ProtocolError';
|
|
171
|
+
this.status = status;
|
|
172
|
+
this.protocolCode = protocolCode;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class SyncBlockedError extends SynchroError {
|
|
177
|
+
readonly failure: SyncFailure;
|
|
178
|
+
|
|
179
|
+
constructor(failure: SyncFailure) {
|
|
180
|
+
super('SYNC_BLOCKED', failure.message);
|
|
181
|
+
this.name = 'SyncBlockedError';
|
|
182
|
+
this.failure = failure;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export class UnsupportedSchemaError extends SynchroError {
|
|
187
|
+
readonly reason: SchemaUnsupportedReason;
|
|
188
|
+
|
|
189
|
+
constructor(reason: SchemaUnsupportedReason) {
|
|
190
|
+
super('UNSUPPORTED_SCHEMA', `Schema recovery is required: ${reason}`);
|
|
191
|
+
this.name = 'UnsupportedSchemaError';
|
|
192
|
+
this.reason = reason;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class InvalidStateTransitionError extends SynchroError {
|
|
197
|
+
readonly from: SyncStatusType;
|
|
198
|
+
readonly to: SyncStatusType;
|
|
199
|
+
|
|
200
|
+
constructor(from: SyncStatusType, to: SyncStatusType) {
|
|
201
|
+
super('INVALID_STATE_TRANSITION', `Invalid sync state transition from ${from} to ${to}`);
|
|
202
|
+
this.name = 'InvalidStateTransitionError';
|
|
203
|
+
this.from = from;
|
|
204
|
+
this.to = to;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export class InvalidSeedError extends SynchroError {
|
|
209
|
+
constructor() {
|
|
210
|
+
super('INVALID_SEED', 'Seed database failed validation');
|
|
211
|
+
this.name = 'InvalidSeedError';
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
120
215
|
export class AlreadyStartedError extends SynchroError {
|
|
121
216
|
constructor() {
|
|
122
217
|
super('ALREADY_STARTED', 'Sync has already been started');
|
|
@@ -141,7 +236,104 @@ export class TransactionTimeoutError extends SynchroError {
|
|
|
141
236
|
interface NativeErrorLike {
|
|
142
237
|
code?: string;
|
|
143
238
|
message?: string;
|
|
144
|
-
userInfo?: Record<string,
|
|
239
|
+
userInfo?: Record<string, unknown>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
243
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseJSONValue(value: unknown, name: string): unknown {
|
|
247
|
+
if (typeof value !== 'string') {
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
return JSON.parse(value) as unknown;
|
|
252
|
+
} catch {
|
|
253
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function requiredString(value: unknown, name: string): string {
|
|
258
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
259
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
260
|
+
}
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function requiredBoolean(value: unknown, name: string): boolean {
|
|
265
|
+
if (typeof value === 'boolean') {
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
if (value === 'true') {
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
if (value === 'false') {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function optionalString(value: unknown): string {
|
|
278
|
+
return typeof value === 'string' ? value : '';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function requiredEnum<T extends string>(value: unknown, values: readonly T[], name: string): T {
|
|
282
|
+
if (typeof value === 'string' && values.includes(value as T)) {
|
|
283
|
+
return value as T;
|
|
284
|
+
}
|
|
285
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function parseSyncFailure(value: unknown): SyncFailure {
|
|
289
|
+
const parsed = parseJSONValue(value, 'sync failure');
|
|
290
|
+
if (!isRecord(parsed)) {
|
|
291
|
+
throw new InvalidResponseError('Native bridge returned an invalid sync failure');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const message = requiredString(parsed.message, 'sync failure message');
|
|
295
|
+
if (message.length > 256) {
|
|
296
|
+
throw new InvalidResponseError('Native bridge returned an invalid sync failure message');
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const metadataValue = parsed.metadata == null ? {} : parseJSONValue(parsed.metadata, 'sync failure metadata');
|
|
300
|
+
if (!isRecord(metadataValue) || Object.keys(metadataValue).length > 8) {
|
|
301
|
+
throw new InvalidResponseError('Native bridge returned invalid sync failure metadata');
|
|
302
|
+
}
|
|
303
|
+
const metadata: Record<string, string> = {};
|
|
304
|
+
Object.entries(metadataValue).forEach(([key, item]) => {
|
|
305
|
+
if (key.length === 0 || key.length > 64 || typeof item !== 'string' || item.length > 128) {
|
|
306
|
+
throw new InvalidResponseError('Native bridge returned invalid sync failure metadata');
|
|
307
|
+
}
|
|
308
|
+
metadata[key] = item;
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
operation: requiredEnum(parsed.operation, SYNC_OPERATION_KINDS, 'sync failure operation'),
|
|
313
|
+
code: requiredEnum(parsed.code, SYNC_FAILURE_CODES, 'sync failure code'),
|
|
314
|
+
retryable: requiredBoolean(parsed.retryable, 'sync failure retry flag'),
|
|
315
|
+
message,
|
|
316
|
+
recoveryAction: requiredEnum(
|
|
317
|
+
parsed.recoveryAction,
|
|
318
|
+
SYNC_RECOVERY_ACTIONS,
|
|
319
|
+
'sync failure recovery action'
|
|
320
|
+
),
|
|
321
|
+
metadata,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function syncFailureFromUserInfo(userInfo: Record<string, unknown>, fallbackMessage: string): SyncFailure {
|
|
326
|
+
if (userInfo.failure != null) {
|
|
327
|
+
return parseSyncFailure(userInfo.failure);
|
|
328
|
+
}
|
|
329
|
+
return parseSyncFailure({
|
|
330
|
+
operation: userInfo.failureOperation,
|
|
331
|
+
code: userInfo.failureCode,
|
|
332
|
+
retryable: userInfo.failureRetryable,
|
|
333
|
+
message: userInfo.failureMessage ?? fallbackMessage,
|
|
334
|
+
recoveryAction: userInfo.failureRecoveryAction,
|
|
335
|
+
metadata: userInfo.failureMetadata ?? {},
|
|
336
|
+
});
|
|
145
337
|
}
|
|
146
338
|
|
|
147
339
|
export function mapNativeError(error: unknown): SynchroError {
|
|
@@ -160,46 +352,94 @@ export function mapNativeError(error: unknown): SynchroError {
|
|
|
160
352
|
case 'SCHEMA_NOT_LOADED':
|
|
161
353
|
return new SchemaNotLoadedError();
|
|
162
354
|
case 'TABLE_NOT_SYNCED':
|
|
163
|
-
return new TableNotSyncedError(userInfo.table
|
|
355
|
+
return new TableNotSyncedError(optionalString(userInfo.table));
|
|
164
356
|
case 'UPGRADE_REQUIRED':
|
|
165
357
|
return new UpgradeRequiredError(
|
|
166
|
-
userInfo.currentVersion
|
|
167
|
-
userInfo.minimumVersion
|
|
358
|
+
optionalString(userInfo.currentVersion),
|
|
359
|
+
optionalString(userInfo.minimumVersion)
|
|
168
360
|
);
|
|
169
361
|
case 'SCHEMA_MISMATCH':
|
|
170
362
|
return new SchemaMismatchError(
|
|
171
|
-
parseInt(userInfo.serverVersion
|
|
172
|
-
userInfo.serverHash
|
|
363
|
+
parseInt(optionalString(userInfo.serverVersion), 10),
|
|
364
|
+
optionalString(userInfo.serverHash)
|
|
173
365
|
);
|
|
174
|
-
case 'SNAPSHOT_REQUIRED':
|
|
175
|
-
return new SnapshotRequiredError();
|
|
176
366
|
case 'PUSH_REJECTED': {
|
|
177
|
-
let results: PushResultItem[] = [];
|
|
178
367
|
try {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
368
|
+
const parsed = parseJSONValue(userInfo.results ?? [], 'push rejection results');
|
|
369
|
+
if (!Array.isArray(parsed)) {
|
|
370
|
+
return new InvalidResponseError('Native bridge returned invalid push rejection results');
|
|
371
|
+
}
|
|
372
|
+
return new PushRejectedError(parsed as PushRejectedMutation[]);
|
|
373
|
+
} catch (parseError) {
|
|
374
|
+
return parseError instanceof SynchroError
|
|
375
|
+
? parseError
|
|
376
|
+
: new InvalidResponseError('Native bridge returned invalid push rejection results');
|
|
182
377
|
}
|
|
183
|
-
return new PushRejectedError(results);
|
|
184
378
|
}
|
|
185
379
|
case 'NETWORK_ERROR':
|
|
186
|
-
return new NetworkError(userInfo.message
|
|
380
|
+
return new NetworkError(typeof userInfo.message === 'string' ? userInfo.message : message);
|
|
187
381
|
case 'SERVER_ERROR':
|
|
188
382
|
return new ServerError(
|
|
189
|
-
parseInt(userInfo.status ?? '0', 10),
|
|
190
|
-
userInfo.message
|
|
383
|
+
Number.parseInt(String(userInfo.status ?? '0'), 10),
|
|
384
|
+
typeof userInfo.message === 'string' ? userInfo.message : message
|
|
385
|
+
);
|
|
386
|
+
case 'PROTOCOL_ERROR': {
|
|
387
|
+
const status = Number.parseInt(String(userInfo.status ?? '0'), 10);
|
|
388
|
+
const protocolCode = requiredEnum(
|
|
389
|
+
userInfo.protocolCode,
|
|
390
|
+
PROTOCOL_ERROR_CODES,
|
|
391
|
+
'protocol error code'
|
|
191
392
|
);
|
|
393
|
+
return new ProtocolError(
|
|
394
|
+
status,
|
|
395
|
+
protocolCode,
|
|
396
|
+
typeof userInfo.message === 'string' ? userInfo.message : message
|
|
397
|
+
);
|
|
398
|
+
}
|
|
192
399
|
case 'DATABASE_ERROR':
|
|
193
|
-
return new DatabaseError(userInfo.message
|
|
400
|
+
return new DatabaseError(typeof userInfo.message === 'string' ? userInfo.message : message);
|
|
194
401
|
case 'INVALID_RESPONSE':
|
|
195
|
-
return new InvalidResponseError(userInfo.message
|
|
402
|
+
return new InvalidResponseError(typeof userInfo.message === 'string' ? userInfo.message : message);
|
|
403
|
+
case 'INVALID_SEED':
|
|
404
|
+
return new InvalidSeedError();
|
|
405
|
+
case 'SYNC_BLOCKED':
|
|
406
|
+
try {
|
|
407
|
+
return new SyncBlockedError(syncFailureFromUserInfo(userInfo, message));
|
|
408
|
+
} catch (parseError) {
|
|
409
|
+
return parseError instanceof SynchroError
|
|
410
|
+
? parseError
|
|
411
|
+
: new InvalidResponseError('Native bridge returned invalid blocking failure details');
|
|
412
|
+
}
|
|
413
|
+
case 'UNSUPPORTED_SCHEMA':
|
|
414
|
+
try {
|
|
415
|
+
return new UnsupportedSchemaError(
|
|
416
|
+
requiredEnum(userInfo.reason, SCHEMA_UNSUPPORTED_REASONS, 'unsupported schema reason')
|
|
417
|
+
);
|
|
418
|
+
} catch (parseError) {
|
|
419
|
+
return parseError instanceof SynchroError
|
|
420
|
+
? parseError
|
|
421
|
+
: new InvalidResponseError('Native bridge returned invalid unsupported schema details');
|
|
422
|
+
}
|
|
423
|
+
case 'INVALID_STATE_TRANSITION':
|
|
424
|
+
try {
|
|
425
|
+
return new InvalidStateTransitionError(
|
|
426
|
+
requiredEnum(userInfo.from, SYNC_STATUS_TYPES, 'state transition source'),
|
|
427
|
+
requiredEnum(userInfo.to, SYNC_STATUS_TYPES, 'state transition target')
|
|
428
|
+
);
|
|
429
|
+
} catch (parseError) {
|
|
430
|
+
return parseError instanceof SynchroError
|
|
431
|
+
? parseError
|
|
432
|
+
: new InvalidResponseError('Native bridge returned invalid state transition details');
|
|
433
|
+
}
|
|
196
434
|
case 'ALREADY_STARTED':
|
|
197
435
|
return new AlreadyStartedError();
|
|
198
436
|
case 'NOT_STARTED':
|
|
199
437
|
return new NotStartedError();
|
|
200
438
|
case 'TRANSACTION_TIMEOUT':
|
|
201
439
|
return new TransactionTimeoutError();
|
|
440
|
+
case 'INVALID_CONFIG':
|
|
441
|
+
return new SynchroError('INVALID_CONFIG', message);
|
|
202
442
|
default:
|
|
203
|
-
return new SynchroError(
|
|
443
|
+
return new SynchroError('UNKNOWN', message);
|
|
204
444
|
}
|
|
205
445
|
}
|
package/src/hooks/useQuery.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
2
|
import type { SynchroClient } from '../SynchroClient';
|
|
3
|
-
import type { Row } from '../types';
|
|
3
|
+
import type { AsyncUnsubscribe, Row, SQLiteBindValue } from '../types';
|
|
4
4
|
import type { SynchroError } from '../errors';
|
|
5
5
|
|
|
6
6
|
interface UseQueryResult {
|
|
@@ -10,20 +10,41 @@ interface UseQueryResult {
|
|
|
10
10
|
refresh: () => void;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function arraysEqual
|
|
14
|
-
a?: readonly
|
|
15
|
-
b?: readonly
|
|
13
|
+
function arraysEqual(
|
|
14
|
+
a?: readonly SQLiteBindValue[],
|
|
15
|
+
b?: readonly SQLiteBindValue[]
|
|
16
16
|
): boolean {
|
|
17
17
|
if (a === b) return true;
|
|
18
18
|
if (!a || !b) return !a && !b;
|
|
19
19
|
if (a.length !== b.length) return false;
|
|
20
20
|
for (let i = 0; i < a.length; i += 1) {
|
|
21
|
-
|
|
21
|
+
const left = a[i];
|
|
22
|
+
const right = b[i];
|
|
23
|
+
if (left === right) continue;
|
|
24
|
+
if (
|
|
25
|
+
typeof left !== 'object' || left === null ||
|
|
26
|
+
typeof right !== 'object' || right === null
|
|
27
|
+
) return false;
|
|
28
|
+
const leftKeys = Object.keys(left);
|
|
29
|
+
const rightKeys = Object.keys(right);
|
|
30
|
+
if (
|
|
31
|
+
leftKeys.length !== 2 || rightKeys.length !== 2 ||
|
|
32
|
+
!leftKeys.includes('type') || !rightKeys.includes('type')
|
|
33
|
+
) return false;
|
|
34
|
+
if (left.type === 'int64' && right.type === 'int64') {
|
|
35
|
+
if (!leftKeys.includes('value') || !rightKeys.includes('value')) return false;
|
|
36
|
+
if (typeof left.value !== 'string' || left.value !== right.value) return false;
|
|
37
|
+
} else if (left.type === 'bytes' && right.type === 'bytes') {
|
|
38
|
+
if (!leftKeys.includes('base64') || !rightKeys.includes('base64')) return false;
|
|
39
|
+
if (typeof left.base64 !== 'string' || left.base64 !== right.base64) return false;
|
|
40
|
+
} else {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
22
43
|
}
|
|
23
44
|
return true;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
function useStableArray<T>(value?: readonly T[]): readonly T[] | undefined {
|
|
47
|
+
function useStableArray<T extends SQLiteBindValue>(value?: readonly T[]): readonly T[] | undefined {
|
|
27
48
|
const ref = useRef<readonly T[] | undefined>(value);
|
|
28
49
|
if (!arraysEqual(ref.current, value)) {
|
|
29
50
|
ref.current = value ? [...value] : value;
|
|
@@ -34,7 +55,7 @@ function useStableArray<T>(value?: readonly T[]): readonly T[] | undefined {
|
|
|
34
55
|
export function useQuery(
|
|
35
56
|
client: SynchroClient,
|
|
36
57
|
sql: string,
|
|
37
|
-
params?:
|
|
58
|
+
params?: SQLiteBindValue[],
|
|
38
59
|
tables?: string[]
|
|
39
60
|
): UseQueryResult {
|
|
40
61
|
const [data, setData] = useState<Row[]>([]);
|
|
@@ -55,21 +76,43 @@ export function useQuery(
|
|
|
55
76
|
// Reactive mode: use watch()
|
|
56
77
|
setLoading(true);
|
|
57
78
|
let firstResult = true;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
let disposed = false;
|
|
80
|
+
let unsubscribe: AsyncUnsubscribe | null = null;
|
|
81
|
+
client
|
|
82
|
+
.watch(sql, stableParams, stableTables as string[], (rows) => {
|
|
83
|
+
if (disposed) return;
|
|
84
|
+
setData(rows);
|
|
85
|
+
setError(null);
|
|
86
|
+
if (firstResult) {
|
|
87
|
+
setLoading(false);
|
|
88
|
+
firstResult = false;
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
.then((remove) => {
|
|
92
|
+
if (disposed) {
|
|
93
|
+
void remove().catch(() => {});
|
|
94
|
+
} else {
|
|
95
|
+
unsubscribe = remove;
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
.catch((err) => {
|
|
99
|
+
if (!disposed) {
|
|
100
|
+
setError(err);
|
|
101
|
+
setLoading(false);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return () => {
|
|
105
|
+
disposed = true;
|
|
106
|
+
if (unsubscribe) {
|
|
107
|
+
void unsubscribe().catch(() => {});
|
|
64
108
|
}
|
|
65
|
-
}
|
|
66
|
-
return unsubscribe;
|
|
109
|
+
};
|
|
67
110
|
} else {
|
|
68
111
|
// One-shot mode: use query()
|
|
69
112
|
let cancelled = false;
|
|
70
113
|
setLoading(true);
|
|
71
114
|
client
|
|
72
|
-
.query(sql, stableParams
|
|
115
|
+
.query(sql, stableParams)
|
|
73
116
|
.then((rows) => {
|
|
74
117
|
if (!cancelled) {
|
|
75
118
|
setData(rows);
|
|
@@ -1,17 +1,50 @@
|
|
|
1
1
|
import { useState, useEffect } from 'react';
|
|
2
2
|
import type { SynchroClient } from '../SynchroClient';
|
|
3
3
|
import type { SyncStatus } from '../types';
|
|
4
|
+
import { mapNativeError, type SynchroError } from '../errors';
|
|
5
|
+
|
|
6
|
+
const uninitialized: SyncStatus = {
|
|
7
|
+
status: 'uninitialized',
|
|
8
|
+
retryAt: null,
|
|
9
|
+
operation: null,
|
|
10
|
+
failure: null,
|
|
11
|
+
};
|
|
4
12
|
|
|
5
13
|
export function useSyncStatus(client: SynchroClient): SyncStatus {
|
|
6
|
-
const [
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
const [snapshot, setSnapshot] = useState<{
|
|
15
|
+
client: SynchroClient;
|
|
16
|
+
status: SyncStatus;
|
|
17
|
+
error: SynchroError | null;
|
|
18
|
+
}>({ client, status: uninitialized, error: null });
|
|
10
19
|
|
|
11
20
|
useEffect(() => {
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
let active = true;
|
|
22
|
+
let receivedEvent = false;
|
|
23
|
+
setSnapshot({ client, status: uninitialized, error: null });
|
|
24
|
+
const unsubscribe = client.onStatusChange((status) => {
|
|
25
|
+
if (!active) return;
|
|
26
|
+
receivedEvent = true;
|
|
27
|
+
setSnapshot({ client, status, error: null });
|
|
28
|
+
});
|
|
29
|
+
client.getSyncStatus().then(
|
|
30
|
+
(status) => {
|
|
31
|
+
if (active && !receivedEvent) {
|
|
32
|
+
setSnapshot({ client, status, error: null });
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
(error: unknown) => {
|
|
36
|
+
if (active && !receivedEvent) {
|
|
37
|
+
setSnapshot({ client, status: uninitialized, error: mapNativeError(error) });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
return () => {
|
|
42
|
+
active = false;
|
|
43
|
+
unsubscribe();
|
|
44
|
+
};
|
|
14
45
|
}, [client]);
|
|
15
46
|
|
|
16
|
-
return
|
|
47
|
+
if (snapshot.client !== client) return uninitialized;
|
|
48
|
+
if (snapshot.error) throw snapshot.error;
|
|
49
|
+
return snapshot.status;
|
|
17
50
|
}
|