@tursodatabase/sync-react-native 0.5.0-pre.4

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 (72) hide show
  1. package/README.md +117 -0
  2. package/android/CMakeLists.txt +53 -0
  3. package/android/build.gradle +84 -0
  4. package/android/cpp-adapter.cpp +49 -0
  5. package/android/src/main/AndroidManifest.xml +2 -0
  6. package/android/src/main/java/com/turso/sync/reactnative/TursoBridge.java +44 -0
  7. package/android/src/main/java/com/turso/sync/reactnative/TursoModule.java +82 -0
  8. package/android/src/main/java/com/turso/sync/reactnative/TursoPackage.java +29 -0
  9. package/cpp/TursoConnectionHostObject.cpp +179 -0
  10. package/cpp/TursoConnectionHostObject.h +52 -0
  11. package/cpp/TursoDatabaseHostObject.cpp +98 -0
  12. package/cpp/TursoDatabaseHostObject.h +49 -0
  13. package/cpp/TursoHostObject.cpp +561 -0
  14. package/cpp/TursoHostObject.h +24 -0
  15. package/cpp/TursoStatementHostObject.cpp +414 -0
  16. package/cpp/TursoStatementHostObject.h +65 -0
  17. package/cpp/TursoSyncChangesHostObject.cpp +41 -0
  18. package/cpp/TursoSyncChangesHostObject.h +52 -0
  19. package/cpp/TursoSyncDatabaseHostObject.cpp +328 -0
  20. package/cpp/TursoSyncDatabaseHostObject.h +61 -0
  21. package/cpp/TursoSyncIoItemHostObject.cpp +304 -0
  22. package/cpp/TursoSyncIoItemHostObject.h +52 -0
  23. package/cpp/TursoSyncOperationHostObject.cpp +168 -0
  24. package/cpp/TursoSyncOperationHostObject.h +53 -0
  25. package/ios/TursoModule.h +8 -0
  26. package/ios/TursoModule.mm +95 -0
  27. package/lib/commonjs/Database.js +445 -0
  28. package/lib/commonjs/Database.js.map +1 -0
  29. package/lib/commonjs/Statement.js +339 -0
  30. package/lib/commonjs/Statement.js.map +1 -0
  31. package/lib/commonjs/index.js +229 -0
  32. package/lib/commonjs/index.js.map +1 -0
  33. package/lib/commonjs/internal/asyncOperation.js +124 -0
  34. package/lib/commonjs/internal/asyncOperation.js.map +1 -0
  35. package/lib/commonjs/internal/ioProcessor.js +315 -0
  36. package/lib/commonjs/internal/ioProcessor.js.map +1 -0
  37. package/lib/commonjs/package.json +1 -0
  38. package/lib/commonjs/types.js +133 -0
  39. package/lib/commonjs/types.js.map +1 -0
  40. package/lib/module/Database.js +441 -0
  41. package/lib/module/Database.js.map +1 -0
  42. package/lib/module/Statement.js +335 -0
  43. package/lib/module/Statement.js.map +1 -0
  44. package/lib/module/index.js +205 -0
  45. package/lib/module/index.js.map +1 -0
  46. package/lib/module/internal/asyncOperation.js +116 -0
  47. package/lib/module/internal/asyncOperation.js.map +1 -0
  48. package/lib/module/internal/ioProcessor.js +309 -0
  49. package/lib/module/internal/ioProcessor.js.map +1 -0
  50. package/lib/module/package.json +1 -0
  51. package/lib/module/types.js +163 -0
  52. package/lib/module/types.js.map +1 -0
  53. package/lib/typescript/Database.d.ts +140 -0
  54. package/lib/typescript/Database.d.ts.map +1 -0
  55. package/lib/typescript/Statement.d.ts +105 -0
  56. package/lib/typescript/Statement.d.ts.map +1 -0
  57. package/lib/typescript/index.d.ts +175 -0
  58. package/lib/typescript/index.d.ts.map +1 -0
  59. package/lib/typescript/internal/asyncOperation.d.ts +39 -0
  60. package/lib/typescript/internal/asyncOperation.d.ts.map +1 -0
  61. package/lib/typescript/internal/ioProcessor.d.ts +48 -0
  62. package/lib/typescript/internal/ioProcessor.d.ts.map +1 -0
  63. package/lib/typescript/types.d.ts +316 -0
  64. package/lib/typescript/types.d.ts.map +1 -0
  65. package/package.json +97 -0
  66. package/src/Database.ts +480 -0
  67. package/src/Statement.ts +372 -0
  68. package/src/index.ts +240 -0
  69. package/src/internal/asyncOperation.ts +147 -0
  70. package/src/internal/ioProcessor.ts +328 -0
  71. package/src/types.ts +391 -0
  72. package/turso-sync-react-native.podspec +56 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Async Operation Driver
