@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,480 @@
1
+ /**
2
+ * Database
3
+ *
4
+ * Unified high-level API for both local and sync databases.
5
+ * Constructor determines whether to use local-only or sync mode based on config.
6
+ */
7
+
8
+ import { Statement } from './Statement';
9
+ import type {
10
+ NativeDatabase,
11
+ NativeSyncDatabase,
12
+ NativeConnection,
13
+ Row,
14
+ RunResult,
15
+
16
+ BindParams,
17
+ DatabaseOpts,
18
+ SyncStats,
19
+ EncryptionOpts,
20
+ } from './types';
21
+ import {
22
+ driveVoidOperation,
23
+ driveConnectionOperation,
24
+ driveChangesOperation,
25
+ driveStatsOperation,
26
+ } from './internal/asyncOperation';
27
+ import { drainSyncIo } from './internal/ioProcessor';
28
+
29
+ /**
30
+ * Check if config has sync properties (url field)
31
+ */
32
+ function isSyncConfig(opts: DatabaseOpts): boolean {
33
+ return opts.url !== undefined && opts.url !== null;
34
+ }
35
+
36
+ /**
37
+ * Calculate reserved bytes based on encryption cipher.
38
+ * These values match the Turso Cloud encryption settings.
39
+ */
40
+ function getReservedBytesForCipher(encryption: EncryptionOpts | undefined): number {
41
+ if (!encryption) {
42
+ return 0;
43
+ }
44
+
45
+ switch (encryption.cipher) {
46
+ case 'aes256gcm':
47
+ case 'aes128gcm':
48
+ case 'chacha20poly1305':
49
+ return 28;
50
+ case 'aegis128l':
51
+ case 'aegis128x2':
52
+ case 'aegis128x4':
53
+ return 32;
54
+ case 'aegis256':
55
+ case 'aegis256x2':
56
+ case 'aegis256x4':
57
+ return 48;
58
+ default:
59
+ return 0;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Database class - works for both local-only and sync databases
65
+ *
66
+ * All database operations are async to properly handle IO requirements:
67
+ * - For local databases: async allows yielding to JS event loop
68
+ * - For sync databases: async required for network operations
69
+ * - For partial sync: async required to load missing pages on-demand
70
+ */
71
+ export class Database {
72
+ private _opts: DatabaseOpts;
73
+ private _nativeDb: NativeDatabase | null = null;
74
+ private _nativeSyncDb: NativeSyncDatabase | null = null;
75
+ private _connection: NativeConnection | null = null;
76
+ private _isSync = false;
77
+ private _connected = false;
78
+ private _closed = false;
79
+ private _extraIo?: () => Promise<void>;
80
+ private _ioContext?: {
81
+ authToken?: string | (() => string | Promise<string> | null);
82
+ baseUrl: string;
83
+ };
84
+
85
+ /**
86
+ * Create a new database (doesn't connect yet - call connect())
87
+ *
88
+ * @param opts - Database options
89
+ */
90
+ constructor(opts: DatabaseOpts) {
91
+ this._opts = opts;
92
+ this._isSync = isSyncConfig(opts);
93
+ }
94
+
95
+ /**
96
+ * Connect to the database (matches JavaScript bindings)
97
+ * For local databases: opens immediately
98
+ * For sync databases: bootstraps if needed
99
+ */
100
+ async connect(): Promise<void> {
101
+ if (this._connected) {
102
+ return;
103
+ }
104
+
105
+ if (this._isSync) {
106
+ await this.initSyncDatabase();
107
+ } else {
108
+ this.initLocalDatabase();
109
+ }
110
+
111
+ this._connected = true;
112
+ }
113
+
114
+ /**
115
+ * Initialize local-only database
116
+ */
117
+ private initLocalDatabase(): void {
118
+ if (typeof __TursoProxy === 'undefined') {
119
+ throw new Error('Turso native module not loaded');
120
+ }
121
+
122
+ const dbConfig = {
123
+ path: this._opts.path,
124
+ async_io: true, // Always use async IO in React Native
125
+ };
126
+
127
+ // Create native database (path normalization happens in C++ JSI layer)
128
+ this._nativeDb = __TursoProxy.newDatabase(this._opts.path, dbConfig);
129
+
130
+ // Open database
131
+ this._nativeDb.open();
132
+
133
+ // Get connection
134
+ this._connection = this._nativeDb.connect();
135
+ }
136
+
137
+ /**
138
+ * Initialize sync database
139
+ */
140
+ private async initSyncDatabase(): Promise<void> {
141
+ if (typeof __TursoProxy === 'undefined') {
142
+ throw new Error('Turso native module not loaded');
143
+ }
144
+
145
+ // Get URL (can be string or function)
146
+ let url: string | null = null;
147
+ if (typeof this._opts.url === 'function') {
148
+ url = this._opts.url();
149
+ } else if (typeof this._opts.url === 'string') {
150
+ url = this._opts.url;
151
+ }
152
+
153
+ if (!url) {
154
+ throw new Error('Sync database requires a URL');
155
+ }
156
+
157
+ // Build dbConfig (path normalization happens in C++ JSI layer)
158
+ const dbConfig = {
159
+ path: this._opts.path,
160
+ async_io: true, // Always use async IO in React Native
161
+ };
162
+
163
+ // Calculate reserved bytes from cipher
164
+ const reservedBytes = getReservedBytesForCipher(this._opts.remoteEncryption);
165
+
166
+ // Build syncConfig with all options
167
+ const syncConfig: any = {
168
+ remoteUrl: url,
169
+ clientName: this._opts.clientName || 'turso-sync-react-native',
170
+ longPollTimeoutMs: this._opts.longPollTimeoutMs,
171
+ bootstrapIfEmpty: this._opts.bootstrapIfEmpty ?? true,
172
+ reservedBytes: reservedBytes,
173
+ // Remote encryption options (key is passed to sync engine for HTTP headers)
174
+ remoteEncryptionKey: this._opts.remoteEncryption?.key,
175
+ remoteEncryptionCipher: this._opts.remoteEncryption?.cipher,
176
+ };
177
+
178
+ // Add partial sync options if present
179
+ if (this._opts.partialSyncExperimental) {
180
+ const partial = this._opts.partialSyncExperimental;
181
+ if (partial.bootstrapStrategy.kind === 'prefix') {
182
+ syncConfig.partialBootstrapStrategyPrefix = partial.bootstrapStrategy.length;
183
+ } else if (partial.bootstrapStrategy.kind === 'query') {
184
+ syncConfig.partialBootstrapStrategyQuery = partial.bootstrapStrategy.query;
185
+ }
186
+ syncConfig.partialBootstrapSegmentSize = partial.segmentSize;
187
+ syncConfig.partialBootstrapPrefetch = partial.prefetch;
188
+ }
189
+
190
+ // Create native sync database
191
+ this._nativeSyncDb = __TursoProxy.newSyncDatabase(dbConfig, syncConfig);
192
+
193
+ // Create IO context with auth token and base URL
194
+ this._ioContext = {
195
+ authToken: this._opts.authToken,
196
+ baseUrl: url,
197
+ };
198
+
199
+ // Create extraIo callback for partial sync support
200
+ // This callback drains the sync engine's IO queue during statement execution
201
+ this._extraIo = async () => {
202
+ if (this._nativeSyncDb && this._ioContext) {
203
+ await drainSyncIo(this._nativeSyncDb, this._ioContext);
204
+ }
205
+ };
206
+
207
+ // Bootstrap/open database
208
+ const operation = this._nativeSyncDb.create();
209
+ await driveVoidOperation(operation, this._nativeSyncDb, this._ioContext);
210
+
211
+ // Get connection
212
+ const connOperation = this._nativeSyncDb.connect();
213
+ this._connection = await driveConnectionOperation(connOperation, this._nativeSyncDb, this._ioContext);
214
+ }
215
+
216
+ /**
217
+ * Prepare a SQL statement
218
+ *
219
+ * @param sql - SQL statement to prepare
220
+ * @returns Prepared statement
221
+ */
222
+ prepare(sql: string): Statement {
223
+ this.checkOpen();
224
+
225
+ if (!this._connection) {
226
+ throw new Error('No connection available');
227
+ }
228
+
229
+ const nativeStmt = this._connection.prepareSingle(sql);
230
+ // Pass extraIo callback for partial sync support
231
+ return new Statement(nativeStmt, this._extraIo);
232
+ }
233
+
234
+ /**
235
+ * Execute SQL without returning results (for DDL, multi-statement SQL)
236
+ *
237
+ * @param sql - SQL to execute
238
+ */
239
+ async exec(sql: string): Promise<void> {
240
+ this.checkOpen();
241
+
242
+ if (!this._connection) {
243
+ throw new Error('No connection available');
244
+ }
245
+
246
+ // Use prepareFirst to handle multiple statements
247
+ let remaining = sql.trim();
248
+
249
+ while (remaining.length > 0) {
250
+ const result = this._connection.prepareFirst(remaining);
251
+
252
+ if (!result.statement) {
253
+ break; // No more statements
254
+ }
255
+
256
+ // Wrap in Statement to get IO handling
257
+ const stmt = new Statement(result.statement, this._extraIo);
258
+ try {
259
+ // Execute - will handle IO if needed
260
+ await stmt.run();
261
+ } finally {
262
+ stmt.finalize();
263
+ }
264
+
265
+ // Move to next statement
266
+ remaining = sql.substring(result.tailIdx).trim();
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Execute statement and return result info
272
+ *
273
+ * @param sql - SQL statement
274
+ * @param params - Bind parameters
275
+ * @returns Run result with changes and lastInsertRowid
276
+ */
277
+ async run(sql: string, ...params: BindParams[]): Promise<RunResult> {
278
+ const stmt = this.prepare(sql);
279
+ try {
280
+ return await stmt.run(...params);
281
+ } finally {
282
+ stmt.finalize();
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Execute query and return first row
288
+ *
289
+ * @param sql - SQL query
290
+ * @param params - Bind parameters
291
+ * @returns First row or undefined
292
+ */
293
+ async get(sql: string, ...params: BindParams[]): Promise<Row | undefined> {
294
+ const stmt = this.prepare(sql);
295
+ try {
296
+ return await stmt.get(...params);
297
+ } finally {
298
+ stmt.finalize();
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Execute query and return all rows
304
+ *
305
+ * @param sql - SQL query
306
+ * @param params - Bind parameters
307
+ * @returns All rows
308
+ */
309
+ async all(sql: string, ...params: BindParams[]): Promise<Row[]> {
310
+ const stmt = this.prepare(sql);
311
+ try {
312
+ return await stmt.all(...params);
313
+ } finally {
314
+ stmt.finalize();
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Execute function within a transaction
320
+ *
321
+ * @param fn - Function to execute
322
+ * @returns Function result
323
+ */
324
+ async transaction<T>(fn: () => T | Promise<T>): Promise<T> {
325
+ this.checkOpen();
326
+ await this.exec('BEGIN');
327
+ try {
328
+ const result = await fn();
329
+ await this.exec('COMMIT');
330
+ return result;
331
+ } catch (error) {
332
+ await this.exec('ROLLBACK');
333
+ throw error;
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Push local changes to remote (sync databases only)
339
+ */
340
+ async push(): Promise<void> {
341
+ if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
342
+ throw new Error('push() is only available for sync databases');
343
+ }
344
+
345
+ const operation = this._nativeSyncDb.pushChanges();
346
+ await driveVoidOperation(operation, this._nativeSyncDb, this._ioContext);
347
+ }
348
+
349
+ /**
350
+ * Pull remote changes and apply locally (sync databases only)
351
+ *
352
+ * @returns true if changes were applied, false if no changes
353
+ */
354
+ async pull(): Promise<boolean> {
355
+ if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
356
+ throw new Error('pull() is only available for sync databases');
357
+ }
358
+
359
+ // Wait for changes
360
+ const waitOperation = this._nativeSyncDb.waitChanges();
361
+ const changes = await driveChangesOperation(waitOperation, this._nativeSyncDb, this._ioContext);
362
+
363
+ // If no changes, return false
364
+ if (!changes) {
365
+ return false;
366
+ }
367
+
368
+ // Apply changes
369
+ const applyOperation = this._nativeSyncDb.applyChanges(changes);
370
+ await driveVoidOperation(applyOperation, this._nativeSyncDb, this._ioContext);
371
+
372
+ return true;
373
+ }
374
+
375
+ /**
376
+ * Get sync statistics (sync databases only)
377
+ *
378
+ * @returns Sync stats
379
+ */
380
+ async stats(): Promise<SyncStats> {
381
+ if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
382
+ throw new Error('stats() is only available for sync databases');
383
+ }
384
+
385
+ const operation = this._nativeSyncDb.stats();
386
+ return driveStatsOperation(operation, this._nativeSyncDb, this._ioContext);
387
+ }
388
+
389
+ /**
390
+ * Checkpoint database (sync databases only)
391
+ */
392
+ async checkpoint(): Promise<void> {
393
+ if (!this._isSync || !this._nativeSyncDb || !this._ioContext) {
394
+ throw new Error('checkpoint() is only available for sync databases');
395
+ }
396
+
397
+ const operation = this._nativeSyncDb.checkpoint();
398
+ await driveVoidOperation(operation, this._nativeSyncDb, this._ioContext);
399
+ }
400
+
401
+ /**
402
+ * Close the database
403
+ */
404
+ close(): void {
405
+ if (this._closed) {
406
+ return;
407
+ }
408
+
409
+ if (this._connection) {
410
+ this._connection.close();
411
+ this._connection = null;
412
+ }
413
+
414
+ if (this._nativeDb) {
415
+ this._nativeDb.close();
416
+ this._nativeDb = null;
417
+ }
418
+
419
+ if (this._nativeSyncDb) {
420
+ this._nativeSyncDb.close();
421
+ this._nativeSyncDb = null;
422
+ }
423
+
424
+ this._connected = false;
425
+ this._closed = true;
426
+ }
427
+
428
+ /**
429
+ * Get database path
430
+ */
431
+ get path(): string {
432
+ return this._opts.path;
433
+ }
434
+
435
+ /**
436
+ * Check if database is a sync database
437
+ */
438
+ get isSync(): boolean {
439
+ return this._isSync;
440
+ }
441
+
442
+ /**
443
+ * Check if database is open
444
+ */
445
+ get open(): boolean {
446
+ return !this._closed && this._connection !== null;
447
+ }
448
+
449
+ /**
450
+ * Check if in transaction
451
+ */
452
+ get inTransaction(): boolean {
453
+ if (!this._connection) {
454
+ return false;
455
+ }
456
+ return !this._connection.getAutocommit();
457
+ }
458
+
459
+ /**
460
+ * Get last insert rowid
461
+ */
462
+ get lastInsertRowid(): number {
463
+ if (!this._connection) {
464
+ return 0;
465
+ }
466
+ return this._connection.lastInsertRowid();
467
+ }
468
+
469
+ /**
470
+ * Check if open and throw if not
471
+ */
472
+ private checkOpen(): void {
473
+ if (this._closed) {
474
+ throw new Error('Database is closed');
475
+ }
476
+ if (!this._connected || !this._connection) {
477
+ throw new Error('Database not connected. Call connect() first.');
478
+ }
479
+ }
480
+ }