@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.
Files changed (64) 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 +1106 -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/example/src/timeouts.d.ts +2 -0
  34. package/lib/typescript/example/src/timeouts.d.ts.map +1 -0
  35. package/lib/typescript/src/NativeSynchro.d.ts +59 -17
  36. package/lib/typescript/src/NativeSynchro.d.ts.map +1 -1
  37. package/lib/typescript/src/SynchroClient.d.ts +27 -10
  38. package/lib/typescript/src/SynchroClient.d.ts.map +1 -1
  39. package/lib/typescript/src/digest.d.ts +3 -0
  40. package/lib/typescript/src/digest.d.ts.map +1 -0
  41. package/lib/typescript/src/errors.d.ts +37 -9
  42. package/lib/typescript/src/errors.d.ts.map +1 -1
  43. package/lib/typescript/src/hooks/useQuery.d.ts +2 -2
  44. package/lib/typescript/src/hooks/useQuery.d.ts.map +1 -1
  45. package/lib/typescript/src/hooks/useSyncStatus.d.ts.map +1 -1
  46. package/lib/typescript/src/index.d.ts +4 -3
  47. package/lib/typescript/src/index.d.ts.map +1 -1
  48. package/lib/typescript/src/inspection.d.ts +43 -0
  49. package/lib/typescript/src/inspection.d.ts.map +1 -0
  50. package/lib/typescript/src/sqliteValues.d.ts +8 -0
  51. package/lib/typescript/src/sqliteValues.d.ts.map +1 -0
  52. package/lib/typescript/src/types.d.ts +203 -11
  53. package/lib/typescript/src/types.d.ts.map +1 -1
  54. package/package.json +20 -8
  55. package/src/NativeSynchro.ts +93 -26
  56. package/src/SynchroClient.ts +900 -120
  57. package/src/digest.ts +93 -0
  58. package/src/errors.ts +273 -33
  59. package/src/hooks/useQuery.ts +59 -16
  60. package/src/hooks/useSyncStatus.ts +40 -7
  61. package/src/index.ts +44 -3
  62. package/src/inspection.ts +226 -0
  63. package/src/sqliteValues.ts +105 -0
  64. package/src/types.ts +318 -16
package/src/index.ts CHANGED
@@ -7,16 +7,48 @@ export type {
7
7
  Row,
8
8
  ExecResult,
9
9
  BatchResult,
10
+ SQLiteBytes,
11
+ SQLiteInt64,
12
+ SQLiteTaggedValue,
13
+ SQLiteBindValue,
10
14
  SQLStatement,
11
15
  ColumnDef,
12
16
  TableOptions,
17
+ ReadTransaction,
13
18
  Transaction,
14
- CheckpointMode,
15
19
  SyncStatus,
16
20
  SyncStatusType,
21
+ SyncOperationKind,
22
+ SyncRecoveryAction,
23
+ SyncFailureCode,
24
+ SyncFailure,
25
+ JSONValue,
26
+ MutationOperation,
27
+ LocalMutationStatus,
28
+ MutationStatus,
29
+ MutationRejectionCode,
30
+ SchemaRef,
31
+ AuthoredMutationField,
32
+ PendingMutationInspection,
33
+ RejectedMutationInspection,
34
+ SchemaAction,
35
+ SyncSchemaEvent,
36
+ SyncMutationEvent,
37
+ SyncRebuildEvent,
38
+ SyncEvent,
39
+ ProtocolErrorCode,
17
40
  ConflictEvent,
18
41
  SynchroConfig,
19
42
  Unsubscribe,
43
+ AsyncUnsubscribe,
44
+ } from './types';
45
+
46
+ export {
47
+ SYNC_STATUS_TYPES,
48
+ SYNC_OPERATION_KINDS,
49
+ SYNC_RECOVERY_ACTIONS,
50
+ SYNC_FAILURE_CODES,
51
+ PROTOCOL_ERROR_CODES,
20
52
  } from './types';
21
53
 