3
+ *
4
+ * Drives async operations returned by sync SDK-KIT methods.
5
+ * This is where ALL the async logic lives - the C++ layer is just a thin bridge.
6
+ *
7
+ * Key responsibilities:
8
+ * - Call resume() in a loop until DONE
9
+ * - When IO is needed, process all pending IO items
10
+ * - Extract and return the final result
11
+ */
12
+
13
+ import type {
14
+ NativeSyncOperation,
15
+ NativeSyncDatabase,
16
+ NativeConnection,
17
+ NativeSyncChanges,
18
+ SyncStats,
19
+ } from '../types';
20
+ import { TursoStatus, SyncOperationResultType } from '../types';
21
+ import { processIoItem } from './ioProcessor';
22
+ import type { IoContext } from './ioProcessor';
23
+
24
+ /**
25
+ * Drive an async operation to completion
26
+ *
27
+ * @param operation - The native operation to drive
28
+ * @param database - The native sync database (for IO queue access)
29
+ * @param context - IO context with auth and URL information
30
+ * @returns Promise that resolves when operation completes
31
+ */
32
+ export async function driveOperation<T = void>(
33
+ operation: NativeSyncOperation,
34
+ database: NativeSyncDatabase,
35
+ context: IoContext
36
+ ): Promise<T> {
37
+ while (true) {
38
+ // Resume the operation
39
+ const status = operation.resume();
40
+
41
+ // Operation completed successfully
42
+ if (status === TursoStatus.DONE) {
43
+ // Extract and return the result based on result type
44
+ const resultKind = operation.resultKind();
45
+
46
+ switch (resultKind) {
47
+ case SyncOperationResultType.NONE:
48
+ return undefined as T;
49
+
50
+ case SyncOperationResultType.CONNECTION:
51
+ return operation.extractConnection() as T;
52
+
53
+ case SyncOperationResultType.CHANGES:
54
+ return operation.extractChanges() as T;
55
+
56
+ case SyncOperationResultType.STATS:
57
+ return operation.extractStats() as T;
58
+
59
+ default:
60
+ throw new Error(`Unknown result type: ${resultKind}`);
61
+ }
62
+ }
63
+
64
+ // Operation needs IO
65
+ if (status === TursoStatus.IO) {
66
+ // Process all pending IO items
67
+ await processIoQueue(database, context);
68
+
69
+ // Step callbacks after IO processing
70
+ database.ioStepCallbacks();
71
+
72
+ // Continue resume loop
73
+ continue;
74
+ }
75
+
76
+ // Any other status is an error
77
+ throw new Error(`Unexpected status from operation.resume(): ${status}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Process all pending IO items in the queue
83
+ *
84
+ * @param database - The native sync database
85
+ * @param context - IO context with auth and URL information
86
+ */
87
+ async function processIoQueue(database: NativeSyncDatabase, context: IoContext): Promise<void> {
88
+ const promises: Promise<void>[] = [];
89
+
90
+ // Take all available IO items from the queue
91
+ while (true) {
92
+ const ioItem = database.ioTakeItem();
93
+ if (!ioItem) {
94
+ break; // No more items
95
+ }
96
+
97
+ // Process each item (potentially in parallel)
98
+ promises.push(processIoItem(ioItem, context));
99
+ }
100
+
101
+ // Wait for all IO operations to complete
102
+ await Promise.all(promises);
103
+ }
104
+
105
+ /**
106
+ * Helper type for operations that return connections
107
+ */
108
+ export async function driveConnectionOperation(
109
+ operation: NativeSyncOperation,
110
+ database: NativeSyncDatabase,
111
+ context: IoContext
112
+ ): Promise<NativeConnection> {
113
+ return driveOperation<NativeConnection>(operation, database, context);
114
+ }
115
+
116
+ /**
117
+ * Helper type for operations that return changes
118
+ */
119
+ export async function driveChangesOperation(
120
+ operation: NativeSyncOperation,
121
+ database: NativeSyncDatabase,
122
+ context: IoContext
123
+ ): Promise<NativeSyncChanges | null> {
124
+ return driveOperation<NativeSyncChanges | null>(operation, database, context);
125
+ }
126
+
127
+ /**
128
+ * Helper type for operations that return stats
129
+ */
130
+ export async function driveStatsOperation(
131
+ operation: NativeSyncOperation,
132
+ database: NativeSyncDatabase,
133
+ context: IoContext
134
+ ): Promise<SyncStats> {
135
+ return driveOperation<SyncStats>(operation, database, context);
136
+ }
137
+
138
+ /**
139
+ * Helper type for operations that return void
140
+ */
141
+ export async function driveVoidOperation(
142
+ operation: NativeSyncOperation,
143
+ database: NativeSyncDatabase,
144
+ context: IoContext
145
+ ): Promise<void> {
146
+ return driveOperation<void>(operation, database, context);
147
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * IO Processor
3
+ *
4
+ * Processes IO requests from the sync engine using JavaScript APIs.
5
+ * This is the key benefit of the thin JSI layer - all IO is handled by
6
+ * React Native's standard APIs (fetch, file system), not C++ code.
7
+ *
8
+ * Benefits:
9
+ * - Network requests visible in React Native debugger
10
+ * - Can customize fetch (add proxies, custom headers, etc.)
11
+ * - Easier to test (can mock fetch)
12
+ * - Uses platform-native networking (not C++ HTTP libraries)
13
+ */
14
+
15
+ import type { NativeSyncIoItem, NativeSyncDatabase } from '../types';
16
+
17
+ /**
18
+ * IO context contains auth and URL information for HTTP requests
19
+ */
20
+ export interface IoContext {
21
+ /** Auth token for HTTP requests */
22
+ authToken?: string | (() => string | Promise<string> | null);
23
+ /** Base URL for normalization (e.g., 'libsql://mydb.turso.io') */
24
+ baseUrl?: string;
25
+ }
26
+
27
+ // Allow users to optionally override file system implementation
28
+ let fsReadFileOverride: ((path: string) => Promise<Uint8Array>) | null = null;
29
+ let fsWriteFileOverride: ((path: string, data: Uint8Array) => Promise<void>) | null = null;
30
+
31
+ /**
32
+ * Set custom file system implementation (optional)
33
+ * By default, uses built-in JSI file system functions.
34
+ * Only call this if you need custom behavior (e.g., encryption, compression).
35
+ *
36
+ * @param readFile - Function to read file as Uint8Array
37
+ * @param writeFile - Function to write Uint8Array to file
38
+ */
39
+ export function setFileSystemImpl(
40
+ readFile: (path: string) => Promise<Uint8Array>,
41
+ writeFile: (path: string, data: Uint8Array) => Promise<void>
42
+ ): void {
43
+ fsReadFileOverride = readFile;
44
+ fsWriteFileOverride = writeFile;
45
+ }
46
+
47
+ /**
48
+ * Read file using custom implementation or built-in JSI function
49
+ */
50
+ async function fsReadFile(path: string): Promise<Uint8Array> {
51
+ if (fsReadFileOverride) {
52
+ return fsReadFileOverride(path);
53
+ }
54
+
55
+ // Use built-in JSI function
56
+ const buffer = __TursoProxy.fsReadFile(path);
57
+ if (buffer === null) {
58
+ // File not found - return empty
59
+ return new Uint8Array(0);
60
+ }
61
+ return new Uint8Array(buffer);
62
+ }
63
+
64
+ /**
65
+ * Write file using custom implementation or built-in JSI function
66
+ */
67
+ async function fsWriteFile(path: string, data: Uint8Array): Promise<void> {
68
+ if (fsWriteFileOverride) {
69
+ return fsWriteFileOverride(path, data);
70
+ }
71
+
72
+ // Use built-in JSI function
73
+ __TursoProxy.fsWriteFile(path, data.buffer as ArrayBuffer);
74
+ }
75
+
76
+ /**
77
+ * Process a single IO item
78
+ *
79
+ * @param item - The IO item to process
80
+ * @param context - IO context with auth and URL information
81
+ */
82
+ export async function processIoItem(item: NativeSyncIoItem, context: IoContext): Promise<void> {
83
+ try {
84
+ const kind = item.getKind();
85
+
86
+ switch (kind) {
87
+ case 'HTTP':
88
+ await processHttpRequest(item, context);
89
+ break;
90
+
91
+ case 'FULL_READ':
92
+ await processFullRead(item);
93
+ break;
94
+
95
+ case 'FULL_WRITE':
96
+ await processFullWrite(item);
97
+ break;
98
+
99
+ case 'NONE':
100
+ default:
101
+ // Nothing to do
102
+ item.done();
103
+ break;
104
+ }
105
+ } catch (error) {
106
+ // Poison the item with error message
107
+ const errorMsg = error instanceof Error ? error.message : String(error);
108
+ item.poison(errorMsg);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Normalize URL from libsql:// to https://
114
+ */
115
+ function normalizeUrl(url: string): string {
116
+ if (url.startsWith('libsql://')) {
117
+ return url.replace('libsql://', 'https://');
118
+ }
119
+ return url;
120
+ }
121
+
122
+ /**
123
+ * Get auth token from context (handles both string and function)
124
+ */
125
+ function getAuthToken(context: IoContext): string | null {
126
+ if (!context.authToken) {
127
+ return null;
128
+ }
129
+
130
+ if (typeof context.authToken === 'function') {
131
+ return context.authToken() as unknown as string | null;
132
+ }
133
+
134
+ return context.authToken;
135
+ }
136
+
137
+ /**
138
+ * Process an HTTP request using fetch()
139
+ *
140
+ * @param item - The IO item
141
+ * @param context - IO context with auth and URL information
142
+ */
143
+ async function processHttpRequest(item: NativeSyncIoItem, context: IoContext): Promise<void> {
144
+ const request = item.getHttpRequest();
145
+
146
+ // Build full URL
147
+ let fullUrl = '';
148
+
149
+ if (request.url) {
150
+ // Normalize URL (libsql:// -> https://)
151
+ let baseUrl = normalizeUrl(request.url);
152
+
153
+ // Combine base URL with path if path is provided
154
+ if (request.path) {
155
+ // Ensure proper URL formatting (avoid double slashes, ensure single slash)
156
+ if (baseUrl.endsWith('/') && request.path.startsWith('/')) {
157
+ fullUrl = baseUrl + request.path.substring(1);
158
+ } else if (!baseUrl.endsWith('/') && !request.path.startsWith('/')) {
159
+ fullUrl = baseUrl + '/' + request.path;
160
+ } else {
161
+ fullUrl = baseUrl + request.path;
162
+ }
163
+ } else {
164
+ fullUrl = baseUrl;
165
+ }
166
+ } else if (request.path) {
167
+ // Path without base URL - shouldn't happen
168
+ throw new Error('HTTP request missing base URL');
169
+ } else {
170
+ throw new Error('HTTP request missing URL and path');
171
+ }
172
+
173
+ // Build fetch options
174
+ const options: RequestInit = {
175
+ method: request.method,
176
+ headers: { ...request.headers },
177
+ };
178
+
179
+ // Inject Authorization header if auth token is available
180
+ const authToken = getAuthToken(context);
181
+ if (authToken) {
182
+ (options.headers as Record<string, string>)['Authorization'] = `Bearer ${authToken}`;
183
+ }
184
+
185
+ // Add body if present
186
+ if (request.body) {
187
+ options.body = request.body;
188
+ }
189
+
190
+ // Debug logging for HTTP requests
191
+ console.log('[Turso HTTP] Request:', {
192
+ method: request.method,
193
+ url: fullUrl,
194
+ hasBody: !!request.body,
195
+ bodySize: request.body ? request.body.byteLength : 0,
196
+ headers: options.headers,
197
+ });
198
+
199
+ // Make the HTTP request
200
+ let response;
201
+ try {
202
+ response = await fetch(fullUrl, options);
203
+ } catch (e) {
204
+ // Detailed error logging
205
+ const errorDetails = {
206
+ url: fullUrl,
207
+ method: request.method,
208
+ hasBody: !!request.body,
209
+ bodySize: request.body ? request.body.byteLength : 0,
210
+ bodyType: request.body ? Object.prototype.toString.call(options.body) : 'none',
211
+ error: e instanceof Error ? {
212
+ message: e.message,
213
+ name: e.name,
214
+ stack: e.stack,
215
+ } : String(e),
216
+ };
217
+ console.error('[Turso HTTP] Request failed:', JSON.stringify(errorDetails, null, 2));
218
+ throw new Error(`HTTP request failed: ${e instanceof Error ? e.message : String(e)}. URL: ${fullUrl}, Method: ${request.method}, Body size: ${request.body ? request.body.byteLength : 0} bytes`);
219
+ }
220
+
221
+
222
+ // Set status code
223
+ item.setStatus(response.status);
224
+
225
+ // Read response body and push to item
226
+ const responseData = await response.arrayBuffer();
227
+ if (responseData.byteLength > 0) {
228
+ item.pushBuffer(responseData);
229
+ }
230
+
231
+ // Mark as done
232
+ item.done();
233
+ }
234
+
235
+ /**
236
+ * Process a full read request (atomic file read)
237
+ *
238
+ * @param item - The IO item
239
+ */
240
+ async function processFullRead(item: NativeSyncIoItem): Promise<void> {
241
+ const path = item.getFullReadPath();
242
+
243
+ try {
244
+ // Read the file (uses built-in JSI function or custom override)
245
+ const data = await fsReadFile(path);
246
+
247
+ // Push data to item
248
+ if (data.byteLength > 0) {
249
+ item.pushBuffer(data.buffer as ArrayBuffer);
250
+ }
251
+
252
+ // Mark as done
253
+ item.done();
254
+ } catch (error) {
255
+ // File not found is okay - treat as empty file
256
+ if (isFileNotFoundError(error)) {
257
+ // Empty file - just mark as done without pushing data
258
+ item.done();
259
+ } else {
260
+ throw error;
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Process a full write request (atomic file write)
267
+ *
268
+ * @param item - The IO item
269
+ */
270
+ async function processFullWrite(item: NativeSyncIoItem): Promise<void> {
271
+ const request = item.getFullWriteRequest();
272
+
273
+ // Convert ArrayBuffer to Uint8Array
274
+ const data = request.content ? new Uint8Array(request.content) : new Uint8Array(0);
275
+
276
+ // Write the file atomically (uses built-in JSI function or custom override)
277
+ await fsWriteFile(request.path, data);
278
+
279
+ // Mark as done
280
+ item.done();
281
+ }
282
+
283
+ /**
284
+ * Check if error is a file-not-found error
285
+ *
286
+ * @param error - The error to check
287
+ * @returns true if file not found
288
+ */
289
+ function isFileNotFoundError(error: unknown): boolean {
290
+ if (error instanceof Error) {
291
+ const message = error.message.toLowerCase();
292
+ return (
293
+ message.includes('enoent') ||
294
+ message.includes('not found') ||
295
+ message.includes('no such file')
296
+ );
297
+ }
298
+ return false;
299
+ }
300
+
301
+ /**
302
+ * Drain all pending IO items from sync engine queue and process them.
303
+ * This is called during statement execution when partial sync needs to load missing pages.
304
+ *
305
+ * @param database - The native sync database
306
+ * @param context - IO context with auth and URL information
307
+ */
308
+ export async function drainSyncIo(database: NativeSyncDatabase, context: IoContext): Promise<void> {
309
+ const promises: Promise<void>[] = [];
310
+
311
+ // Take all available IO items from the queue
312
+ while (true) {
313
+ const ioItem = database.ioTakeItem();
314
+ if (!ioItem) {
315
+ break; // No more items
316
+ }
317
+
318
+ // Process each item
319
+ promises.push(processIoItem(ioItem, context));
320
+ }
321
+
322
+ // Wait for all IO operations to complete
323
+ await Promise.all(promises);
324
+
325
+ // Step callbacks after IO processing
326
+ // This allows the sync engine to run any post-IO callbacks
327
+ database.ioStepCallbacks();
328
+ }