@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.
Files changed (62) hide show
  1. package/README.md +103 -24
  2. package/SynchroReactNative.podspec +3 -2
  3. package/android/build.gradle +8 -3
  4. package/android/src/main/kotlin/com/trainstar/synchro/rn/SynchroModule.kt +1105 -238
  5. package/ios/SynchroModule.mm +178 -51
  6. package/ios/SynchroModule.swift +1382 -295
  7. package/lib/module/NativeSynchro.js +5 -0
  8. package/lib/module/NativeSynchro.js.map +1 -1
  9. package/lib/module/SynchroClient.js +637 -65
  10. package/lib/module/SynchroClient.js.map +1 -1
  11. package/lib/module/digest.js +82 -0
  12. package/lib/module/digest.js.map +1 -0
  13. package/lib/module/errors.js +165 -22
  14. package/lib/module/errors.js.map +1 -1
  15. package/lib/module/hooks/useQuery.js +37 -3
  16. package/lib/module/hooks/useQuery.js.map +1 -1
  17. package/lib/module/hooks/useSyncStatus.js +51 -6
  18. package/lib/module/hooks/useSyncStatus.js.map +1 -1
  19. package/lib/module/index.js +2 -1
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/inspection.js +121 -0
  22. package/lib/module/inspection.js.map +1 -0
  23. package/lib/module/sqliteValues.js +87 -0
  24. package/lib/module/sqliteValues.js.map +1 -0
  25. package/lib/module/types.js +10 -0
  26. package/lib/module/types.js.map +1 -1
  27. package/lib/typescript/example/e2e/corpus-harness.d.ts +34 -0
  28. package/lib/typescript/example/e2e/corpus-harness.d.ts.map +1 -0
  29. package/lib/typescript/example/src/conformance/runner.d.ts +125 -0
  30. package/lib/typescript/example/src/conformance/runner.d.ts.map +1 -0
  31. package/lib/typescript/example/src/conformance/types.d.ts +71 -0
  32. package/lib/typescript/example/src/conformance/types.d.ts.map +1 -0
  33. package/lib/typescript/src/NativeSynchro.d.ts +59 -17
  34. package/lib/typescript/src/NativeSynchro.d.ts.map +1 -1
  35. package/lib/typescript/src/SynchroClient.d.ts +27 -10
  36. package/lib/typescript/src/SynchroClient.d.ts.map +1 -1
  37. package/lib/typescript/src/digest.d.ts +3 -0
  38. package/lib/typescript/src/digest.d.ts.map +1 -0
  39. package/lib/typescript/src/errors.d.ts +37 -9
  40. package/lib/typescript/src/errors.d.ts.map +1 -1
  41. package/lib/typescript/src/hooks/useQuery.d.ts +2 -2
  42. package/lib/typescript/src/hooks/useQuery.d.ts.map +1 -1
  43. package/lib/typescript/src/hooks/useSyncStatus.d.ts.map +1 -1
  44. package/lib/typescript/src/index.d.ts +4 -3
  45. package/lib/typescript/src/index.d.ts.map +1 -1
  46. package/lib/typescript/src/inspection.d.ts +43 -0
  47. package/lib/typescript/src/inspection.d.ts.map +1 -0
  48. package/lib/typescript/src/sqliteValues.d.ts +8 -0
  49. package/lib/typescript/src/sqliteValues.d.ts.map +1 -0
  50. package/lib/typescript/src/types.d.ts +203 -11
  51. package/lib/typescript/src/types.d.ts.map +1 -1
  52. package/package.json +20 -8
  53. package/src/NativeSynchro.ts +93 -26
  54. package/src/SynchroClient.ts +899 -120
  55. package/src/digest.ts +93 -0
  56. package/src/errors.ts +273 -33
  57. package/src/hooks/useQuery.ts +59 -16
  58. package/src/hooks/useSyncStatus.ts +40 -7
  59. package/src/index.ts +44 -3
  60. package/src/inspection.ts +226 -0
  61. package/src/sqliteValues.ts +105 -0
  62. package/src/types.ts +317 -16
@@ -2,27 +2,505 @@
2
2
 
3
3
  import { Platform } from 'react-native';
4
4
  import NativeSynchro from "./NativeSynchro.js";
5
- import { mapNativeError } from "./errors.js";
5
+ import { InvalidResponseError, SynchroError, mapNativeError, parseSyncFailure } from "./errors.js";
6
+ import { assertValidSQLiteBindParams } from "./sqliteValues.js";
7
+ import { SYNC_OPERATION_KINDS, SYNC_STATUS_TYPES } from "./types.js";
6
8
  let observerCounter = 0;
9
+ let activeNativeOwner = null;
10
+ const inspectionOwners = new WeakMap();
11
+ const inspectionCapacities = new WeakMap();
12
+ const inspectionRequireNewDatabases = new WeakMap();
7
13
  function nextObserverID() {
8
14
  return `obs_${++observerCounter}_${Date.now()}`;
9
15
  }