22
54
  export {
@@ -26,16 +58,25 @@ export {
26
58
  TableNotSyncedError,
27
59
  UpgradeRequiredError,
28
60
  SchemaMismatchError,
29
- SnapshotRequiredError,
30
61
  PushRejectedError,
31
62
  NetworkError,
32
63
  ServerError,
64
+ ProtocolError,
65
+ SyncBlockedError,
66
+ UnsupportedSchemaError,
67
+ InvalidStateTransitionError,
33
68
  DatabaseError,
34
69
  InvalidResponseError,
70
+ InvalidSeedError,
35
71
  AlreadyStartedError,
36
72
  NotStartedError,
37
73
  TransactionTimeoutError,
74
+ NATIVE_ERROR_CODES,
38
75
  mapNativeError,
39
76
  } from './errors';
40
77
 
41
- export type { PushResultItem } from './errors';
78
+ export type {
79
+ NativeErrorCode,
80
+ SchemaUnsupportedReason,
81
+ PushRejectedMutation,
82
+ } from './errors';
@@ -0,0 +1,226 @@
1
+ import {
2
+ configureInspection,
3
+ nativeForInspection,
4
+ parseClientStateInspection,
5
+ parseTransportObservationSnapshot,
6
+ SynchroClient,
7
+ } from './SynchroClient';
8
+ import { InvalidResponseError, mapNativeError } from './errors';
9
+ import type {
10
+ ClientStateInspection,
11
+ TransportObservationSnapshot,
12
+ TransportOperationClass,
13
+ } from './types';
14
+
15
+ const TRANSPORT_OPERATION_CLASSES: readonly TransportOperationClass[] = [
16
+ 'connect',
17
+ 'pull',
18
+ 'push',
19
+ 'checkpoint',
20
+ 'schemas',
21
+ 'rebuild',
22
+ 'other',
23
+ ];
24
+
25
+ export interface DurableStateInspection {
26
+ row_metadata: {
27
+ table_name: string;
28
+ record_id: string;
29
+ server_version: string;
30
+ row_checksum: string | null;
31
+ } | null;
32
+ rebuild_receipts: Array<{
33
+ rebuild_id_fingerprint: string;
34
+ page_count: number;
35
+ returned_record_count: number;
36
+ request_chain_expected: string[];
37
+ request_chain_observed: string[];
38
+ record_identities_hex: string[];
39
+ received_row_checksums: string[];
40
+ computed_row_checksums: string[];
41
+ computed_scope_checksum: string | null;
42
+ final_scope_checksum: string | null;
43
+ stored_scope_checksum: string | null;
44
+ local_scope_checksum: string | null;
45
+ }>;
46
+ }
47
+
48
+ export interface SynchroInspectionOptions {
49
+ transportObservationCapacity?: number;
50
+ requireNewDatabase?: boolean;
51
+ }
52
+
53
+ export class SynchroInspection {
54
+ constructor(
55
+ private readonly client: SynchroClient,
56
+ options: SynchroInspectionOptions = {}
57
+ ) {
58
+ configureInspection(
59
+ client,
60
+ options.transportObservationCapacity ?? 0,
61
+ options.requireNewDatabase ?? false
62
+ );
63
+ }
64
+
65
+ async clientState(): Promise<ClientStateInspection> {
66
+ try {
67
+ return parseClientStateInspection(parseJSON(await nativeForInspection(this.client).inspectClientState()));
68
+ } catch (error) {
69
+ throw mapNativeError(error);
70
+ }
71
+ }
72
+
73
+ async durableState(tableName: string, recordID: string): Promise<DurableStateInspection> {
74
+ if (tableName.length === 0 || recordID.length === 0) {
75
+ throw new InvalidResponseError('Durable-state identity is invalid');
76
+ }
77
+ try {
78
+ return parseDurableState(
79
+ parseJSON(await nativeForInspection(this.client).inspectDurableState(tableName, recordID))
80
+ );
81
+ } catch (error) {
82
+ throw mapNativeError(error);
83
+ }
84
+ }
85
+
86
+ async processIdentity(): Promise<string> {
87
+ try {
88
+ const value = await nativeForInspection(this.client).getProcessIdentity();
89
+ if (!/^(?:ios|android)-app:[1-9][0-9]*$/.test(value)) {
90
+ throw new InvalidResponseError('Native bridge returned invalid process identity');
91
+ }
92
+ return value;
93
+ } catch (error) {
94
+ throw mapNativeError(error);
95
+ }
96
+ }
97
+
98
+ async transportObservations(): Promise<TransportObservationSnapshot> {
99
+ try {
100
+ return parseTransportObservationSnapshot(
101
+ parseJSON(await nativeForInspection(this.client).inspectTransportObservations())
102
+ );
103
+ } catch (error) {
104
+ throw mapNativeError(error);
105
+ }
106
+ }
107
+
108
+ async armTransportPause(operationClass: TransportOperationClass): Promise<void> {
109
+ requireTransportOperationClass(operationClass);
110
+ try {
111
+ await nativeForInspection(this.client).armTransportPause(operationClass);
112
+ } catch (error) {
113
+ throw mapNativeError(error);
114
+ }
115
+ }
116
+
117
+ async awaitTransportPause(operationClass: TransportOperationClass, timeoutMs: number): Promise<void> {
118
+ requireTransportOperationClass(operationClass);
119
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000) {
120
+ throw new InvalidResponseError('Transport pause request is invalid');
121
+ }
122
+ try {
123
+ await nativeForInspection(this.client).awaitTransportPause(operationClass, timeoutMs);
124
+ } catch (error) {
125
+ throw mapNativeError(error);
126
+ }
127
+ }
128
+
129
+ async resumeTransportPause(): Promise<void> {
130
+ try {
131
+ await nativeForInspection(this.client).resumeTransportPause();
132
+ } catch (error) {
133
+ throw mapNativeError(error);
134
+ }
135
+ }
136
+ }
137
+
138
+ function parseJSON(source: string): unknown {
139
+ try {
140
+ return JSON.parse(source) as unknown;
141
+ } catch {
142
+ throw new InvalidResponseError('Native bridge returned malformed inspection JSON');
143
+ }
144
+ }
145
+
146
+ function parseDurableState(value: unknown): DurableStateInspection {
147
+ const proof = requireRecord(value, 'durable state');
148
+ if (
149
+ Object.keys(proof).length !== 2 ||
150
+ !Object.prototype.hasOwnProperty.call(proof, 'row_metadata') ||
151
+ !Array.isArray(proof.rebuild_receipts)
152
+ ) {
153
+ throw new InvalidResponseError('Native bridge returned invalid durable state');
154
+ }
155
+ if (proof.row_metadata !== null) {
156
+ const metadata = requireRecord(proof.row_metadata, 'row metadata');
157
+ if (
158
+ Object.keys(metadata).length !== 4 ||
159
+ typeof metadata.table_name !== 'string' ||
160
+ typeof metadata.record_id !== 'string' ||
161
+ typeof metadata.server_version !== 'string' ||
162
+ (metadata.row_checksum !== null && typeof metadata.row_checksum !== 'string')
163
+ ) {
164
+ throw new InvalidResponseError('Native bridge returned invalid row metadata');
165
+ }
166
+ }
167
+ for (const receiptValue of proof.rebuild_receipts) {
168
+ const receipt = requireRecord(receiptValue, 'rebuild receipt');
169
+ if (
170
+ Object.keys(receipt).length !== 12 ||
171
+ typeof receipt.rebuild_id_fingerprint !== 'string' ||
172
+ !isNonnegativeSafeInteger(receipt.page_count) ||
173
+ !isNonnegativeSafeInteger(receipt.returned_record_count) ||
174
+ !isStringArray(receipt.request_chain_expected) ||
175
+ !isStringArray(receipt.request_chain_observed) ||
176
+ !isStringArray(receipt.record_identities_hex) ||
177
+ !isStringArray(receipt.received_row_checksums) ||
178
+ !isStringArray(receipt.computed_row_checksums) ||
179
+ !isNullableString(receipt.computed_scope_checksum) ||
180
+ !isNullableString(receipt.final_scope_checksum) ||
181
+ !isNullableString(receipt.stored_scope_checksum) ||
182
+ !isNullableString(receipt.local_scope_checksum)
183
+ ) {
184
+ throw new InvalidResponseError('Native bridge returned invalid rebuild receipt');
185
+ }
186
+ }
187
+ return proof as unknown as DurableStateInspection;
188
+ }
189
+
190
+ function requireRecord(value: unknown, name: string): Record<string, unknown> {
191
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
192
+ throw new InvalidResponseError(`Native bridge returned invalid ${name}`);
193
+ }
194
+ return value as Record<string, unknown>;
195
+ }
196
+
197
+ function isNonnegativeSafeInteger(value: unknown): value is number {
198
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
199
+ }
200
+
201
+ function isStringArray(value: unknown): value is string[] {
202
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
203
+ }
204
+
205
+ function isNullableString(value: unknown): value is string | null {
206
+ return value === null || typeof value === 'string';
207
+ }
208
+
209
+ function requireTransportOperationClass(value: TransportOperationClass): void {
210
+ if (!TRANSPORT_OPERATION_CLASSES.includes(value)) {
211
+ throw new InvalidResponseError('Transport operation class is invalid');
212
+ }
213
+ }
214
+
215
+ export { sha256Hex } from './digest';
216
+ export { isCanonicalBase64Url, isCanonicalInt64 } from './sqliteValues';
217
+
218
+ export type {
219
+ ClientStateInspection,
220
+ RebuildAttemptInspection,
221
+ ScopeRowInspection,
222
+ ScopeStateInspection,
223
+ TransportObservation,
224
+ TransportObservationSnapshot,
225
+ TransportOperationClass,
226
+ } from './types';
@@ -0,0 +1,105 @@
1
+ import type { SQLiteBindValue } from './types';
2
+
3
+ const MIN_INT64 = -(BigInt(1) << BigInt(63));
4
+ const MAX_INT64 = (BigInt(1) << BigInt(63)) - BigInt(1);
5
+
6
+ const BASE64URL_ALPHABET =
7
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
8
+
9
+ function base64urlValue(character: string): number {
10
+ return BASE64URL_ALPHABET.indexOf(character);
11
+ }
12
+
13
+ /** Validate an unpadded, canonical RFC 4648 base64url string. */
14
+ export function isCanonicalBase64Url(value: string): boolean {
15
+ if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) {
16
+ return false;
17
+ }
18
+
19
+ const remainder = value.length % 4;
20
+ if (remainder === 2) {
21
+ const last = base64urlValue(value[value.length - 1]);
22
+ return (last & 0x0f) === 0;
23
+ }
24
+ if (remainder === 3) {
25
+ const last = base64urlValue(value[value.length - 1]);
26
+ return (last & 0x03) === 0;
27
+ }
28
+ return true;
29
+ }
30
+
31
+ export function isCanonicalInt64(value: string): boolean {
32
+ if (!/^(?:0|-?[1-9][0-9]*)$/.test(value)) {
33
+ return false;
34
+ }
35
+
36
+ try {
37
+ const parsed = BigInt(value);
38
+ return parsed >= MIN_INT64 && parsed <= MAX_INT64 && parsed.toString() === value;
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+
44
+ function invalidBindValue(index: number, reason: string): TypeError {
45
+ return new TypeError(`Invalid SQL bind value at index ${index}: ${reason}`);
46
+ }
47
+
48
+ /** Reject malformed tagged values before they cross the native bridge. */
49
+ export function assertValidSQLiteBindValue(
50
+ value: unknown,
51
+ index: number
52
+ ): asserts value is SQLiteBindValue {
53
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') {
54
+ return;
55
+ }
56
+
57
+ if (typeof value === 'number') {
58
+ if (!Number.isFinite(value)) {
59
+ throw invalidBindValue(index, 'number must be finite');
60
+ }
61
+ if (Number.isInteger(value) && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
62
+ throw invalidBindValue(index, 'integer is outside the safe range');
63
+ }
64
+ return;
65
+ }
66
+
67
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
68
+ throw invalidBindValue(index, 'unsupported value type');
69
+ }
70
+
71
+ const keys = Object.keys(value);
72
+ if (keys.length !== 2 || !keys.includes('type')) {
73
+ throw invalidBindValue(index, 'tag must contain exactly type and payload fields');
74
+ }
75
+
76
+ const tagged = value as Record<string, unknown>;
77
+ if (tagged.type === 'bytes') {
78
+ if (keys.length !== 2 || !keys.includes('base64') || typeof tagged.base64 !== 'string') {
79
+ throw invalidBindValue(index, 'bytes tag must contain a base64 string');
80
+ }
81
+ if (!isCanonicalBase64Url(tagged.base64)) {
82
+ throw invalidBindValue(index, 'bytes tag must use canonical base64url');
83
+ }
84
+ return;
85
+ }
86
+
87
+ if (tagged.type === 'int64') {
88
+ if (keys.length !== 2 || !keys.includes('value') || typeof tagged.value !== 'string') {
89
+ throw invalidBindValue(index, 'int64 tag must contain a signed decimal string');
90
+ }
91
+ if (!isCanonicalInt64(tagged.value)) {
92
+ throw invalidBindValue(index, 'int64 tag must contain a canonical signed int64');
93
+ }
94
+ return;
95
+ }
96
+
97
+ throw invalidBindValue(index, 'unknown SQL bind tag');
98
+ }
99
+
100
+ export function assertValidSQLiteBindParams(
101
+ values: readonly SQLiteBindValue[]
102
+ ): readonly SQLiteBindValue[] {
103
+ values.forEach((value, index) => assertValidSQLiteBindValue(value, index));
104
+ return values;
105
+ }