@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/SynchroClient.ts
CHANGED
|
@@ -1,55 +1,723 @@
|
|
|
1
1
|
import { Platform } from 'react-native';
|
|
2
2
|
import type { EventSubscription } from 'react-native';
|
|
3
3
|
import NativeSynchro from './NativeSynchro';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
InvalidResponseError,
|
|
6
|
+
SynchroError,
|
|
7
|
+
mapNativeError,
|
|
8
|
+
parseSyncFailure,
|
|
9
|
+
} from './errors';
|
|
10
|
+
import { assertValidSQLiteBindParams } from './sqliteValues';
|
|
11
|
+
import { SYNC_OPERATION_KINDS, SYNC_STATUS_TYPES } from './types';
|
|
5
12
|
import type {
|
|
6
13
|
Row,
|
|
7
14
|
ExecResult,
|
|
8
15
|
BatchResult,
|
|
9
16
|
SQLStatement,
|
|
17
|
+
SQLiteBindValue,
|
|
10
18
|
ColumnDef,
|
|
11
19
|
TableOptions,
|
|
20
|
+
ReadTransaction,
|
|
12
21
|
Transaction,
|
|
13
|
-
CheckpointMode,
|
|
14
22
|
SyncStatus,
|
|
15
23
|
SyncStatusType,
|
|
24
|
+
SyncEvent,
|
|
25
|
+
SyncOperationKind,
|
|
26
|
+
SchemaAction,
|
|
27
|
+
MutationStatus,
|
|
28
|
+
MutationRejectionCode,
|
|
29
|
+
PendingMutationInspection,
|
|
30
|
+
RejectedMutationInspection,
|
|
31
|
+
ClientStateInspection,
|
|
32
|
+
ScopeStateInspection,
|
|
33
|
+
ScopeRowInspection,
|
|
34
|
+
RebuildAttemptInspection,
|
|
35
|
+
TransportObservation,
|
|
36
|
+
TransportObservationSnapshot,
|
|
37
|
+
TransportOperationClass,
|
|
16
38
|
ConflictEvent,
|
|
17
39
|
SynchroConfig,
|
|
18
40
|
Unsubscribe,
|
|
41
|
+
AsyncUnsubscribe,
|
|
19
42
|
} from './types';
|
|
20
43
|
|
|
21
44
|
let observerCounter = 0;
|
|
45
|
+
let activeNativeOwner: object | null = null;
|
|
46
|
+
const inspectionOwners = new WeakMap<SynchroClient, object>();
|
|
47
|
+
const inspectionCapacities = new WeakMap<SynchroClient, number>();
|
|
48
|
+
const inspectionRequireNewDatabases = new WeakMap<SynchroClient, boolean>();
|
|
49
|
+
|
|
22
50
|
function nextObserverID(): string {
|
|
23
51
|
return `obs_${++observerCounter}_${Date.now()}`;
|
|
24
52
|
}
|
|
25
53
|
|
|
54
|
+
function nullableRow(row: unknown): Row | null {
|
|
55
|
+
return row == null ? null : (row as Row);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type NativeSyncStatus = {
|
|
59
|
+
status: unknown;
|
|
60
|
+
retryAt: unknown;
|
|
61
|
+
operation?: unknown;
|
|
62
|
+
failure?: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type NativeSyncEvent = {
|
|
66
|
+
type: unknown;
|
|
67
|
+
from: unknown;
|
|
68
|
+
to: unknown;
|
|
69
|
+
operation: unknown;
|
|
70
|
+
attempt: unknown;
|
|
71
|
+
retryAt: unknown;
|
|
72
|
+
source: unknown;
|
|
73
|
+
target: unknown;
|
|
74
|
+
action: unknown;
|
|
75
|
+
mutationID: unknown;
|
|
76
|
+
tableID: unknown;
|
|
77
|
+
mutationStatus: unknown;
|
|
78
|
+
rejectionCode: unknown;
|
|
79
|
+
scopeID: unknown;
|
|
80
|
+
rebuildID: unknown;
|
|
81
|
+
failure: unknown;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const SYNC_EVENT_TYPES = [
|
|
85
|
+
'state_changed',
|
|
86
|
+
'backoff',
|
|
87
|
+
'schema_applying',
|
|
88
|
+
'schema_applied',
|
|
89
|
+
'mutation_accepted',
|
|
90
|
+
'mutation_rejected',
|
|
91
|
+
'rebuild_requested',
|
|
92
|
+
'rebuild_completed',
|
|
93
|
+
'failure',
|
|
94
|
+
] as const;
|
|
95
|
+
|
|
96
|
+
type SyncEventType = (typeof SYNC_EVENT_TYPES)[number];
|
|
97
|
+
|
|
98
|
+
const SCHEMA_ACTIONS: readonly SchemaAction[] = [
|
|
99
|
+
'none',
|
|
100
|
+
'replace',
|
|
101
|
+
'rebuild_local',
|
|
102
|
+
'unsupported',
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const MUTATION_STATUSES: readonly MutationStatus[] = [
|
|
106
|
+
'applied',
|
|
107
|
+
'conflict',
|
|
108
|
+
'rejected_terminal',
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const MUTATION_REJECTION_CODES: readonly MutationRejectionCode[] = [
|
|
112
|
+
'version_conflict',
|
|
113
|
+
'row_already_exists',
|
|
114
|
+
'row_deleted',
|
|
115
|
+
'row_not_found',
|
|
116
|
+
'schema_incompatible',
|
|
117
|
+
'policy_rejected',
|
|
118
|
+
'validation_failed',
|
|
119
|
+
'table_not_synced',
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const MUTATION_OPERATIONS = ['insert', 'upsert', 'update', 'delete'] as const;
|
|
123
|
+
const LOCAL_MUTATION_STATUSES = [
|
|
124
|
+
'pending',
|
|
125
|
+
'sealed',
|
|
126
|
+
'superseded_before_send',
|
|
127
|
+
'cancelled_before_send',
|
|
128
|
+
'blocked_by_predecessor',
|
|
129
|
+
'server_rejected',
|
|
130
|
+
'exceeds_push_limit',
|
|
131
|
+
] as const;
|
|
132
|
+
|
|
133
|
+
const TRANSPORT_OPERATION_CLASSES: readonly TransportOperationClass[] = [
|
|
134
|
+
'connect',
|
|
135
|
+
'pull',
|
|
136
|
+
'push',
|
|
137
|
+
'checkpoint',
|
|
138
|
+
'schemas',
|
|
139
|
+
'rebuild',
|
|
140
|
+
'other',
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const LEGAL_SYNC_STATUS_TRANSITIONS: Readonly<
|
|
144
|
+
Record<SyncStatusType, readonly SyncStatusType[]>
|
|
145
|
+
> = {
|
|
146
|
+
uninitialized: ['local_ready', 'error', 'stopped'],
|
|
147
|
+
local_ready: ['connecting', 'error', 'stopped'],
|
|
148
|
+
connecting: ['schema_applying', 'ready', 'backoff', 'error', 'stopped'],
|
|
149
|
+
schema_applying: ['ready', 'rebuilding', 'error', 'stopped'],
|
|
150
|
+
ready: ['connecting', 'pushing', 'pulling', 'rebuilding', 'error', 'stopped'],
|
|
151
|
+
pushing: ['pushing', 'ready', 'pulling', 'connecting', 'backoff', 'error', 'stopped'],
|
|
152
|
+
pulling: ['pulling', 'ready', 'rebuilding', 'connecting', 'backoff', 'error', 'stopped'],
|
|
153
|
+
rebuilding: ['rebuilding', 'ready', 'connecting', 'backoff', 'error', 'stopped'],
|
|
154
|
+
backoff: ['connecting', 'pushing', 'pulling', 'rebuilding', 'error', 'stopped'],
|
|
155
|
+
error: ['local_ready', 'stopped'],
|
|
156
|
+
stopped: ['local_ready'],
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
160
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function requiredString(value: unknown, name: string): string {
|
|
164
|
+
if (typeof value !== 'string') {
|
|
165
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
166
|
+
}
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function nullableString(value: unknown, name: string): string | null {
|
|
171
|
+
if (value == null) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return requiredString(value, name);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function requiredSafeInteger(value: unknown, name: string): number {
|
|
178
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
|
|
179
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function requiredNonnegativeSafeInteger(value: unknown, name: string): number {
|
|
185
|
+
const integer = requiredSafeInteger(value, name);
|
|
186
|
+
if (integer < 0) {
|
|
187
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
188
|
+
}
|
|
189
|
+
return integer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function nullableSafeInteger(value: unknown, name: string): number | null {
|
|
193
|
+
return value == null ? null : requiredSafeInteger(value, name);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseDate(value: unknown, name: string): Date | null {
|
|
197
|
+
const stringValue = nullableString(value, name);
|
|
198
|
+
if (stringValue === null) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const date = new Date(stringValue);
|
|
202
|
+
if (Number.isNaN(date.getTime())) {
|
|
203
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
204
|
+
}
|
|
205
|
+
return date;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseStatus(value: unknown): SyncStatus | null {
|
|
209
|
+
if (!isRecord(value) || typeof value.status !== 'string') {
|
|
210
|
+
throw new InvalidResponseError('Native bridge returned an invalid sync status');
|
|
211
|
+
}
|
|
212
|
+
if (!SYNC_STATUS_TYPES.includes(value.status as SyncStatusType)) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const status = value.status as SyncStatusType;
|
|
216
|
+
const retryAt = parseDate(value.retryAt, 'retryAt');
|
|
217
|
+
const operationValue = nullableString(value.operation, 'status operation');
|
|
218
|
+
const operation = operationValue === null
|
|
219
|
+
? null
|
|
220
|
+
: SYNC_OPERATION_KINDS.includes(operationValue as SyncOperationKind)
|
|
221
|
+
? (operationValue as SyncOperationKind)
|
|
222
|
+
: (() => {
|
|
223
|
+
throw new InvalidResponseError('Native bridge returned an invalid status operation');
|
|
224
|
+
})();
|
|
225
|
+
const failure = value.failure == null ? null : parseSyncFailure(value.failure);
|
|
226
|
+
|
|
227
|
+
if (status === 'backoff') {
|
|
228
|
+
if (retryAt === null || operation === null || failure !== null) {
|
|
229
|
+
throw new InvalidResponseError('Native bridge returned invalid backoff status details');
|
|
230
|
+
}
|
|
231
|
+
return { status, retryAt, operation, failure: null };
|
|
232
|
+
}
|
|
233
|
+
if (status === 'error') {
|
|
234
|
+
if (retryAt !== null || operation !== null || failure === null) {
|
|
235
|
+
throw new InvalidResponseError('Native bridge returned invalid error status details');
|
|
236
|
+
}
|
|
237
|
+
return { status, retryAt: null, operation: null, failure };
|
|
238
|
+
}
|
|
239
|
+
if (retryAt !== null || operation !== null || failure !== null) {
|
|
240
|
+
throw new InvalidResponseError('Native bridge returned invalid state detail fields');
|
|
241
|
+
}
|
|
242
|
+
return { status, retryAt: null, operation: null, failure: null };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseSchemaRef(value: unknown, name: string) {
|
|
246
|
+
if (!isRecord(value) || typeof value.version !== 'number' || !Number.isSafeInteger(value.version)) {
|
|
247
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
version: value.version,
|
|
251
|
+
hash: requiredString(value.hash, `${name} hash`),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parseMutationStatus(value: unknown): MutationStatus {
|
|
256
|
+
if (!MUTATION_STATUSES.includes(value as MutationStatus)) {
|
|
257
|
+
throw new InvalidResponseError('Native bridge returned an invalid mutation status');
|
|
258
|
+
}
|
|
259
|
+
return value as MutationStatus;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function parseRejectionCode(value: unknown): MutationRejectionCode | null {
|
|
263
|
+
if (value == null) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (!MUTATION_REJECTION_CODES.includes(value as MutationRejectionCode)) {
|
|
267
|
+
throw new InvalidResponseError('Native bridge returned an invalid mutation rejection code');
|
|
268
|
+
}
|
|
269
|
+
return value as MutationRejectionCode;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function parseSchemaAction(value: unknown): SchemaAction {
|
|
273
|
+
if (!SCHEMA_ACTIONS.includes(value as SchemaAction)) {
|
|
274
|
+
throw new InvalidResponseError('Native bridge returned an invalid schema action');
|
|
275
|
+
}
|
|
276
|
+
return value as SchemaAction;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function parseSyncEvent(value: NativeSyncEvent): SyncEvent | null {
|
|
280
|
+
if (typeof value.type !== 'string') {
|
|
281
|
+
throw new InvalidResponseError('Native bridge returned an invalid sync event');
|
|
282
|
+
}
|
|
283
|
+
if (!SYNC_EVENT_TYPES.includes(value.type as SyncEventType)) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
switch (value.type as SyncEventType) {
|
|
287
|
+
case 'state_changed': {
|
|
288
|
+
if (typeof value.from !== 'string' || typeof value.to !== 'string') {
|
|
289
|
+
throw new InvalidResponseError('Native bridge returned an invalid lifecycle event');
|
|
290
|
+
}
|
|
291
|
+
if (
|
|
292
|
+
!SYNC_STATUS_TYPES.includes(value.from as SyncStatusType) ||
|
|
293
|
+
!SYNC_STATUS_TYPES.includes(value.to as SyncStatusType)
|
|
294
|
+
) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const from = value.from as SyncStatusType;
|
|
298
|
+
const to = value.to as SyncStatusType;
|
|
299
|
+
if (!LEGAL_SYNC_STATUS_TRANSITIONS[from].includes(to)) {
|
|
300
|
+
throw new InvalidResponseError('Native bridge returned an invalid lifecycle event');
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
type: 'state_changed',
|
|
304
|
+
from,
|
|
305
|
+
to,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
case 'backoff': {
|
|
309
|
+
const attempt = value.attempt;
|
|
310
|
+
if (typeof attempt !== 'number' || !Number.isSafeInteger(attempt) || attempt < 0) {
|
|
311
|
+
throw new InvalidResponseError('Native bridge returned an invalid backoff attempt');
|
|
312
|
+
}
|
|
313
|
+
const retryAt = parseDate(value.retryAt, 'backoff retryAt');
|
|
314
|
+
if (retryAt === null) {
|
|
315
|
+
throw new InvalidResponseError('Native bridge returned a missing backoff retryAt');
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
type: 'backoff',
|
|
319
|
+
operation: SYNC_OPERATION_KINDS.includes(value.operation as SyncOperationKind)
|
|
320
|
+
? (value.operation as SyncOperationKind)
|
|
321
|
+
: (() => {
|
|
322
|
+
throw new InvalidResponseError('Native bridge returned an invalid backoff operation');
|
|
323
|
+
})(),
|
|
324
|
+
attempt,
|
|
325
|
+
retryAt,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
case 'schema_applying':
|
|
329
|
+
case 'schema_applied':
|
|
330
|
+
return {
|
|
331
|
+
type: value.type as 'schema_applying' | 'schema_applied',
|
|
332
|
+
source: parseSchemaRef(value.source, 'schema source'),
|
|
333
|
+
target: parseSchemaRef(value.target, 'schema target'),
|
|
334
|
+
action: parseSchemaAction(value.action),
|
|
335
|
+
};
|
|
336
|
+
case 'mutation_accepted':
|
|
337
|
+
case 'mutation_rejected': {
|
|
338
|
+
const mutationID = requiredString(value.mutationID, 'mutation ID');
|
|
339
|
+
return {
|
|
340
|
+
type: value.type as 'mutation_accepted' | 'mutation_rejected',
|
|
341
|
+
mutationID,
|
|
342
|
+
tableID: requiredString(value.tableID, 'mutation table ID'),
|
|
343
|
+
status: parseMutationStatus(value.mutationStatus),
|
|
344
|
+
rejectionCode: parseRejectionCode(value.rejectionCode),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
case 'rebuild_requested':
|
|
348
|
+
case 'rebuild_completed':
|
|
349
|
+
return {
|
|
350
|
+
type: value.type as 'rebuild_requested' | 'rebuild_completed',
|
|
351
|
+
scopeID: requiredString(value.scopeID, 'rebuild scope ID'),
|
|
352
|
+
rebuildID: requiredString(value.rebuildID, 'rebuild ID'),
|
|
353
|
+
};
|
|
354
|
+
case 'failure': {
|
|
355
|
+
return { type: 'failure', failure: parseSyncFailure(value.failure) };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function parseNativeDTO<T>(json: string): T {
|
|
361
|
+
try {
|
|
362
|
+
return JSON.parse(json) as T;
|
|
363
|
+
} catch {
|
|
364
|
+
throw new InvalidResponseError('Native bridge returned malformed JSON');
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function parseNativeArray(json: string, name: string): unknown[] {
|
|
369
|
+
const value = parseNativeDTO<unknown>(json);
|
|
370
|
+
if (!Array.isArray(value)) {
|
|
371
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
|
|
372
|
+
}
|
|
373
|
+
return value;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function parsePendingMutationInspection(
|
|
377
|
+
value: unknown,
|
|
378
|
+
index: number
|
|
379
|
+
): PendingMutationInspection {
|
|
380
|
+
const name = `pending mutation at index ${index}`;
|
|
381
|
+
if (!isRecord(value)) {
|
|
382
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
383
|
+
}
|
|
384
|
+
if (!MUTATION_OPERATIONS.includes(value.operation as never)) {
|
|
385
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name} operation`);
|
|
386
|
+
}
|
|
387
|
+
if (!LOCAL_MUTATION_STATUSES.includes(value.status as never)) {
|
|
388
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name} status`);
|
|
389
|
+
}
|
|
390
|
+
if (!Array.isArray(value.authoredFields)) {
|
|
391
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name} fields`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const authoredFields = value.authoredFields.map((field, fieldIndex) => {
|
|
395
|
+
if (!isRecord(field) || !Object.prototype.hasOwnProperty.call(field, 'value')) {
|
|
396
|
+
throw new InvalidResponseError(
|
|
397
|
+
`Native bridge returned an invalid ${name} field at index ${fieldIndex}`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
fieldID: requiredString(field.fieldID, `${name} field ID`),
|
|
402
|
+
logicalType: requiredString(field.logicalType, `${name} field logical type`),
|
|
403
|
+
value: field.value as PendingMutationInspection['authoredFields'][number]['value'],
|
|
404
|
+
};
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
mutationID: requiredString(value.mutationID, `${name} ID`),
|
|
409
|
+
localOrder: requiredSafeInteger(value.localOrder, `${name} local order`),
|
|
410
|
+
tableID: requiredString(value.tableID, `${name} table ID`),
|
|
411
|
+
tableName: requiredString(value.tableName, `${name} table name`),
|
|
412
|
+
recordID: requiredString(value.recordID, `${name} record ID`),
|
|
413
|
+
primaryKeyFieldID: requiredString(
|
|
414
|
+
value.primaryKeyFieldID,
|
|
415
|
+
`${name} primary-key field ID`
|
|
416
|
+
),
|
|
417
|
+
primaryKeyLogicalType: requiredString(
|
|
418
|
+
value.primaryKeyLogicalType,
|
|
419
|
+
`${name} primary-key logical type`
|
|
420
|
+
),
|
|
421
|
+
operation: value.operation as PendingMutationInspection['operation'],
|
|
422
|
+
authoredSchema: parseSchemaRef(value.authoredSchema, `${name} authored schema`),
|
|
423
|
+
baseVersion: nullableString(value.baseVersion, `${name} base version`),
|
|
424
|
+
clientVersion: requiredString(value.clientVersion, `${name} client version`),
|
|
425
|
+
status: value.status as PendingMutationInspection['status'],
|
|
426
|
+
sourceKind: requiredString(value.sourceKind, `${name} source kind`),
|
|
427
|
+
dependsOnMutationID: nullableString(
|
|
428
|
+
value.dependsOnMutationID,
|
|
429
|
+
`${name} predecessor mutation ID`
|
|
430
|
+
),
|
|
431
|
+
normalizedMutationID: nullableString(
|
|
432
|
+
value.normalizedMutationID,
|
|
433
|
+
`${name} normalized mutation ID`
|
|
434
|
+
),
|
|
435
|
+
sealedBatchID: nullableString(value.sealedBatchID, `${name} sealed batch ID`),
|
|
436
|
+
sealedOrdinal: nullableSafeInteger(value.sealedOrdinal, `${name} sealed ordinal`),
|
|
437
|
+
authoredFields,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function parseRejectedMutationInspection(
|
|
442
|
+
value: unknown,
|
|
443
|
+
index: number
|
|
444
|
+
): RejectedMutationInspection {
|
|
445
|
+
const name = `rejected mutation at index ${index}`;
|
|
446
|
+
if (!isRecord(value)) {
|
|
447
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
448
|
+
}
|
|
449
|
+
const code = parseRejectionCode(value.code);
|
|
450
|
+
if (code === null) {
|
|
451
|
+
throw new InvalidResponseError(`Native bridge returned a missing ${name} code`);
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
mutationID: requiredString(value.mutationID, `${name} ID`),
|
|
455
|
+
tableName: requiredString(value.tableName, `${name} table name`),
|
|
456
|
+
recordID: requiredString(value.recordID, `${name} record ID`),
|
|
457
|
+
status: parseMutationStatus(value.status),
|
|
458
|
+
code,
|
|
459
|
+
message: nullableString(value.message, `${name} message`),
|
|
460
|
+
serverRowJSON: nullableString(value.serverRowJSON, `${name} server row JSON`),
|
|
461
|
+
serverVersion: nullableString(value.serverVersion, `${name} server version`),
|
|
462
|
+
mutationJSON: requiredString(value.mutationJSON, `${name} mutation JSON`),
|
|
463
|
+
rejectionJSON: requiredString(value.rejectionJSON, `${name} rejection JSON`),
|
|
464
|
+
createdAt: requiredString(value.createdAt, `${name} creation time`),
|
|
465
|
+
updatedAt: requiredString(value.updatedAt, `${name} update time`),
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function parseScopeStateInspection(value: unknown, index: number): ScopeStateInspection {
|
|
470
|
+
const name = `scope state at index ${index}`;
|
|
471
|
+
if (!isRecord(value)) {
|
|
472
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
scopeID: requiredString(value.scope_id, `${name} ID`),
|
|
476
|
+
cursor: nullableString(value.cursor, `${name} cursor`),
|
|
477
|
+
checksum: nullableString(value.checksum, `${name} checksum`),
|
|
478
|
+
localChecksum: requiredString(value.local_checksum, `${name} local checksum`),
|
|
479
|
+
generation: requiredSafeInteger(value.generation, `${name} generation`),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseScopeRowInspection(value: unknown, index: number): ScopeRowInspection {
|
|
484
|
+
const name = `scope row at index ${index}`;
|
|
485
|
+
if (!isRecord(value)) {
|
|
486
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
487
|
+
}
|
|
488
|
+
return {
|
|
489
|
+
scopeID: requiredString(value.scope_id, `${name} scope ID`),
|
|
490
|
+
tableName: requiredString(value.table_name, `${name} table name`),
|
|
491
|
+
recordID: requiredString(value.record_id, `${name} record ID`),
|
|
492
|
+
checksum: requiredString(value.checksum, `${name} checksum`),
|
|
493
|
+
generation: requiredSafeInteger(value.generation, `${name} generation`),
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function parseRebuildAttemptInspection(value: unknown, index: number): RebuildAttemptInspection {
|
|
498
|
+
const name = `rebuild attempt at index ${index}`;
|
|
499
|
+
if (!isRecord(value)) {
|
|
500
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
scopeID: requiredString(value.scope_id, `${name} scope ID`),
|
|
504
|
+
rebuildID: requiredString(value.rebuild_id, `${name} rebuild ID`),
|
|
505
|
+
clientGeneration: requiredSafeInteger(value.client_generation, `${name} client generation`),
|
|
506
|
+
schemaVersion: requiredSafeInteger(value.schema_version, `${name} schema version`),
|
|
507
|
+
schemaHash: requiredString(value.schema_hash, `${name} schema hash`),
|
|
508
|
+
generation: requiredSafeInteger(value.generation, `${name} generation`),
|
|
509
|
+
cursor: nullableString(value.cursor, `${name} cursor`),
|
|
510
|
+
pageLimit: requiredSafeInteger(value.page_limit, `${name} page limit`),
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function parseClientStateInspection(value: unknown): ClientStateInspection {
|
|
515
|
+
if (!isRecord(value)) {
|
|
516
|
+
throw new InvalidResponseError('Native bridge returned invalid client state inspection');
|
|
517
|
+
}
|
|
518
|
+
const schema = value.schema == null ? null : parseSchemaRef(value.schema, 'client schema');
|
|
519
|
+
if (!Array.isArray(value.scope_states) || !Array.isArray(value.scope_rows) || !Array.isArray(value.rebuild_attempts)) {
|
|
520
|
+
throw new InvalidResponseError('Native bridge returned incomplete client state inspection');
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
schema,
|
|
524
|
+
scopeStates: value.scope_states.map(parseScopeStateInspection),
|
|
525
|
+
scopeRows: value.scope_rows.map(parseScopeRowInspection),
|
|
526
|
+
rebuildAttempts: value.rebuild_attempts.map(parseRebuildAttemptInspection),
|
|
527
|
+
applicationRowCount: requiredNonnegativeSafeInteger(
|
|
528
|
+
value.application_row_count,
|
|
529
|
+
'application row count'
|
|
530
|
+
),
|
|
531
|
+
mutationLedgerCount: requiredNonnegativeSafeInteger(
|
|
532
|
+
value.mutation_ledger_count,
|
|
533
|
+
'mutation ledger count'
|
|
534
|
+
),
|
|
535
|
+
mutationOutcomeCount: requiredNonnegativeSafeInteger(
|
|
536
|
+
value.mutation_outcome_count,
|
|
537
|
+
'mutation outcome count'
|
|
538
|
+
),
|
|
539
|
+
sealedBatchCount: requiredNonnegativeSafeInteger(
|
|
540
|
+
value.sealed_batch_count,
|
|
541
|
+
'sealed batch count'
|
|
542
|
+
),
|
|
543
|
+
rejectedMutationCount: requiredNonnegativeSafeInteger(
|
|
544
|
+
value.rejected_mutation_count,
|
|
545
|
+
'rejected mutation count'
|
|
546
|
+
),
|
|
547
|
+
scopeStateCount: requiredNonnegativeSafeInteger(
|
|
548
|
+
value.scope_state_count,
|
|
549
|
+
'scope state count'
|
|
550
|
+
),
|
|
551
|
+
scopeRowCount: requiredNonnegativeSafeInteger(
|
|
552
|
+
value.scope_row_count,
|
|
553
|
+
'scope row count'
|
|
554
|
+
),
|
|
555
|
+
provenanceCount: requiredNonnegativeSafeInteger(
|
|
556
|
+
value.provenance_count,
|
|
557
|
+
'provenance count'
|
|
558
|
+
),
|
|
559
|
+
rowMetadataCount: requiredNonnegativeSafeInteger(
|
|
560
|
+
value.row_metadata_count,
|
|
561
|
+
'row metadata count'
|
|
562
|
+
),
|
|
563
|
+
rebuildAttemptCount: requiredNonnegativeSafeInteger(
|
|
564
|
+
value.rebuild_attempt_count,
|
|
565
|
+
'rebuild attempt count'
|
|
566
|
+
),
|
|
567
|
+
rebuildReceiptCount: requiredNonnegativeSafeInteger(
|
|
568
|
+
value.rebuild_receipt_count,
|
|
569
|
+
'rebuild receipt count'
|
|
570
|
+
),
|
|
571
|
+
provenanceMaintenanceWorkCursor: requiredNonnegativeInt64String(
|
|
572
|
+
value.provenance_maintenance_work_cursor,
|
|
573
|
+
'provenance maintenance work cursor'
|
|
574
|
+
),
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function requiredNonnegativeInt64String(value: unknown, name: string): string {
|
|
579
|
+
const maximum = '9223372036854775807';
|
|
580
|
+
if (
|
|
581
|
+
typeof value !== 'string' ||
|
|
582
|
+
!/^(0|[1-9][0-9]*)$/.test(value) ||
|
|
583
|
+
value.length > maximum.length ||
|
|
584
|
+
(value.length === maximum.length && value > maximum)
|
|
585
|
+
) {
|
|
586
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
|
|
587
|
+
}
|
|
588
|
+
return value;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function optionalTransportFacts(value: unknown, name: string): Record<string, import('./types').JSONValue> | undefined {
|
|
592
|
+
if (value == null) {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
if (!isRecord(value)) {
|
|
596
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
|
|
597
|
+
}
|
|
598
|
+
return value as Record<string, import('./types').JSONValue>;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function optionalTransportRequestFacts(
|
|
602
|
+
value: unknown,
|
|
603
|
+
name: string
|
|
604
|
+
): Record<string, import('./types').JSONValue> | undefined {
|
|
605
|
+
const facts = optionalTransportFacts(value, name);
|
|
606
|
+
const mutationCount = facts?.mutation_count;
|
|
607
|
+
if (
|
|
608
|
+
facts !== undefined &&
|
|
609
|
+
Object.prototype.hasOwnProperty.call(facts, 'mutation_count') &&
|
|
610
|
+
(typeof mutationCount !== 'number' ||
|
|
611
|
+
!Number.isSafeInteger(mutationCount) ||
|
|
612
|
+
mutationCount < 0)
|
|
613
|
+
) {
|
|
614
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name} mutation count`);
|
|
615
|
+
}
|
|
616
|
+
return facts;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function parseTransportObservation(value: unknown, index: number): TransportObservation {
|
|
620
|
+
const name = `transport observation at index ${index}`;
|
|
621
|
+
if (!isRecord(value) || !TRANSPORT_OPERATION_CLASSES.includes(value.operation_class as TransportOperationClass)) {
|
|
622
|
+
throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
|
|
623
|
+
}
|
|
624
|
+
const cursorFingerprints = value.cursor_fingerprints;
|
|
625
|
+
if (cursorFingerprints !== undefined && (!Array.isArray(cursorFingerprints) || !cursorFingerprints.every((item) => typeof item === 'string'))) {
|
|
626
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name} cursor fingerprints`);
|
|
627
|
+
}
|
|
628
|
+
const cursorFingerprintsComplete = value.cursor_fingerprints_complete;
|
|
629
|
+
if (cursorFingerprintsComplete !== undefined && typeof cursorFingerprintsComplete !== 'boolean') {
|
|
630
|
+
throw new InvalidResponseError(`Native bridge returned invalid ${name} cursor completeness`);
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
sequence: requiredSafeInteger(value.sequence, `${name} sequence`),
|
|
634
|
+
operationClass: value.operation_class as TransportOperationClass,
|
|
635
|
+
statusCode: requiredSafeInteger(value.status_code, `${name} status code`),
|
|
636
|
+
durationNanoseconds: requiredSafeInteger(value.duration_nanoseconds, `${name} duration`),
|
|
637
|
+
...(cursorFingerprints === undefined ? {} : { cursorFingerprints: [...cursorFingerprints] as string[] }),
|
|
638
|
+
...(cursorFingerprintsComplete === undefined ? {} : { cursorFingerprintsComplete }),
|
|
639
|
+
...(value.request_facts == null ? {} : { requestFacts: optionalTransportRequestFacts(value.request_facts, `${name} request facts`)! }),
|
|
640
|
+
...(value.rebuild_response_facts == null ? {} : { rebuildResponseFacts: optionalTransportFacts(value.rebuild_response_facts, `${name} rebuild response facts`)! }),
|
|
641
|
+
...(value.pull_response_facts == null ? {} : { pullResponseFacts: optionalTransportFacts(value.pull_response_facts, `${name} pull response facts`)! }),
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
export function parseTransportObservationSnapshot(value: unknown): TransportObservationSnapshot {
|
|
646
|
+
if (!isRecord(value) || !Array.isArray(value.observations) || typeof value.overflowed !== 'boolean') {
|
|
647
|
+
throw new InvalidResponseError('Native bridge returned invalid transport observations');
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
observations: value.observations.map(parseTransportObservation),
|
|
651
|
+
overflowed: value.overflowed,
|
|
652
|
+
sequenceCheckpoint: requiredSafeInteger(value.sequence_checkpoint, 'transport sequence checkpoint'),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function checkedParams(params: readonly SQLiteBindValue[]): readonly SQLiteBindValue[] {
|
|
657
|
+
return assertValidSQLiteBindParams(params);
|
|
658
|
+
}
|
|
659
|
+
|
|
26
660
|
export class SynchroClient {
|
|
27
|
-
private readonly
|
|
661
|
+
private readonly nativeOwner = {};
|
|
28
662
|
private readonly config: SynchroConfig;
|
|
29
663
|
private authSubscription: EventSubscription | null = null;
|
|
30
664
|
|
|
31
665
|
constructor(config: SynchroConfig) {
|
|
32
666
|
this.config = config;
|
|
667
|
+
inspectionOwners.set(this, this.nativeOwner);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
private get native(): typeof NativeSynchro {
|
|
671
|
+
if (activeNativeOwner !== null && activeNativeOwner !== this.nativeOwner) {
|
|
672
|
+
throw new SynchroError(
|
|
673
|
+
'CLIENT_ALREADY_ACTIVE',
|
|
674
|
+
'Another SynchroClient owns the native runtime'
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
return NativeSynchro;
|
|
678
|
+
}
|
|
33
679
|
|
|
34
|
-
|
|
35
|
-
this.authSubscription
|
|
680
|
+
private installAuthSubscription(): void {
|
|
681
|
+
this.authSubscription?.remove();
|
|
682
|
+
const owner = this.nativeOwner;
|
|
683
|
+
this.authSubscription = NativeSynchro.onAuthRequest(
|
|
36
684
|
(event: { requestID: string }) => {
|
|
37
685
|
this.config
|
|
38
686
|
.authProvider()
|
|
39
687
|
.then((token) => {
|
|
40
|
-
|
|
688
|
+
if (activeNativeOwner === owner) {
|
|
689
|
+
NativeSynchro.resolveAuthRequest(event.requestID, token);
|
|
690
|
+
}
|
|
41
691
|
})
|
|
42
692
|
.catch((err) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
693
|
+
if (activeNativeOwner === owner) {
|
|
694
|
+
NativeSynchro.rejectAuthRequest(
|
|
695
|
+
event.requestID,
|
|
696
|
+
err instanceof Error ? err.message : String(err)
|
|
697
|
+
);
|
|
698
|
+
}
|
|
47
699
|
});
|
|
48
700
|
}
|
|
49
701
|
);
|
|
50
702
|
}
|
|
51
703
|
|
|
704
|
+
private releaseNativeOwnership(): void {
|
|
705
|
+
this.authSubscription?.remove();
|
|
706
|
+
this.authSubscription = null;
|
|
707
|
+
if (activeNativeOwner === this.nativeOwner) {
|
|
708
|
+
activeNativeOwner = null;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
52
712
|
async initialize(): Promise<void> {
|
|
713
|
+
if (activeNativeOwner !== null && activeNativeOwner !== this.nativeOwner) {
|
|
714
|
+
throw new SynchroError(
|
|
715
|
+
'CLIENT_ALREADY_ACTIVE',
|
|
716
|
+
'Another SynchroClient owns the native runtime'
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
activeNativeOwner = this.nativeOwner;
|
|
720
|
+
this.installAuthSubscription();
|
|
53
721
|
try {
|
|
54
722
|
await this.native.initialize({
|
|
55
723
|
dbPath: this.config.dbPath,
|
|
@@ -62,44 +730,56 @@ export class SynchroClient {
|
|
|
62
730
|
maxRetryAttempts: this.config.maxRetryAttempts ?? 5,
|
|
63
731
|
pullPageSize: this.config.pullPageSize ?? 100,
|
|
64
732
|
pushBatchSize: this.config.pushBatchSize ?? 100,
|
|
65
|
-
|
|
733
|
+
seedDatabasePath: this.config.seedDatabasePath,
|
|
734
|
+
transportObservationCapacity: inspectionCapacities.get(this) ?? 0,
|
|
735
|
+
requireNewDatabase: inspectionRequireNewDatabases.get(this) ?? false,
|
|
66
736
|
});
|
|
67
737
|
} catch (error) {
|
|
738
|
+
this.releaseNativeOwnership();
|
|
68
739
|
throw mapNativeError(error);
|
|
69
740
|
}
|
|
70
741
|
}
|
|
71
742
|
|
|
72
|
-
//
|
|
743
|
+
// Core SQL
|
|
73
744
|
|
|
74
|
-
async query(sql: string, params?:
|
|
745
|
+
async query(sql: string, params?: readonly SQLiteBindValue[]): Promise<Row[]> {
|
|
75
746
|
try {
|
|
76
|
-
|
|
77
|
-
sql,
|
|
78
|
-
JSON.stringify(params ?? [])
|
|
79
|
-
);
|
|
80
|
-
return JSON.parse(json) as Row[];
|
|
747
|
+
return [...(await this.native.query(sql, checkedParams(params ?? [])))] as Row[];
|
|
81
748
|
} catch (error) {
|
|
82
749
|
throw mapNativeError(error);
|
|
83
750
|
}
|
|
84
751
|
}
|
|
85
752
|
|
|
86
|
-
async queryOne(sql: string, params?:
|
|
753
|
+
async queryOne(sql: string, params?: readonly SQLiteBindValue[]): Promise<Row | null> {
|
|
87
754
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
755
|
+
return nullableRow(await this.native.queryOne(sql, checkedParams(params ?? [])));
|
|
756
|
+
} catch (error) {
|
|
757
|
+
throw mapNativeError(error);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async execute(sql: string, params?: readonly SQLiteBindValue[]): Promise<ExecResult> {
|
|
762
|
+
try {
|
|
763
|
+
return await this.native.execute(sql, checkedParams(params ?? []));
|
|
93
764
|
} catch (error) {
|
|
94
765
|
throw mapNativeError(error);
|
|
95
766
|
}
|
|
96
767
|
}
|
|
97
768
|
|
|
98
|
-
async
|
|
769
|
+
async executeAuthoredWrite(
|
|
770
|
+
tableName: string,
|
|
771
|
+
operation: string,
|
|
772
|
+
columnNames: readonly string[],
|
|
773
|
+
sql: string,
|
|
774
|
+
values?: readonly SQLiteBindValue[]
|
|
775
|
+
): Promise<ExecResult> {
|
|
99
776
|
try {
|
|
100
|
-
return await this.native.
|
|
777
|
+
return await this.native.executeAuthoredWrite(
|
|
778
|
+
tableName,
|
|
779
|
+
operation,
|
|
780
|
+
columnNames,
|
|
101
781
|
sql,
|
|
102
|
-
|
|
782
|
+
checkedParams(values ?? [])
|
|
103
783
|
);
|
|
104
784
|
} catch (error) {
|
|
105
785
|
throw mapNativeError(error);
|
|
@@ -110,15 +790,15 @@ export class SynchroClient {
|
|
|
110
790
|
try {
|
|
111
791
|
const payload = statements.map((s) => ({
|
|
112
792
|
sql: s.sql,
|
|
113
|
-
params: s.params ?? [],
|
|
793
|
+
params: checkedParams(s.params ?? []),
|
|
114
794
|
}));
|
|
115
|
-
return await this.native.executeBatch(
|
|
795
|
+
return await this.native.executeBatch(payload);
|
|
116
796
|
} catch (error) {
|
|
117
797
|
throw mapNativeError(error);
|
|
118
798
|
}
|
|
119
799
|
}
|
|
120
800
|
|
|
121
|
-
//
|
|
801
|
+
// Transactions
|
|
122
802
|
|
|
123
803
|
async writeTransaction<T>(
|
|
124
804
|
callback: (tx: Transaction) => Promise<T>
|
|
@@ -133,27 +813,15 @@ export class SynchroClient {
|
|
|
133
813
|
try {
|
|
134
814
|
const tx: Transaction = {
|
|
135
815
|
query: async (sql, params) => {
|
|
136
|
-
|
|
137
|
-
txID,
|
|
138
|
-
sql,
|
|
139
|
-
JSON.stringify(params ?? [])
|
|
140
|
-
);
|
|
141
|
-
return JSON.parse(json) as Row[];
|
|
816
|
+
return [...(await this.native.txQuery(txID, sql, checkedParams(params ?? [])))] as Row[];
|
|
142
817
|
},
|
|
143
818
|
queryOne: async (sql, params) => {
|
|
144
|
-
|
|
145
|
-
txID,
|
|
146
|
-
sql,
|
|
147
|
-
JSON.stringify(params ?? [])
|
|
819
|
+
return nullableRow(
|
|
820
|
+
await this.native.txQueryOne(txID, sql, checkedParams(params ?? []))
|
|
148
821
|
);
|
|
149
|
-
return json ? (JSON.parse(json) as Row) : null;
|
|
150
822
|
},
|
|
151
823
|
execute: async (sql, params) => {
|
|
152
|
-
return await this.native.txExecute(
|
|
153
|
-
txID,
|
|
154
|
-
sql,
|
|
155
|
-
JSON.stringify(params ?? [])
|
|
156
|
-
);
|
|
824
|
+
return await this.native.txExecute(txID, sql, checkedParams(params ?? []));
|
|
157
825
|
},
|
|
158
826
|
};
|
|
159
827
|
const result = await callback(tx);
|
|
@@ -170,7 +838,7 @@ export class SynchroClient {
|
|
|
170
838
|
}
|
|
171
839
|
|
|
172
840
|
async readTransaction<T>(
|
|
173
|
-
callback: (tx:
|
|
841
|
+
callback: (tx: ReadTransaction) => Promise<T>
|
|
174
842
|
): Promise<T> {
|
|
175
843
|
let txID: string;
|
|
176
844
|
try {
|
|
@@ -180,28 +848,13 @@ export class SynchroClient {
|
|
|
180
848
|
}
|
|
181
849
|
|
|
182
850
|
try {
|
|
183
|
-
const tx:
|
|
851
|
+
const tx: ReadTransaction = {
|
|
184
852
|
query: async (sql, params) => {
|
|
185
|
-
|
|
186
|
-
txID,
|
|
187
|
-
sql,
|
|
188
|
-
JSON.stringify(params ?? [])
|
|
189
|
-
);
|
|
190
|
-
return JSON.parse(json) as Row[];
|
|
853
|
+
return [...(await this.native.txQuery(txID, sql, checkedParams(params ?? [])))] as Row[];
|
|
191
854
|
},
|
|
192
855
|
queryOne: async (sql, params) => {
|
|
193
|
-
|
|
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 ?? [])
|
|
856
|
+
return nullableRow(
|
|
857
|
+
await this.native.txQueryOne(txID, sql, checkedParams(params ?? []))
|
|
205
858
|
);
|
|
206
859
|
},
|
|
207
860
|
};
|
|
@@ -218,7 +871,7 @@ export class SynchroClient {
|
|
|
218
871
|
}
|
|
219
872
|
}
|
|
220
873
|
|
|
221
|
-
//
|
|
874
|
+
// Schema, local-only
|
|
222
875
|
|
|
223
876
|
async createTable(
|
|
224
877
|
name: string,
|
|
@@ -256,9 +909,9 @@ export class SynchroClient {
|
|
|
256
909
|
}
|
|
257
910
|
}
|
|
258
911
|
|
|
259
|
-
//
|
|
912
|
+
// Observation
|
|
260
913
|
|
|
261
|
-
onChange(tables: string[], callback: () => void):
|
|
914
|
+
async onChange(tables: string[], callback: () => void): Promise<AsyncUnsubscribe> {
|
|
262
915
|
const observerID = nextObserverID();
|
|
263
916
|
|
|
264
917
|
const subscription = this.native.onChange(
|
|
@@ -269,80 +922,151 @@ export class SynchroClient {
|
|
|
269
922
|
}
|
|
270
923
|
);
|
|
271
924
|
|
|
272
|
-
|
|
925
|
+
try {
|
|
926
|
+
await this.native.addChangeObserver(observerID, tables);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
subscription.remove();
|
|
929
|
+
throw mapNativeError(error);
|
|
930
|
+
}
|
|
273
931
|
|
|
274
|
-
return () => {
|
|
932
|
+
return async () => {
|
|
275
933
|
subscription.remove();
|
|
276
|
-
|
|
934
|
+
try {
|
|
935
|
+
await NativeSynchro.removeObserver(observerID);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
throw mapNativeError(error);
|
|
938
|
+
}
|
|
277
939
|
};
|
|
278
940
|
}
|
|
279
941
|
|
|
280
|
-
watch(
|
|
942
|
+
async watch(
|
|
281
943
|
sql: string,
|
|
282
|
-
params:
|
|
944
|
+
params: readonly SQLiteBindValue[] | undefined,
|
|
283
945
|
tables: string[],
|
|
284
946
|
callback: (rows: Row[]) => void
|
|
285
|
-
):
|
|
947
|
+
): Promise<AsyncUnsubscribe> {
|
|
286
948
|
const observerID = nextObserverID();
|
|
287
949
|
|
|
288
950
|
const subscription = this.native.onQueryResult(
|
|
289
|
-
(event
|
|
951
|
+
(event) => {
|
|
290
952
|
if (event.observerID === observerID) {
|
|
291
|
-
callback(
|
|
953
|
+
callback([...(event.rows as readonly Row[])]);
|
|
292
954
|
}
|
|
293
955
|
}
|
|
294
956
|
);
|
|
295
957
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
958
|
+
try {
|
|
959
|
+
await this.native.addQueryObserver(
|
|
960
|
+
observerID,
|
|
961
|
+
sql,
|
|
962
|
+
checkedParams(params ?? []),
|
|
963
|
+
tables
|
|
964
|
+
);
|
|
965
|
+
} catch (error) {
|
|
966
|
+
subscription.remove();
|
|
967
|
+
throw mapNativeError(error);
|
|
968
|
+
}
|
|
302
969
|
|
|
303
|
-
return () => {
|
|
970
|
+
return async () => {
|
|
304
971
|
subscription.remove();
|
|
305
|
-
|
|
972
|
+
try {
|
|
973
|
+
await NativeSynchro.removeObserver(observerID);
|
|
974
|
+
} catch (error) {
|
|
975
|
+
throw mapNativeError(error);
|
|
976
|
+
}
|
|
306
977
|
};
|
|
307
978
|
}
|
|
308
979
|
|
|
309
|
-
//
|
|
980
|
+
// Lifecycle
|
|
310
981
|
|
|
311
|
-
async
|
|
982
|
+
async close(): Promise<void> {
|
|
983
|
+
if (activeNativeOwner !== null && activeNativeOwner !== this.nativeOwner) {
|
|
984
|
+
throw new SynchroError(
|
|
985
|
+
'CLIENT_ALREADY_ACTIVE',
|
|
986
|
+
'Another SynchroClient owns the native runtime'
|
|
987
|
+
);
|
|
988
|
+
}
|
|
312
989
|
try {
|
|
313
|
-
await
|
|
990
|
+
await NativeSynchro.close();
|
|
991
|
+
this.releaseNativeOwnership();
|
|
314
992
|
} catch (error) {
|
|
315
993
|
throw mapNativeError(error);
|
|
316
994
|
}
|
|
317
995
|
}
|
|
318
996
|
|
|
319
|
-
|
|
997
|
+
async getPath(): Promise<string> {
|
|
998
|
+
try {
|
|
999
|
+
return await this.native.getPath();
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
throw mapNativeError(error);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
320
1004
|
|
|
321
|
-
async
|
|
1005
|
+
async pendingChangeCount(): Promise<number> {
|
|
322
1006
|
try {
|
|
323
|
-
await this.native.
|
|
1007
|
+
return await this.native.pendingChangeCount();
|
|
324
1008
|
} catch (error) {
|
|
325
1009
|
throw mapNativeError(error);
|
|
326
1010
|
}
|
|
327
1011
|
}
|
|
328
1012
|
|
|
329
|
-
async
|
|
1013
|
+
async getSyncStatus(): Promise<SyncStatus> {
|
|
330
1014
|
try {
|
|
331
|
-
|
|
1015
|
+
const status = parseNativeDTO<NativeSyncStatus>(
|
|
1016
|
+
await this.native.getSyncStatus()
|
|
1017
|
+
);
|
|
1018
|
+
const parsed = parseStatus(status);
|
|
1019
|
+
if (parsed === null) {
|
|
1020
|
+
throw new InvalidResponseError('Native bridge returned an unknown sync status');
|
|
1021
|
+
}
|
|
1022
|
+
return parsed;
|
|
332
1023
|
} catch (error) {
|
|
333
1024
|
throw mapNativeError(error);
|
|
334
1025
|
}
|
|
335
1026
|
}
|
|
336
1027
|
|
|
337
|
-
async
|
|
1028
|
+
async inspectPendingMutations(): Promise<PendingMutationInspection[]> {
|
|
338
1029
|
try {
|
|
339
|
-
return
|
|
1030
|
+
return parseNativeArray(
|
|
1031
|
+
await this.native.inspectPendingMutations(),
|
|
1032
|
+
'pending mutation inspection'
|
|
1033
|
+
).map(parsePendingMutationInspection);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
throw mapNativeError(error);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
async inspectRetainedMutations(): Promise<PendingMutationInspection[]> {
|
|
1040
|
+
try {
|
|
1041
|
+
return parseNativeArray(
|
|
1042
|
+
await this.native.inspectRetainedMutations(),
|
|
1043
|
+
'retained mutation inspection'
|
|
1044
|
+
).map(parsePendingMutationInspection);
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
throw mapNativeError(error);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async inspectRejectedMutations(): Promise<RejectedMutationInspection[]> {
|
|
1051
|
+
try {
|
|
1052
|
+
return parseNativeArray(
|
|
1053
|
+
await this.native.inspectRejectedMutations(),
|
|
1054
|
+
'rejected mutation inspection'
|
|
1055
|
+
).map(parseRejectedMutationInspection);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
throw mapNativeError(error);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
async clearRejectedMutations(): Promise<void> {
|
|
1062
|
+
try {
|
|
1063
|
+
await this.native.clearRejectedMutations();
|
|
340
1064
|
} catch (error) {
|
|
341
1065
|
throw mapNativeError(error);
|
|
342
1066
|
}
|
|
343
1067
|
}
|
|
344
1068
|
|
|
345
|
-
//
|
|
1069
|
+
// Sync
|
|
346
1070
|
|
|
347
1071
|
async start(): Promise<void> {
|
|
348
1072
|
try {
|
|
@@ -360,6 +1084,38 @@ export class SynchroClient {
|
|
|
360
1084
|
}
|
|
361
1085
|
}
|
|
362
1086
|
|
|
1087
|
+
async enterBackground(): Promise<void> {
|
|
1088
|
+
try {
|
|
1089
|
+
await this.native.enterBackground();
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
throw mapNativeError(error);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
async enterForeground(): Promise<void> {
|
|
1096
|
+
try {
|
|
1097
|
+
await this.native.enterForeground();
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
throw mapNativeError(error);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async retryAfterError(): Promise<void> {
|
|
1104
|
+
try {
|
|
1105
|
+
await this.native.retryAfterError();
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
throw mapNativeError(error);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
async resetSchemaAndStart(): Promise<void> {
|
|
1112
|
+
try {
|
|
1113
|
+
await this.native.resetSchemaAndStart();
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
throw mapNativeError(error);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
363
1119
|
async syncNow(): Promise<void> {
|
|
364
1120
|
try {
|
|
365
1121
|
await this.native.syncNow();
|
|
@@ -368,17 +1124,21 @@ export class SynchroClient {
|
|
|
368
1124
|
}
|
|
369
1125
|
}
|
|
370
1126
|
|
|
371
|
-
//
|
|
1127
|
+
// Status and events
|
|
372
1128
|
|
|
373
1129
|
onStatusChange(callback: (status: SyncStatus) => void): Unsubscribe {
|
|
374
|
-
const subscription = this.native.onStatusChange(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1130
|
+
const subscription = this.native.onStatusChange((event) => {
|
|
1131
|
+
const status = parseStatus(event);
|
|
1132
|
+
if (status !== null) callback(status);
|
|
1133
|
+
});
|
|
1134
|
+
return () => subscription.remove();
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
onSyncEvent(callback: (event: SyncEvent) => void): Unsubscribe {
|
|
1138
|
+
const subscription = this.native.onSyncEvent((event) => {
|
|
1139
|
+
const parsed = parseSyncEvent(event);
|
|
1140
|
+
if (parsed !== null) callback(parsed);
|
|
1141
|
+
});
|
|
382
1142
|
return () => subscription.remove();
|
|
383
1143
|
}
|
|
384
1144
|
|
|
@@ -405,20 +1165,40 @@ export class SynchroClient {
|
|
|
405
1165
|
return () => subscription.remove();
|
|
406
1166
|
}
|
|
407
1167
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
return
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export function configureInspection(
|
|
1171
|
+
client: SynchroClient,
|
|
1172
|
+
transportObservationCapacity: number,
|
|
1173
|
+
requireNewDatabase: boolean
|
|
1174
|
+
): void {
|
|
1175
|
+
const owner = inspectionOwners.get(client);
|
|
1176
|
+
if (
|
|
1177
|
+
owner !== undefined &&
|
|
1178
|
+
activeNativeOwner === owner &&
|
|
1179
|
+
transportObservationCapacity === 0 &&
|
|
1180
|
+
!requireNewDatabase
|
|
1181
|
+
) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (
|
|
1185
|
+
owner === undefined ||
|
|
1186
|
+
activeNativeOwner === owner ||
|
|
1187
|
+
typeof requireNewDatabase !== 'boolean' ||
|
|
1188
|
+
!Number.isSafeInteger(transportObservationCapacity) ||
|
|
1189
|
+
transportObservationCapacity < 0 ||
|
|
1190
|
+
transportObservationCapacity > 512
|
|
1191
|
+
) {
|
|
1192
|
+
throw new InvalidResponseError('Inspection configuration is invalid');
|
|
1193
|
+
}
|
|
1194
|
+
inspectionCapacities.set(client, transportObservationCapacity);
|
|
1195
|
+
inspectionRequireNewDatabases.set(client, requireNewDatabase);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
export function nativeForInspection(client: SynchroClient): typeof NativeSynchro {
|
|
1199
|
+
const owner = inspectionOwners.get(client);
|
|
1200
|
+
if (owner === undefined || activeNativeOwner !== owner) {
|
|
1201
|
+
throw new SynchroError('NOT_CONNECTED', 'SynchroClient is not initialized');
|
|
423
1202
|
}
|
|
1203
|
+
return NativeSynchro;
|
|
424
1204
|
}
|