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