@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,372 @@
1
+ /**
2
+ * Statement
3
+ *
4
+ * High-level wrapper around NativeStatement providing a clean API.
5
+ * Handles parameter binding, row conversion, and result collection.
6
+ */
7
+
8
+ import type {
9
+ NativeStatement,
10
+ SQLiteValue,
11
+ BindParams,
12
+ Row,
13
+ RunResult,
14
+ } from './types';
15
+ import { TursoStatus, TursoType } from './types';
16
+
17
+ /**
18
+ * Prepared SQL statement
19
+ */
20
+ export class Statement {
21
+ private _statement: NativeStatement;
22
+ private _finalized = false;
23
+ private _extraIo?: () => Promise<void>;
24
+
25
+ constructor(statement: NativeStatement, extraIo?: () => Promise<void>) {
26
+ this._statement = statement;
27
+ this._extraIo = extraIo;
28
+ }
29
+
30
+ /**
31
+ * Bind parameters to the statement
32
+ *
33
+ * @param params - Parameters to bind (array, object, or single value)
34
+ * @returns this for chaining
35
+ */
36
+ bind(...params: BindParams[]): this {
37
+ if (this._finalized) {
38
+ throw new Error('Statement has been finalized');
39
+ }
40
+
41
+ // Flatten parameters if single array passed
42
+ let flatParams: SQLiteValue[];
43
+ if (params.length === 1 && Array.isArray(params[0])) {
44
+ flatParams = params[0];
45
+ } else if (params.length === 1 && typeof params[0] === 'object' && params[0] !== null) {
46
+ // Named parameters
47
+ const namedParams = params[0] as Record<string, SQLiteValue>;
48
+ this.bindNamed(namedParams);
49
+ return this;
50
+ } else {
51
+ flatParams = params as SQLiteValue[];
52
+ }
53
+
54
+ // Bind positional parameters
55
+ this.bindPositional(flatParams);
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Bind positional parameters (1-indexed)
61
+ *
62
+ * @param params - Array of values to bind
63
+ */
64
+ private bindPositional(params: SQLiteValue[]): void {
65
+ for (let i = 0; i < params.length; i++) {
66
+ const position = i + 1; // 1-indexed
67
+ const value = params[i]!;
68
+
69
+ this.bindValue(position, value);
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Bind named parameters
75
+ *
76
+ * @param params - Object with named parameters
77
+ */
78
+ private bindNamed(params: Record<string, SQLiteValue>): void {
79
+ for (const [name, value] of Object.entries(params)) {
80
+ // Get position for named parameter
81
+ const position = this._statement.namedPosition(name);
82
+ if (position < 0) {
83
+ throw new Error(`Unknown parameter name: ${name}`);
84
+ }
85
+
86
+ this.bindValue(position, value);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Bind a single value at a position
92
+ *
93
+ * @param position - 1-indexed position
94
+ * @param value - Value to bind
95
+ */
96
+ private bindValue(position: number, value: SQLiteValue): void {
97
+ if (value === null || value === undefined) {
98
+ this._statement.bindPositionalNull(position);
99
+ } else if (typeof value === 'number') {
100
+ // Check if integer or float
101
+ if (Number.isInteger(value)) {
102
+ this._statement.bindPositionalInt(position, value);
103
+ } else {
104
+ this._statement.bindPositionalDouble(position, value);
105
+ }
106
+ } else if (typeof value === 'string') {
107
+ this._statement.bindPositionalText(position, value);
108
+ } else if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) {
109
+ const buffer = value as unknown as ArrayBuffer;
110
+ this._statement.bindPositionalBlob(position, buffer);
111
+ } else {
112
+ throw new Error(`Unsupported parameter type: ${typeof value}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Execute statement without returning rows (for INSERT, UPDATE, DELETE)
118
+ *
119
+ * @param params - Optional parameters to bind
120
+ * @returns Result with changes and lastInsertRowid
121
+ */
122
+ async run(...params: BindParams[]): Promise<RunResult> {
123
+ if (this._finalized) {
124
+ throw new Error('Statement has been finalized');
125
+ }
126
+
127
+ // Bind parameters if provided
128
+ if (params.length > 0) {
129
+ this.bind(...params);
130
+ }
131
+
132
+ // Execute statement with IO handling
133
+ const result = await this.executeWithIo();
134
+
135
+ // Reset for next execution
136
+ this._statement.reset();
137
+
138
+ return {
139
+ changes: result.rowsChanged,
140
+ lastInsertRowid: 0, // Not available from execute, would need connection
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Execute statement handling potential IO (for partial sync)
146
+ * Matches Python's _run_execute_with_io pattern
147
+ *
148
+ * @returns Execution result
149
+ */
150
+ private async executeWithIo(): Promise<{ status: number; rowsChanged: number }> {
151
+ while (true) {
152
+ const result = this._statement.execute();
153
+
154
+ if (result.status === TursoStatus.IO) {
155
+ // Statement needs IO (e.g., loading missing pages with partial sync)
156
+ this._statement.runIo();
157
+
158
+ // Drain sync engine IO queue
159
+ if (this._extraIo) {
160
+ await this._extraIo();
161
+ }
162
+
163
+ continue;
164
+ }
165
+
166
+ if (result.status !== TursoStatus.DONE) {
167
+ throw new Error(`Statement execution failed with status: ${result.status}`);
168
+ }
169
+
170
+ return result;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Step statement once handling potential IO (for partial sync)
176
+ * Matches Python's _step_once_with_io pattern
177
+ *
178
+ * @returns Status code
179
+ */
180
+ private async stepWithIo(): Promise<number> {
181
+ while (true) {
182
+ const status = this._statement.step();
183
+
184
+ if (status === TursoStatus.IO) {
185
+ // Statement needs IO (e.g., loading missing pages with partial sync)
186
+ this._statement.runIo();
187
+
188
+ // Drain sync engine IO queue
189
+ if (this._extraIo) {
190
+ await this._extraIo();
191
+ }
192
+
193
+ continue;
194
+ }
195
+
196
+ return status;
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Execute statement and return first row
202
+ *
203
+ * @param params - Optional parameters to bind
204
+ * @returns First row or undefined
205
+ */
206
+ async get(...params: BindParams[]): Promise<Row | undefined> {
207
+ if (this._finalized) {
208
+ throw new Error('Statement has been finalized');
209
+ }
210
+
211
+ // Bind parameters if provided
212
+ if (params.length > 0) {
213
+ this.bind(...params);
214
+ }
215
+
216
+ // Step once with async IO handling
217
+ const status = await this.stepWithIo();
218
+
219
+ if (status === TursoStatus.ROW) {
220
+ const row = this.readRow();
221
+ this._statement.reset();
222
+ return row;
223
+ }
224
+
225
+ if (status === TursoStatus.DONE) {
226
+ this._statement.reset();
227
+ return undefined;
228
+ }
229
+
230
+ throw new Error(`Statement step failed with status: ${status}`);
231
+ }
232
+
233
+ /**
234
+ * Execute statement and return all rows
235
+ *
236
+ * @param params - Optional parameters to bind
237
+ * @returns Array of rows
238
+ */
239
+ async all(...params: BindParams[]): Promise<Row[]> {
240
+ if (this._finalized) {
241
+ throw new Error('Statement has been finalized');
242
+ }
243
+
244
+ // Bind parameters if provided
245
+ if (params.length > 0) {
246
+ this.bind(...params);
247
+ }
248
+
249
+ const rows: Row[] = [];
250
+
251
+ // Step through all rows with async IO handling
252
+ while (true) {
253
+ const status = await this.stepWithIo();
254
+
255
+ if (status === TursoStatus.ROW) {
256
+ rows.push(this.readRow());
257
+ } else if (status === TursoStatus.DONE) {
258
+ break;
259
+ } else {
260
+ throw new Error(`Statement step failed with status: ${status}`);
261
+ }
262
+ }
263
+
264
+ this._statement.reset();
265
+ return rows;
266
+ }
267
+
268
+ /**
269
+ * Read current row into an object
270
+ *
271
+ * @returns Row object with column name keys
272
+ */
273
+ private readRow(): Row {
274
+ const row: Row = {};
275
+ const columnCount = this._statement.columnCount();
276
+
277
+ for (let i = 0; i < columnCount; i++) {
278
+ const name = this._statement.columnName(i);
279
+ if (!name) {
280
+ throw new Error(`Failed to get column name at index ${i}`);
281
+ }
282
+
283
+ const value = this.readColumnValue(i);
284
+ row[name] = value;
285
+ }
286
+
287
+ return row;
288
+ }
289
+
290
+ /**
291
+ * Read value at column index
292
+ *
293
+ * @param index - Column index
294
+ * @returns Column value
295
+ */
296
+ private readColumnValue(index: number): SQLiteValue {
297
+ const kind = this._statement.rowValueKind(index);
298
+
299
+ switch (kind) {
300
+ case TursoType.NULL:
301
+ return null;
302
+
303
+ case TursoType.INTEGER:
304
+ return this._statement.rowValueInt(index);
305
+
306
+ case TursoType.REAL:
307
+ return this._statement.rowValueDouble(index);
308
+
309
+ case TursoType.TEXT:
310
+ // Use rowValueText which directly returns a string from C++ (avoids encoding issues)
311
+ return this._statement.rowValueText(index);
312
+
313
+ case TursoType.BLOB:
314
+ return this._statement.rowValueBytesPtr(index) || new ArrayBuffer(0);
315
+
316
+ default:
317
+ throw new Error(`Unknown column type: ${kind}`);
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Reset statement for re-execution
323
+ *
324
+ * @returns this for chaining
325
+ */
326
+ reset(): this {
327
+ if (this._finalized) {
328
+ throw new Error('Statement has been finalized');
329
+ }
330
+
331
+ this._statement.reset();
332
+ return this;
333
+ }
334
+
335
+ /**
336
+ * Finalize and release statement resources
337
+ */
338
+ async finalize(): Promise<void> {
339
+ if (this._finalized) {
340
+ return;
341
+ }
342
+
343
+ while (true) {
344
+ const status = this._statement.finalize();
345
+
346
+ if (status === TursoStatus.IO) {
347
+ // Statement needs IO (e.g., loading missing pages with partial sync)
348
+ this._statement.runIo();
349
+
350
+ // Drain sync engine IO queue
351
+ if (this._extraIo) {
352
+ await this._extraIo();
353
+ }
354
+
355
+ continue;
356
+ }
357
+
358
+ if (status !== TursoStatus.DONE) {
359
+ throw new Error(`Statement finalization failed with status: ${status}`);
360
+ }
361
+ break;
362
+ }
363
+ this._finalized = true;
364
+ }
365
+
366
+ /**
367
+ * Check if statement has been finalized
368
+ */
369
+ get finalized(): boolean {
370
+ return this._finalized;
371
+ }
372
+ }
package/src/index.ts ADDED
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Turso React Native SDK
3
+ *
4
+ * Main entry point for the SDK. Supports both local-only and sync databases.
5
+ */
6
+
7
+ import { NativeModules } from 'react-native';
8
+ import { Database } from './Database';
9
+ import type {
10
+ DatabaseOpts,
11
+ TursoNativeModule,
12
+ TursoProxy as TursoProxyType,
13
+ } from './types';
14
+ import { setFileSystemImpl } from './internal/ioProcessor';
15
+
16
+ // Re-export all public types
17
+ export type {
18
+ // Core types
19
+ SQLiteValue,
20
+ BindParams,
21
+ Row,
22
+ RunResult,
23
+
24
+ // Database config
25
+ DatabaseOpts,
26
+ EncryptionOpts,
27
+
28
+ // Sync types
29
+ SyncStats,
30
+
31
+ // Enums
32
+ TursoStatus,
33
+ TursoType,
34
+ } from './types';
35
+
36
+ // Re-export classes
37
+ export { Database } from './Database';
38
+ export { Statement } from './Statement';
39
+
40
+ // Export file system configuration function
41
+ export { setFileSystemImpl } from './internal/ioProcessor';
42
+
43
+ // Get the native module
44
+ const TursoNative: TursoNativeModule | undefined = NativeModules.Turso;
45
+
46
+ // Check if native module is available
47
+ if (!TursoNative) {
48
+ throw new Error(
49
+ `@tursodatabase/sync-react-native: Native module not found. Make sure you have properly linked the library.\n` +
50
+ `- iOS: Run 'pod install' in your ios directory\n` +
51
+ `- Android: Make sure the package is properly included in your MainApplication.java`
52
+ );
53
+ }
54
+
55
+ // Install the JSI bindings
56
+ const installed = TursoNative.install();
57
+ if (!installed) {
58
+ throw new Error(
59
+ '@tursodatabase/sync-react-native: Failed to install JSI bindings. Make sure the New Architecture is enabled.'
60
+ );
61
+ }
62
+
63
+ // Get the proxy that was installed on the global object
64
+ // __TursoProxy is declared globally in types.ts
65
+ const TursoProxy: TursoProxyType = __TursoProxy;
66
+
67
+ if (!TursoProxy) {
68
+ throw new Error(
69
+ '@tursodatabase/sync-react-native: JSI bindings not found on global object. This is a bug.'
70
+ );
71
+ }
72
+
73
+ /**
74
+ * Helper function to construct a database path in a writable directory.
75
+ * On mobile platforms, you must use writable directories (not relative paths).
76
+ *
77
+ * @param filename - Database filename (e.g., 'mydb.db')
78
+ * @param directory - Directory to use ('documents', 'database', or 'library')
79
+ * @returns Absolute path to the database file
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * import { getDbPath, connect } from '@tursodatabase/sync-react-native';
84
+ *
85
+ * const dbPath = getDbPath('mydb.db');
86
+ * const db = await connect({ path: dbPath });
87
+ * ```
88
+ */
89
+ export function getDbPath(filename: string, directory: 'documents' | 'database' | 'library' = 'documents'): string {
90
+ const basePath = paths[directory];
91
+ if (!basePath || basePath === '.') {
92
+ throw new Error(
93
+ `Unable to get ${directory} path for this platform. ` +
94
+ 'Make sure the native module is properly loaded.'
95
+ );
96
+ }
97
+ return `${basePath}/${filename}`;
98
+ }
99
+
100
+ /**
101
+ * Connect to a database asynchronously (matches JavaScript bindings API)
102
+ *
103
+ * This is the main entry point for the SDK, matching the API from
104
+ * @tursodatabase/sync-native and @tursodatabase/database-native.
105
+ *
106
+ * **Path handling**: Relative paths are automatically placed in writable directories:
107
+ * - Android: app's database directory (`/data/data/com.app/databases/`)
108
+ * - iOS: app's documents directory
109
+ *
110
+ * Absolute paths and `:memory:` are used as-is.
111
+ *
112
+ * @param opts - Database options
113
+ * @returns Promise resolving to Database instance
114
+ *
115
+ * @example Local database (relative path)
116
+ * ```ts
117
+ * import { connect } from '@tursodatabase/sync-react-native';
118
+ *
119
+ * // Relative path automatically placed in writable directory
120
+ * const db = await connect({ path: 'local.db' });
121
+ * await db.exec('CREATE TABLE users (id INTEGER, name TEXT)');
122
+ * ```
123
+ *
124
+ * @example Using :memory: for in-memory database
125
+ * ```ts
126
+ * const db = await connect({ path: ':memory:' });
127
+ * ```
128
+ *
129
+ * @example Sync database
130
+ * ```ts
131
+ * const db = await connect({
132
+ * path: 'replica.db',
133
+ * url: 'libsql://mydb.turso.io',
134
+ * authToken: 'token-here',
135
+ * });
136
+ * const users = await db.all('SELECT * FROM users');
137
+ * await db.push();
138
+ * await db.pull();
139
+ * ```
140
+ *
141
+ * @example Using absolute path (advanced)
142
+ * ```ts
143
+ * import { connect, paths } from '@tursodatabase/sync-react-native';
144
+ *
145
+ * const db = await connect({ path: `${paths.documents}/mydb.db` });
146
+ * ```
147
+ */
148
+ export async function connect(opts: DatabaseOpts): Promise<Database> {
149
+ const db = new Database(opts);
150
+ await db.connect();
151
+ return db;
152
+ }
153
+
154
+ /**
155
+ * Returns the Turso library version.
156
+ */
157
+ export function version(): string {
158
+ return TursoProxy.version();
159
+ }
160
+
161
+ /**
162
+ * Configure Turso settings such as logging.
163
+ * Should be called before any database operations.
164
+ *
165
+ * @param options - Configuration options
166
+ * @example
167
+ * ```ts
168
+ * import { setup } from '@tursodatabase/sync-react-native';
169
+ *
170
+ * setup({ logLevel: 'debug' });
171
+ * ```
172
+ */
173
+ export function setup(options: {logLevel?: string}): void {
174
+ TursoProxy.setup(options);
175
+ }
176
+
177
+ /**
178
+ * Platform-specific writable directory paths.
179
+ * Use these to construct absolute paths for database files.
180
+ *
181
+ * NOTE: With automatic path normalization, you typically don't need this.
182
+ * Just pass relative paths like 'mydb.db' and they'll be placed in the correct directory.
183
+ *
184
+ * @example
185
+ * ```ts
186
+ * import { paths, connect } from '@tursodatabase/sync-react-native';
187
+ *
188
+ * // Create database in app's documents/files directory
189
+ * const dbPath = `${paths.documents}/mydb.db`;
190
+ * const db = await connect({ path: dbPath });
191
+ * ```
192
+ */
193
+ export const paths = {
194
+ /**
195
+ * Primary documents/database directory (writable)
196
+ * - iOS: App's Documents directory (absolute path)
197
+ * - Android: App's database directory (absolute path) - preferred for databases
198
+ */
199
+ get documents(): string {
200
+ return TursoNative?.IOS_DOCUMENT_PATH || TursoNative?.ANDROID_DATABASE_PATH || '.';
201
+ },
202
+
203
+ /**
204
+ * Database-specific directory (writable)
205
+ * - iOS: Same as documents
206
+ * - Android: Database directory (absolute path)
207
+ */
208
+ get database(): string {
209
+ return TursoNative?.IOS_DOCUMENT_PATH || TursoNative?.ANDROID_DATABASE_PATH || '.';
210
+ },
211
+
212
+ /**
213
+ * Files directory (writable)
214
+ * - iOS: Same as documents
215
+ * - Android: App's files directory (absolute path)
216
+ */
217
+ get files(): string {
218
+ return TursoNative?.IOS_DOCUMENT_PATH || TursoNative?.ANDROID_FILES_PATH || '.';
219
+ },
220
+
221
+ /**
222
+ * Library directory (iOS only, writable)
223
+ * - iOS: App's Library directory (absolute path)
224
+ * - Android: Same as files
225
+ */
226
+ get library(): string {
227
+ return TursoNative?.IOS_LIBRARY_PATH || TursoNative?.ANDROID_FILES_PATH || '.';
228
+ },
229
+ };
230
+
231
+ // Default export
232
+ export default {
233
+ connect,
234
+ version,
235
+ setup,
236
+ setFileSystemImpl,
237
+ getDbPath,
238
+ paths,
239
+ Database,
240
+ };