16
+ function nullableRow(row) {
17
+ return row == null ? null : row;
18
+ }
19
+ const SYNC_EVENT_TYPES = ['state_changed', 'backoff', 'schema_applying', 'schema_applied', 'mutation_accepted', 'mutation_rejected', 'rebuild_requested', 'rebuild_completed', 'failure'];
20
+ const SCHEMA_ACTIONS = ['none', 'replace', 'rebuild_local', 'unsupported'];
21
+ const MUTATION_STATUSES = ['applied', 'conflict', 'rejected_terminal'];
22
+ const MUTATION_REJECTION_CODES = ['version_conflict', 'row_already_exists', 'row_deleted', 'row_not_found', 'schema_incompatible', 'policy_rejected', 'validation_failed', 'table_not_synced'];
23
+ const MUTATION_OPERATIONS = ['insert', 'upsert', 'update', 'delete'];
24
+ const LOCAL_MUTATION_STATUSES = ['pending', 'sealed', 'superseded_before_send', 'cancelled_before_send', 'blocked_by_predecessor', 'server_rejected'];
25
+ const TRANSPORT_OPERATION_CLASSES = ['connect', 'pull', 'push', 'checkpoint', 'schemas', 'rebuild', 'other'];
26
+ const LEGAL_SYNC_STATUS_TRANSITIONS = {
27
+ uninitialized: ['local_ready', 'error', 'stopped'],
28
+ local_ready: ['connecting', 'error', 'stopped'],
29
+ connecting: ['schema_applying', 'ready', 'backoff', 'error', 'stopped'],
30
+ schema_applying: ['ready', 'rebuilding', 'error', 'stopped'],
31
+ ready: ['connecting', 'pushing', 'pulling', 'rebuilding', 'error', 'stopped'],
32
+ pushing: ['pushing', 'ready', 'pulling', 'connecting', 'backoff', 'error', 'stopped'],
33
+ pulling: ['pulling', 'ready', 'rebuilding', 'connecting', 'backoff', 'error', 'stopped'],
34
+ rebuilding: ['rebuilding', 'ready', 'connecting', 'backoff', 'error', 'stopped'],
35
+ backoff: ['connecting', 'pushing', 'pulling', 'rebuilding', 'error', 'stopped'],
36
+ error: ['local_ready', 'stopped'],
37
+ stopped: ['local_ready']
38
+ };
39
+ function isRecord(value) {
40
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
41
+ }
42
+ function requiredString(value, name) {
43
+ if (typeof value !== 'string') {
44
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
45
+ }
46
+ return value;
47
+ }
48
+ function nullableString(value, name) {
49
+ if (value == null) {
50
+ return null;
51
+ }
52
+ return requiredString(value, name);
53
+ }
54
+ function requiredSafeInteger(value, name) {
55
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
56
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
57
+ }
58
+ return value;
59
+ }
60
+ function requiredNonnegativeSafeInteger(value, name) {
61
+ const integer = requiredSafeInteger(value, name);
62
+ if (integer < 0) {
63
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
64
+ }
65
+ return integer;
66
+ }
67
+ function nullableSafeInteger(value, name) {
68
+ return value == null ? null : requiredSafeInteger(value, name);
69
+ }
70
+ function parseDate(value, name) {
71
+ const stringValue = nullableString(value, name);
72
+ if (stringValue === null) {
73
+ return null;
74
+ }
75
+ const date = new Date(stringValue);
76
+ if (Number.isNaN(date.getTime())) {
77
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
78
+ }
79
+ return date;
80
+ }
81
+ function parseStatus(value) {
82
+ if (!isRecord(value) || typeof value.status !== 'string') {
83
+ throw new InvalidResponseError('Native bridge returned an invalid sync status');
84
+ }
85
+ if (!SYNC_STATUS_TYPES.includes(value.status)) {
86
+ return null;
87
+ }
88
+ const status = value.status;
89
+ const retryAt = parseDate(value.retryAt, 'retryAt');
90
+ const operationValue = nullableString(value.operation, 'status operation');
91
+ const operation = operationValue === null ? null : SYNC_OPERATION_KINDS.includes(operationValue) ? operationValue : (() => {
92
+ throw new InvalidResponseError('Native bridge returned an invalid status operation');
93
+ })();
94
+ const failure = value.failure == null ? null : parseSyncFailure(value.failure);
95
+ if (status === 'backoff') {
96
+ if (retryAt === null || operation === null || failure !== null) {
97
+ throw new InvalidResponseError('Native bridge returned invalid backoff status details');
98
+ }
99
+ return {
100
+ status,
101
+ retryAt,
102
+ operation,
103
+ failure: null
104
+ };
105
+ }
106
+ if (status === 'error') {
107
+ if (retryAt !== null || operation !== null || failure === null) {
108
+ throw new InvalidResponseError('Native bridge returned invalid error status details');
109
+ }
110
+ return {
111
+ status,
112
+ retryAt: null,
113
+ operation: null,
114
+ failure
115
+ };
116
+ }
117
+ if (retryAt !== null || operation !== null || failure !== null) {
118
+ throw new InvalidResponseError('Native bridge returned invalid state detail fields');
119
+ }
120
+ return {
121
+ status,
122
+ retryAt: null,
123
+ operation: null,
124
+ failure: null
125
+ };
126
+ }
127
+ function parseSchemaRef(value, name) {
128
+ if (!isRecord(value) || typeof value.version !== 'number' || !Number.isSafeInteger(value.version)) {
129
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
130
+ }
131
+ return {
132
+ version: value.version,
133
+ hash: requiredString(value.hash, `${name} hash`)
134
+ };
135
+ }
136
+ function parseMutationStatus(value) {
137
+ if (!MUTATION_STATUSES.includes(value)) {
138
+ throw new InvalidResponseError('Native bridge returned an invalid mutation status');
139
+ }
140
+ return value;
141
+ }
142
+ function parseRejectionCode(value) {
143
+ if (value == null) {
144
+ return null;
145
+ }
146
+ if (!MUTATION_REJECTION_CODES.includes(value)) {
147
+ throw new InvalidResponseError('Native bridge returned an invalid mutation rejection code');
148
+ }
149
+ return value;
150
+ }
151
+ function parseSchemaAction(value) {
152
+ if (!SCHEMA_ACTIONS.includes(value)) {
153
+ throw new InvalidResponseError('Native bridge returned an invalid schema action');
154
+ }
155
+ return value;
156
+ }
157
+ function parseSyncEvent(value) {
158
+ if (typeof value.type !== 'string') {
159
+ throw new InvalidResponseError('Native bridge returned an invalid sync event');
160
+ }
161
+ if (!SYNC_EVENT_TYPES.includes(value.type)) {
162
+ return null;
163
+ }
164
+ switch (value.type) {
165
+ case 'state_changed':
166
+ {
167
+ if (typeof value.from !== 'string' || typeof value.to !== 'string') {
168
+ throw new InvalidResponseError('Native bridge returned an invalid lifecycle event');
169
+ }
170
+ if (!SYNC_STATUS_TYPES.includes(value.from) || !SYNC_STATUS_TYPES.includes(value.to)) {
171
+ return null;
172
+ }
173
+ const from = value.from;
174
+ const to = value.to;
175
+ if (!LEGAL_SYNC_STATUS_TRANSITIONS[from].includes(to)) {
176
+ throw new InvalidResponseError('Native bridge returned an invalid lifecycle event');
177
+ }
178
+ return {
179
+ type: 'state_changed',
180
+ from,
181
+ to
182
+ };
183
+ }
184
+ case 'backoff':
185
+ {
186
+ const attempt = value.attempt;
187
+ if (typeof attempt !== 'number' || !Number.isSafeInteger(attempt) || attempt < 0) {
188
+ throw new InvalidResponseError('Native bridge returned an invalid backoff attempt');
189
+ }
190
+ const retryAt = parseDate(value.retryAt, 'backoff retryAt');
191
+ if (retryAt === null) {
192
+ throw new InvalidResponseError('Native bridge returned a missing backoff retryAt');
193
+ }
194
+ return {
195
+ type: 'backoff',
196
+ operation: SYNC_OPERATION_KINDS.includes(value.operation) ? value.operation : (() => {
197
+ throw new InvalidResponseError('Native bridge returned an invalid backoff operation');
198
+ })(),
199
+ attempt,
200
+ retryAt
201
+ };
202
+ }
203
+ case 'schema_applying':
204
+ case 'schema_applied':
205
+ return {
206
+ type: value.type,
207
+ source: parseSchemaRef(value.source, 'schema source'),
208
+ target: parseSchemaRef(value.target, 'schema target'),
209
+ action: parseSchemaAction(value.action)
210
+ };
211
+ case 'mutation_accepted':
212
+ case 'mutation_rejected':
213
+ {
214
+ const mutationID = requiredString(value.mutationID, 'mutation ID');
215
+ return {
216
+ type: value.type,
217
+ mutationID,
218
+ tableID: requiredString(value.tableID, 'mutation table ID'),
219
+ status: parseMutationStatus(value.mutationStatus),
220
+ rejectionCode: parseRejectionCode(value.rejectionCode)
221
+ };
222
+ }
223
+ case 'rebuild_requested':
224
+ case 'rebuild_completed':
225
+ return {
226
+ type: value.type,
227
+ scopeID: requiredString(value.scopeID, 'rebuild scope ID'),
228
+ rebuildID: requiredString(value.rebuildID, 'rebuild ID')
229
+ };
230
+ case 'failure':
231
+ {
232
+ return {
233
+ type: 'failure',
234
+ failure: parseSyncFailure(value.failure)
235
+ };
236
+ }
237
+ }
238
+ }
239
+ function parseNativeDTO(json) {
240
+ try {
241
+ return JSON.parse(json);
242
+ } catch {
243
+ throw new InvalidResponseError('Native bridge returned malformed JSON');
244
+ }
245
+ }
246
+ function parseNativeArray(json, name) {
247
+ const value = parseNativeDTO(json);
248
+ if (!Array.isArray(value)) {
249
+ throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
250
+ }
251
+ return value;
252
+ }
253
+ function parsePendingMutationInspection(value, index) {
254
+ const name = `pending mutation at index ${index}`;
255
+ if (!isRecord(value)) {
256
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
257
+ }
258
+ if (!MUTATION_OPERATIONS.includes(value.operation)) {
259
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name} operation`);
260
+ }
261
+ if (!LOCAL_MUTATION_STATUSES.includes(value.status)) {
262
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name} status`);
263
+ }
264
+ if (!Array.isArray(value.authoredFields)) {
265
+ throw new InvalidResponseError(`Native bridge returned invalid ${name} fields`);
266
+ }
267
+ const authoredFields = value.authoredFields.map((field, fieldIndex) => {
268
+ if (!isRecord(field) || !Object.prototype.hasOwnProperty.call(field, 'value')) {
269
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name} field at index ${fieldIndex}`);
270
+ }
271
+ return {
272
+ fieldID: requiredString(field.fieldID, `${name} field ID`),
273
+ logicalType: requiredString(field.logicalType, `${name} field logical type`),
274
+ value: field.value
275
+ };
276
+ });
277
+ return {
278
+ mutationID: requiredString(value.mutationID, `${name} ID`),
279
+ localOrder: requiredSafeInteger(value.localOrder, `${name} local order`),
280
+ tableID: requiredString(value.tableID, `${name} table ID`),
281
+ tableName: requiredString(value.tableName, `${name} table name`),
282
+ recordID: requiredString(value.recordID, `${name} record ID`),
283
+ primaryKeyFieldID: requiredString(value.primaryKeyFieldID, `${name} primary-key field ID`),
284
+ primaryKeyLogicalType: requiredString(value.primaryKeyLogicalType, `${name} primary-key logical type`),
285
+ operation: value.operation,
286
+ authoredSchema: parseSchemaRef(value.authoredSchema, `${name} authored schema`),
287
+ baseVersion: nullableString(value.baseVersion, `${name} base version`),
288
+ clientVersion: requiredString(value.clientVersion, `${name} client version`),
289
+ status: value.status,
290
+ sourceKind: requiredString(value.sourceKind, `${name} source kind`),
291
+ dependsOnMutationID: nullableString(value.dependsOnMutationID, `${name} predecessor mutation ID`),
292
+ normalizedMutationID: nullableString(value.normalizedMutationID, `${name} normalized mutation ID`),
293
+ sealedBatchID: nullableString(value.sealedBatchID, `${name} sealed batch ID`),
294
+ sealedOrdinal: nullableSafeInteger(value.sealedOrdinal, `${name} sealed ordinal`),
295
+ authoredFields
296
+ };
297
+ }
298
+ function parseRejectedMutationInspection(value, index) {
299
+ const name = `rejected mutation at index ${index}`;
300
+ if (!isRecord(value)) {
301
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
302
+ }
303
+ const code = parseRejectionCode(value.code);
304
+ if (code === null) {
305
+ throw new InvalidResponseError(`Native bridge returned a missing ${name} code`);
306
+ }
307
+ return {
308
+ mutationID: requiredString(value.mutationID, `${name} ID`),
309
+ tableName: requiredString(value.tableName, `${name} table name`),
310
+ recordID: requiredString(value.recordID, `${name} record ID`),
311
+ status: parseMutationStatus(value.status),
312
+ code,
313
+ message: nullableString(value.message, `${name} message`),
314
+ serverRowJSON: nullableString(value.serverRowJSON, `${name} server row JSON`),
315
+ serverVersion: nullableString(value.serverVersion, `${name} server version`),
316
+ mutationJSON: requiredString(value.mutationJSON, `${name} mutation JSON`),
317
+ rejectionJSON: requiredString(value.rejectionJSON, `${name} rejection JSON`),
318
+ createdAt: requiredString(value.createdAt, `${name} creation time`),
319
+ updatedAt: requiredString(value.updatedAt, `${name} update time`)
320
+ };
321
+ }
322
+ function parseScopeStateInspection(value, index) {
323
+ const name = `scope state at index ${index}`;
324
+ if (!isRecord(value)) {
325
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
326
+ }
327
+ return {
328
+ scopeID: requiredString(value.scope_id, `${name} ID`),
329
+ cursor: nullableString(value.cursor, `${name} cursor`),
330
+ checksum: nullableString(value.checksum, `${name} checksum`),
331
+ localChecksum: requiredString(value.local_checksum, `${name} local checksum`),
332
+ generation: requiredSafeInteger(value.generation, `${name} generation`)
333
+ };
334
+ }
335
+ function parseScopeRowInspection(value, index) {
336
+ const name = `scope row at index ${index}`;
337
+ if (!isRecord(value)) {
338
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
339
+ }
340
+ return {
341
+ scopeID: requiredString(value.scope_id, `${name} scope ID`),
342
+ tableName: requiredString(value.table_name, `${name} table name`),
343
+ recordID: requiredString(value.record_id, `${name} record ID`),
344
+ checksum: requiredString(value.checksum, `${name} checksum`),
345
+ generation: requiredSafeInteger(value.generation, `${name} generation`)
346
+ };
347
+ }
348
+ function parseRebuildAttemptInspection(value, index) {
349
+ const name = `rebuild attempt at index ${index}`;
350
+ if (!isRecord(value)) {
351
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
352
+ }
353
+ return {
354
+ scopeID: requiredString(value.scope_id, `${name} scope ID`),
355
+ rebuildID: requiredString(value.rebuild_id, `${name} rebuild ID`),
356
+ clientGeneration: requiredSafeInteger(value.client_generation, `${name} client generation`),
357
+ schemaVersion: requiredSafeInteger(value.schema_version, `${name} schema version`),
358
+ schemaHash: requiredString(value.schema_hash, `${name} schema hash`),
359
+ generation: requiredSafeInteger(value.generation, `${name} generation`),
360
+ cursor: nullableString(value.cursor, `${name} cursor`),
361
+ pageLimit: requiredSafeInteger(value.page_limit, `${name} page limit`)
362
+ };
363
+ }
364
+ export function parseClientStateInspection(value) {
365
+ if (!isRecord(value)) {
366
+ throw new InvalidResponseError('Native bridge returned invalid client state inspection');
367
+ }
368
+ const schema = value.schema == null ? null : parseSchemaRef(value.schema, 'client schema');
369
+ if (!Array.isArray(value.scope_states) || !Array.isArray(value.scope_rows) || !Array.isArray(value.rebuild_attempts)) {
370
+ throw new InvalidResponseError('Native bridge returned incomplete client state inspection');
371
+ }
372
+ return {
373
+ schema,
374
+ scopeStates: value.scope_states.map(parseScopeStateInspection),
375
+ scopeRows: value.scope_rows.map(parseScopeRowInspection),
376
+ rebuildAttempts: value.rebuild_attempts.map(parseRebuildAttemptInspection),
377
+ applicationRowCount: requiredNonnegativeSafeInteger(value.application_row_count, 'application row count'),
378
+ mutationLedgerCount: requiredNonnegativeSafeInteger(value.mutation_ledger_count, 'mutation ledger count'),
379
+ mutationOutcomeCount: requiredNonnegativeSafeInteger(value.mutation_outcome_count, 'mutation outcome count'),
380
+ sealedBatchCount: requiredNonnegativeSafeInteger(value.sealed_batch_count, 'sealed batch count'),
381
+ rejectedMutationCount: requiredNonnegativeSafeInteger(value.rejected_mutation_count, 'rejected mutation count'),
382
+ scopeStateCount: requiredNonnegativeSafeInteger(value.scope_state_count, 'scope state count'),
383
+ scopeRowCount: requiredNonnegativeSafeInteger(value.scope_row_count, 'scope row count'),
384
+ provenanceCount: requiredNonnegativeSafeInteger(value.provenance_count, 'provenance count'),
385
+ rowMetadataCount: requiredNonnegativeSafeInteger(value.row_metadata_count, 'row metadata count'),
386
+ rebuildAttemptCount: requiredNonnegativeSafeInteger(value.rebuild_attempt_count, 'rebuild attempt count'),
387
+ rebuildReceiptCount: requiredNonnegativeSafeInteger(value.rebuild_receipt_count, 'rebuild receipt count'),
388
+ provenanceMaintenanceWorkCursor: requiredNonnegativeInt64String(value.provenance_maintenance_work_cursor, 'provenance maintenance work cursor')
389
+ };
390
+ }
391
+ function requiredNonnegativeInt64String(value, name) {
392
+ const maximum = '9223372036854775807';
393
+ if (typeof value !== 'string' || !/^(0|[1-9][0-9]*)$/.test(value) || value.length > maximum.length || value.length === maximum.length && value > maximum) {
394
+ throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
395
+ }
396
+ return value;
397
+ }
398
+ function optionalTransportFacts(value, name) {
399
+ if (value == null) {
400
+ return undefined;
401
+ }
402
+ if (!isRecord(value)) {
403
+ throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
404
+ }
405
+ return value;
406
+ }
407
+ function optionalTransportRequestFacts(value, name) {
408
+ const facts = optionalTransportFacts(value, name);
409
+ const mutationCount = facts?.mutation_count;
410
+ if (facts !== undefined && Object.prototype.hasOwnProperty.call(facts, 'mutation_count') && (typeof mutationCount !== 'number' || !Number.isSafeInteger(mutationCount) || mutationCount < 0)) {
411
+ throw new InvalidResponseError(`Native bridge returned invalid ${name} mutation count`);
412
+ }
413
+ return facts;
414
+ }
415
+ function parseTransportObservation(value, index) {
416
+ const name = `transport observation at index ${index}`;
417
+ if (!isRecord(value) || !TRANSPORT_OPERATION_CLASSES.includes(value.operation_class)) {
418
+ throw new InvalidResponseError(`Native bridge returned an invalid ${name}`);
419
+ }
420
+ const cursorFingerprints = value.cursor_fingerprints;
421
+ if (cursorFingerprints !== undefined && (!Array.isArray(cursorFingerprints) || !cursorFingerprints.every(item => typeof item === 'string'))) {
422
+ throw new InvalidResponseError(`Native bridge returned invalid ${name} cursor fingerprints`);
423
+ }
424
+ const cursorFingerprintsComplete = value.cursor_fingerprints_complete;
425
+ if (cursorFingerprintsComplete !== undefined && typeof cursorFingerprintsComplete !== 'boolean') {
426
+ throw new InvalidResponseError(`Native bridge returned invalid ${name} cursor completeness`);
427
+ }
428
+ return {
429
+ sequence: requiredSafeInteger(value.sequence, `${name} sequence`),
430
+ operationClass: value.operation_class,
431
+ statusCode: requiredSafeInteger(value.status_code, `${name} status code`),
432
+ durationNanoseconds: requiredSafeInteger(value.duration_nanoseconds, `${name} duration`),
433
+ ...(cursorFingerprints === undefined ? {} : {
434
+ cursorFingerprints: [...cursorFingerprints]
435
+ }),
436
+ ...(cursorFingerprintsComplete === undefined ? {} : {
437
+ cursorFingerprintsComplete
438
+ }),
439
+ ...(value.request_facts == null ? {} : {
440
+ requestFacts: optionalTransportRequestFacts(value.request_facts, `${name} request facts`)
441
+ }),
442
+ ...(value.rebuild_response_facts == null ? {} : {
443
+ rebuildResponseFacts: optionalTransportFacts(value.rebuild_response_facts, `${name} rebuild response facts`)
444
+ }),
445
+ ...(value.pull_response_facts == null ? {} : {
446
+ pullResponseFacts: optionalTransportFacts(value.pull_response_facts, `${name} pull response facts`)
447
+ })
448
+ };
449
+ }
450
+ export function parseTransportObservationSnapshot(value) {
451
+ if (!isRecord(value) || !Array.isArray(value.observations) || typeof value.overflowed !== 'boolean') {
452
+ throw new InvalidResponseError('Native bridge returned invalid transport observations');
453
+ }
454
+ return {
455
+ observations: value.observations.map(parseTransportObservation),
456
+ overflowed: value.overflowed,
457
+ sequenceCheckpoint: requiredSafeInteger(value.sequence_checkpoint, 'transport sequence checkpoint')
458
+ };
459
+ }
460
+ function checkedParams(params) {
461
+ return assertValidSQLiteBindParams(params);
462
+ }
10
463
  export class SynchroClient {
11
- native = NativeSynchro;
464
+ nativeOwner = {};
12
465
  authSubscription = null;
13
466
  constructor(config) {
14
467
  this.config = config;
15
-
16
- // Wire auth callback using Codegen EventEmitter pattern
17
- this.authSubscription = this.native.onAuthRequest(event => {
468
+ inspectionOwners.set(this, this.nativeOwner);
469
+ }
470
+ get native() {
471
+ if (activeNativeOwner !== null && activeNativeOwner !== this.nativeOwner) {
472
+ throw new SynchroError('CLIENT_ALREADY_ACTIVE', 'Another SynchroClient owns the native runtime');
473
+ }
474
+ return NativeSynchro;
475
+ }
476
+ installAuthSubscription() {
477
+ this.authSubscription?.remove();
478
+ const owner = this.nativeOwner;
479
+ this.authSubscription = NativeSynchro.onAuthRequest(event => {
18
480
  this.config.authProvider().then(token => {
19
- this.native.resolveAuthRequest(event.requestID, token);
481
+ if (activeNativeOwner === owner) {
482
+ NativeSynchro.resolveAuthRequest(event.requestID, token);
483
+ }
20
484
  }).catch(err => {
21
- this.native.rejectAuthRequest(event.requestID, err instanceof Error ? err.message : String(err));
485
+ if (activeNativeOwner === owner) {
486
+ NativeSynchro.rejectAuthRequest(event.requestID, err instanceof Error ? err.message : String(err));
487
+ }
22
488
  });
23
489
  });
24
490
  }
491
+ releaseNativeOwnership() {
492
+ this.authSubscription?.remove();
493
+ this.authSubscription = null;
494
+ if (activeNativeOwner === this.nativeOwner) {
495
+ activeNativeOwner = null;
496
+ }
497
+ }
25
498
  async initialize() {
499
+ if (activeNativeOwner !== null && activeNativeOwner !== this.nativeOwner) {
500
+ throw new SynchroError('CLIENT_ALREADY_ACTIVE', 'Another SynchroClient owns the native runtime');
501
+ }
502
+ activeNativeOwner = this.nativeOwner;
503
+ this.installAuthSubscription();
26
504
  try {
27
505
  await this.native.initialize({
28
506
  dbPath: this.config.dbPath,
@@ -35,34 +513,42 @@ export class SynchroClient {
35
513
  maxRetryAttempts: this.config.maxRetryAttempts ?? 5,
36
514
  pullPageSize: this.config.pullPageSize ?? 100,
37
515
  pushBatchSize: this.config.pushBatchSize ?? 100,
38
- snapshotPageSize: this.config.snapshotPageSize ?? 100
516
+ seedDatabasePath: this.config.seedDatabasePath,
517
+ transportObservationCapacity: inspectionCapacities.get(this) ?? 0,
518
+ requireNewDatabase: inspectionRequireNewDatabases.get(this) ?? false
39
519
  });
40
520
  } catch (error) {
521
+ this.releaseNativeOwnership();
41
522
  throw mapNativeError(error);
42
523
  }
43
524
  }
44
525
 
45
- // -- Core SQL --
526
+ // Core SQL
46
527
 
47
528
  async query(sql, params) {
48
529
  try {
49
- const json = await this.native.query(sql, JSON.stringify(params ?? []));
50
- return JSON.parse(json);
530
+ return [...(await this.native.query(sql, checkedParams(params ?? [])))];
51
531
  } catch (error) {
52
532
  throw mapNativeError(error);
53
533
  }
54
534
  }
55
535
  async queryOne(sql, params) {
56
536
  try {
57
- const json = await this.native.queryOne(sql, JSON.stringify(params ?? []));
58
- return json ? JSON.parse(json) : null;
537
+ return nullableRow(await this.native.queryOne(sql, checkedParams(params ?? [])));
59
538
  } catch (error) {
60
539
  throw mapNativeError(error);
61
540
  }
62
541
  }
63
542
  async execute(sql, params) {
64
543
  try {
65
- return await this.native.execute(sql, JSON.stringify(params ?? []));
544
+ return await this.native.execute(sql, checkedParams(params ?? []));
545
+ } catch (error) {
546
+ throw mapNativeError(error);
547
+ }
548
+ }
549
+ async executeAuthoredWrite(tableName, operation, columnNames, sql, values) {
550
+ try {
551
+ return await this.native.executeAuthoredWrite(tableName, operation, columnNames, sql, checkedParams(values ?? []));
66
552
  } catch (error) {
67
553
  throw mapNativeError(error);
68
554
  }
@@ -71,15 +557,15 @@ export class SynchroClient {
71
557
  try {
72
558
  const payload = statements.map(s => ({
73
559
  sql: s.sql,
74
- params: s.params ?? []
560
+ params: checkedParams(s.params ?? [])
75
561
  }));
76
- return await this.native.executeBatch(JSON.stringify(payload));
562
+ return await this.native.executeBatch(payload);
77
563
  } catch (error) {
78
564
  throw mapNativeError(error);
79
565
  }
80
566
  }
81
567
 
82
- // -- Transactions --
568
+ // Transactions
83
569
 
84
570
  async writeTransaction(callback) {
85
571
  let txID;
@@ -91,15 +577,13 @@ export class SynchroClient {
91
577
  try {
92
578
  const tx = {
93
579
  query: async (sql, params) => {
94
- const json = await this.native.txQuery(txID, sql, JSON.stringify(params ?? []));
95
- return JSON.parse(json);
580
+ return [...(await this.native.txQuery(txID, sql, checkedParams(params ?? [])))];
96
581
  },
97
582
  queryOne: async (sql, params) => {
98
- const json = await this.native.txQueryOne(txID, sql, JSON.stringify(params ?? []));
99
- return json ? JSON.parse(json) : null;
583
+ return nullableRow(await this.native.txQueryOne(txID, sql, checkedParams(params ?? [])));
100
584
  },
101
585
  execute: async (sql, params) => {
102
- return await this.native.txExecute(txID, sql, JSON.stringify(params ?? []));
586
+ return await this.native.txExecute(txID, sql, checkedParams(params ?? []));
103
587
  }
104
588
  };
105
589
  const result = await callback(tx);
@@ -124,15 +608,10 @@ export class SynchroClient {
124
608
  try {
125
609
  const tx = {
126
610
  query: async (sql, params) => {
127
- const json = await this.native.txQuery(txID, sql, JSON.stringify(params ?? []));
128
- return JSON.parse(json);
611
+ return [...(await this.native.txQuery(txID, sql, checkedParams(params ?? [])))];
129
612
  },
130
613
  queryOne: async (sql, params) => {
131
- const json = await this.native.txQueryOne(txID, sql, JSON.stringify(params ?? []));
132
- return json ? JSON.parse(json) : null;
133
- },
134
- execute: async (sql, params) => {
135
- return await this.native.txExecute(txID, sql, JSON.stringify(params ?? []));
614
+ return nullableRow(await this.native.txQueryOne(txID, sql, checkedParams(params ?? [])));
136
615
  }
137
616
  };
138
617
  const result = await callback(tx);
@@ -148,7 +627,7 @@ export class SynchroClient {
148
627
  }
149
628
  }
150
629
 
151
- // -- Schema (local-only) --
630
+ // Schema, local-only
152
631
 
153
632
  async createTable(name, columns, options) {
154
633
  try {
@@ -172,50 +651,62 @@ export class SynchroClient {
172
651
  }
173
652
  }
174
653
 
175
- // -- Observation --
654
+ // Observation
176
655
 
177
- onChange(tables, callback) {
656
+ async onChange(tables, callback) {
178
657
  const observerID = nextObserverID();
179
658
  const subscription = this.native.onChange(event => {
180
659
  if (event.observerID === observerID) {
181
660
  callback();
182
661
  }
183
662
  });
184
- this.native.addChangeObserver(observerID, tables);
185
- return () => {
663
+ try {
664
+ await this.native.addChangeObserver(observerID, tables);
665
+ } catch (error) {
666
+ subscription.remove();
667
+ throw mapNativeError(error);
668
+ }
669
+ return async () => {
186
670
  subscription.remove();
187
- this.native.removeObserver(observerID);
671
+ try {
672
+ await NativeSynchro.removeObserver(observerID);
673
+ } catch (error) {
674
+ throw mapNativeError(error);
675
+ }
188
676
  };
189
677
  }
190
- watch(sql, params, tables, callback) {
678
+ async watch(sql, params, tables, callback) {
191
679
  const observerID = nextObserverID();
192
680
  const subscription = this.native.onQueryResult(event => {
193
681
  if (event.observerID === observerID) {
194
- callback(JSON.parse(event.rowsJson));
682
+ callback([...event.rows]);
195
683
  }
196
684
  });
197
- this.native.addQueryObserver(observerID, sql, JSON.stringify(params ?? []), tables);
198
- return () => {
199
- subscription.remove();
200
- this.native.removeObserver(observerID);
201
- };
202
- }
203
-
204
- // -- WAL --
205
-
206
- async checkpoint(mode = 'passive') {
207
685
  try {
208
- await this.native.checkpoint(mode);
686
+ await this.native.addQueryObserver(observerID, sql, checkedParams(params ?? []), tables);
209
687
  } catch (error) {
688
+ subscription.remove();
210
689
  throw mapNativeError(error);
211
690
  }
691
+ return async () => {
692
+ subscription.remove();
693
+ try {
694
+ await NativeSynchro.removeObserver(observerID);
695
+ } catch (error) {
696
+ throw mapNativeError(error);
697
+ }
698
+ };
212
699
  }
213
700
 
214
- // -- Lifecycle --
701
+ // Lifecycle
215
702
 
216
703
  async close() {
704
+ if (activeNativeOwner !== null && activeNativeOwner !== this.nativeOwner) {
705
+ throw new SynchroError('CLIENT_ALREADY_ACTIVE', 'Another SynchroClient owns the native runtime');
706
+ }
217
707
  try {
218
- await this.native.close();
708
+ await NativeSynchro.close();
709
+ this.releaseNativeOwnership();
219
710
  } catch (error) {
220
711
  throw mapNativeError(error);
221
712
  }
@@ -234,8 +725,48 @@ export class SynchroClient {
234
725
  throw mapNativeError(error);
235
726
  }
236
727
  }
728
+ async getSyncStatus() {
729
+ try {
730
+ const status = parseNativeDTO(await this.native.getSyncStatus());
731
+ const parsed = parseStatus(status);
732
+ if (parsed === null) {
733
+ throw new InvalidResponseError('Native bridge returned an unknown sync status');
734
+ }
735
+ return parsed;
736
+ } catch (error) {
737
+ throw mapNativeError(error);
738
+ }
739
+ }
740
+ async inspectPendingMutations() {
741
+ try {
742
+ return parseNativeArray(await this.native.inspectPendingMutations(), 'pending mutation inspection').map(parsePendingMutationInspection);
743
+ } catch (error) {
744
+ throw mapNativeError(error);
745
+ }
746
+ }
747
+ async inspectRetainedMutations() {
748
+ try {
749
+ return parseNativeArray(await this.native.inspectRetainedMutations(), 'retained mutation inspection').map(parsePendingMutationInspection);
750
+ } catch (error) {
751
+ throw mapNativeError(error);
752
+ }
753
+ }
754
+ async inspectRejectedMutations() {
755
+ try {
756
+ return parseNativeArray(await this.native.inspectRejectedMutations(), 'rejected mutation inspection').map(parseRejectedMutationInspection);
757
+ } catch (error) {
758
+ throw mapNativeError(error);
759
+ }
760
+ }
761
+ async clearRejectedMutations() {
762
+ try {
763
+ await this.native.clearRejectedMutations();
764
+ } catch (error) {
765
+ throw mapNativeError(error);
766
+ }
767
+ }
237
768
 
238
- // -- Sync --
769
+ // Sync
239
770
 
240
771
  async start() {
241
772
  try {
@@ -251,6 +782,34 @@ export class SynchroClient {
251
782
  throw mapNativeError(error);
252
783
  }
253
784
  }
785
+ async enterBackground() {
786
+ try {
787
+ await this.native.enterBackground();
788
+ } catch (error) {
789
+ throw mapNativeError(error);
790
+ }
791
+ }
792
+ async enterForeground() {
793
+ try {
794
+ await this.native.enterForeground();
795
+ } catch (error) {
796
+ throw mapNativeError(error);
797
+ }
798
+ }
799
+ async retryAfterError() {
800
+ try {
801
+ await this.native.retryAfterError();
802
+ } catch (error) {
803
+ throw mapNativeError(error);
804
+ }
805
+ }
806
+ async resetSchemaAndStart() {
807
+ try {
808
+ await this.native.resetSchemaAndStart();
809
+ } catch (error) {
810
+ throw mapNativeError(error);
811
+ }
812
+ }
254
813
  async syncNow() {
255
814
  try {
256
815
  await this.native.syncNow();
@@ -259,14 +818,19 @@ export class SynchroClient {
259
818
  }
260
819
  }
261
820
 
262
- // -- Status & Events --
821
+ // Status and events
263
822
 
264
823
  onStatusChange(callback) {
265
824
  const subscription = this.native.onStatusChange(event => {
266
- callback({
267
- status: event.status,
268
- retryAt: event.retryAt ? new Date(event.retryAt) : null
269
- });
825
+ const status = parseStatus(event);
826
+ if (status !== null) callback(status);
827
+ });
828
+ return () => subscription.remove();
829
+ }
830
+ onSyncEvent(callback) {
831
+ const subscription = this.native.onSyncEvent(event => {
832
+ const parsed = parseSyncEvent(event);
833
+ if (parsed !== null) callback(parsed);
270
834
  });
271
835
  return () => subscription.remove();
272
836
  }
@@ -281,15 +845,23 @@ export class SynchroClient {
281
845
  });
282
846
  return () => subscription.remove();
283
847
  }
284
- onSnapshotRequired(handler) {
285
- const subscription = this.native.onSnapshotRequired(event => {
286
- handler().then(approved => {
287
- this.native.resolveSnapshotRequest(event.requestID, approved);
288
- }).catch(() => {
289
- this.native.resolveSnapshotRequest(event.requestID, false);
290
- });
291
- });
292
- return () => subscription.remove();
848
+ }
849
+ export function configureInspection(client, transportObservationCapacity, requireNewDatabase) {
850
+ const owner = inspectionOwners.get(client);
851
+ if (owner !== undefined && activeNativeOwner === owner && transportObservationCapacity === 0 && !requireNewDatabase) {
852
+ return;
853
+ }
854
+ if (owner === undefined || activeNativeOwner === owner || typeof requireNewDatabase !== 'boolean' || !Number.isSafeInteger(transportObservationCapacity) || transportObservationCapacity < 0 || transportObservationCapacity > 512) {
855
+ throw new InvalidResponseError('Inspection configuration is invalid');
856
+ }
857
+ inspectionCapacities.set(client, transportObservationCapacity);
858
+ inspectionRequireNewDatabases.set(client, requireNewDatabase);
859
+ }
860
+ export function nativeForInspection(client) {
861
+ const owner = inspectionOwners.get(client);
862
+ if (owner === undefined || activeNativeOwner !== owner) {
863
+ throw new SynchroError('NOT_CONNECTED', 'SynchroClient is not initialized');
293
864
  }
865
+ return NativeSynchro;
294
866
  }
295
867
  //# sourceMappingURL=SynchroClient.js.map