@syncular/dialect-expo-sqlite 0.0.1-98 → 0.0.2-126

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.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @syncular/dialect-expo-sqlite
2
+
3
+ Expo SQLite Kysely dialect for React Native (expo-sqlite). Includes JSON serialization support via `SerializePlugin`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @syncular/dialect-expo-sqlite
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { openDatabaseSync } from 'expo-sqlite';
15
+ import { createExpoSqliteDb } from '@syncular/dialect-expo-sqlite';
16
+
17
+ const db = createExpoSqliteDb<MyDb>({ name: 'app.db', openDatabaseSync });
18
+ ```
19
+
20
+ ## Documentation
21
+
22
+ - Dialect selection: https://syncular.dev/docs/introduction/installation#client-database-dialects
23
+
24
+ ## Links
25
+
26
+ - GitHub: https://github.com/syncular/syncular
27
+ - Issues: https://github.com/syncular/syncular/issues
28
+
29
+ > Status: Alpha. APIs may change between releases.
package/dist/index.d.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * SQLite-compatible — use with @syncular/server-dialect-sqlite.
7
7
  *
8
8
  * Implements a custom Kysely Driver that wraps expo-sqlite's sync API
9
- * into the promise-based interface Kysely expects.
9
+ * into the promise-based interface Kysely expects. All operations are
10
+ * serialized through a single connection to prevent "database is locked"
11
+ * errors from concurrent access on the native handle.
10
12
  */
11
13
  import { SerializePlugin } from '@syncular/core';
12
14
  import type { DatabaseIntrospector, Dialect, DialectAdapter, Driver, QueryCompiler } from 'kysely';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAEV,oBAAoB,EACpB,OAAO,EACP,cAAc,EACd,MAAM,EACN,aAAa,EAGd,MAAM,QAAQ,CAAC;AAChB,OAAO,EAEL,MAAM,EAIP,MAAM,QAAQ,CAAC;AAEhB,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,EAAE,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,CAAC,EAAE,CAAC;IAC9D,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,SAAS,mBAAmB,EAAE,GAAG,CAAC,EAAE,CAAC;IAC3E,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,mBAAmB,CAAC;IACxE,OAAO,CACL,GAAG,EAAE,MAAM,EACX,GAAG,MAAM,EAAE,SAAS,mBAAmB,EAAE,GACxC,mBAAmB,CAAC;IACvB,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,0DAA0D;AAC1D,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,sBAAsB,CAAC;AAEjE,UAAU,qBAAqB;IAC7B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AAED,UAAU,yBAAyB;IACjC,gDAAgD;IAChD,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,MAAM,MAAM,iBAAiB,GACzB,qBAAqB,GACrB,yBAAyB,CAAC;AAE9B;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC,CAAC,CAAC,CAK3E;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,GACzB,iBAAiB,CAEnB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD;AAMD,cAAM,iBAAkB,YAAW,OAAO;;IAGxC,YAAY,OAAO,EAAE,iBAAiB,EAErC;IAED,aAAa,IAAI,cAAc,CAE9B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,mBAAmB,IAAI,aAAa,CAEnC;IAED,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,oBAAoB,CAE5D;CACF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,KAAK,EAEV,oBAAoB,EACpB,OAAO,EACP,cAAc,EACd,MAAM,EACN,aAAa,EAGd,MAAM,QAAQ,CAAC;AAChB,OAAO,EAEL,MAAM,EAIP,MAAM,QAAQ,CAAC;AAEhB,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,UAAU,CAAC;AAEtE,MAAM,MAAM,oBAAoB,GAAG,mBAAmB,EAAE,CAAC;AAEzD,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,CAAC,EAAE,CAAC;IAC9D,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,SAAS,mBAAmB,EAAE,GAAG,CAAC,EAAE,CAAC;IAC3E,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,mBAAmB,CAAC;IACxE,OAAO,CACL,GAAG,EAAE,MAAM,EACX,GAAG,MAAM,EAAE,SAAS,mBAAmB,EAAE,GACxC,mBAAmB,CAAC;IACvB,SAAS,IAAI,IAAI,CAAC;CACnB;AAED,0DAA0D;AAC1D,KAAK,gBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,sBAAsB,CAAC;AAEjE,UAAU,qBAAqB;IAC7B,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,gBAAgB,EAAE,gBAAgB,CAAC;CACpC;AAED,UAAU,yBAAyB;IACjC,gDAAgD;IAChD,QAAQ,EAAE,sBAAsB,CAAC;CAClC;AAED,MAAM,MAAM,iBAAiB,GACzB,qBAAqB,GACrB,yBAAyB,CAAC;AAE9B;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,OAAO,EAAE,iBAAiB,GAAG,MAAM,CAAC,CAAC,CAAC,CAK3E;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,iBAAiB,GACzB,iBAAiB,CAEnB;AAED;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,eAAe,CAEvD;AAkCD,cAAM,iBAAkB,YAAW,OAAO;;IAGxC,YAAY,OAAO,EAAE,iBAAiB,EAErC;IAED,aAAa,IAAI,cAAc,CAE9B;IAED,YAAY,IAAI,MAAM,CAErB;IAED,mBAAmB,IAAI,aAAa,CAEnC;IAED,kBAAkB,CAAC,EAAE,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,oBAAoB,CAE5D;CACF"}
package/dist/index.js CHANGED
@@ -6,7 +6,9 @@
6
6
  * SQLite-compatible — use with @syncular/server-dialect-sqlite.
7
7
  *
8
8
  * Implements a custom Kysely Driver that wraps expo-sqlite's sync API
9
- * into the promise-based interface Kysely expects.
9
+ * into the promise-based interface Kysely expects. All operations are
10
+ * serialized through a single connection to prevent "database is locked"
11
+ * errors from concurrent access on the native handle.
10
12
  */
11
13
  import { SerializePlugin } from '@syncular/core';
12
14
  import { CompiledQuery, Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, } from 'kysely';
@@ -44,6 +46,31 @@ export function createSerializePlugin() {
44
46
  return new SerializePlugin();
45
47
  }
46
48
  // ---------------------------------------------------------------------------
49
+ // Simple async mutex — serializes all DB access on a single native handle.
50
+ // ---------------------------------------------------------------------------
51
+ class Mutex {
52
+ #queue = [];
53
+ #locked = false;
54
+ async acquire() {
55
+ if (!this.#locked) {
56
+ this.#locked = true;
57
+ return;
58
+ }
59
+ return new Promise((resolve) => {
60
+ this.#queue.push(resolve);
61
+ });
62
+ }
63
+ release() {
64
+ const next = this.#queue.shift();
65
+ if (next) {
66
+ next();
67
+ }
68
+ else {
69
+ this.#locked = false;
70
+ }
71
+ }
72
+ }
73
+ // ---------------------------------------------------------------------------
47
74
  // Kysely Dialect implementation for expo-sqlite
48
75
  // ---------------------------------------------------------------------------
49
76
  class ExpoSqliteDialect {
@@ -67,17 +94,26 @@ class ExpoSqliteDialect {
67
94
  class ExpoSqliteDriver {
68
95
  #options;
69
96
  #db;
97
+ #connection;
98
+ #mutex = new Mutex();
70
99
  constructor(options) {
71
100
  this.#options = options;
72
101
  }
73
102
  async init() {
74
103
  this.#db = this.#resolveDatabase();
104
+ // Enable WAL mode for better concurrency (allows concurrent reads
105
+ // while writing, prevents "database is locked" errors during sync).
106
+ this.#db.runSync('PRAGMA journal_mode = WAL', []);
107
+ // Wait up to 5s for locks to clear instead of failing immediately.
108
+ this.#db.runSync('PRAGMA busy_timeout = 5000', []);
109
+ this.#connection = new ExpoSqliteConnection(this.#db);
75
110
  }
76
111
  async acquireConnection() {
77
- return new ExpoSqliteConnection(this.#db);
112
+ await this.#mutex.acquire();
113
+ return this.#connection;
78
114
  }
79
115
  async beginTransaction(connection, _settings) {
80
- await connection.executeQuery(CompiledQuery.raw('begin'));
116
+ await connection.executeQuery(CompiledQuery.raw('begin immediate'));
81
117
  }
82
118
  async commitTransaction(connection) {
83
119
  await connection.executeQuery(CompiledQuery.raw('commit'));
@@ -86,7 +122,7 @@ class ExpoSqliteDriver {
86
122
  await connection.executeQuery(CompiledQuery.raw('rollback'));
87
123
  }
88
124
  async releaseConnection(_connection) {
89
- // Single-connection model — nothing to release.
125
+ this.#mutex.release();
90
126
  }
91
127
  async destroy() {
92
128
  if (this.#db) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/dialect-expo-sqlite",
3
- "version": "0.0.1-98",
3
+ "version": "0.0.2-126",
4
4
  "description": "Expo SQLite dialect for the Syncular client",
5
5
  "license": "MIT",
6
6
  "author": "Benjamin Kniffler",
@@ -43,7 +43,7 @@
43
43
  "release": "bunx syncular-publish"
44
44
  },
45
45
  "dependencies": {
46
- "@syncular/core": "0.0.1"
46
+ "@syncular/core": "0.0.2-126"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "expo-sqlite": "^16.0.10",
package/src/index.test.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { Database } from 'bun:sqlite';
1
2
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
2
- import type { Kysely } from 'kysely';
3
+ import { type Kysely, sql } from 'kysely';
3
4
  import type {
4
5
  ExpoSqliteBindParams,
5
6
  ExpoSqliteBindValue,
@@ -61,9 +62,18 @@ describe('expo sqlite dialect RETURNING behavior', () => {
61
62
  let fakeDb: FakeExpoDatabase;
62
63
  let db: Kysely<TestDb>;
63
64
 
64
- beforeEach(() => {
65
+ beforeEach(async () => {
65
66
  fakeDb = new FakeExpoDatabase();
66
67
  db = createExpoSqliteDb<TestDb>({ database: fakeDb });
68
+ // Run a no-op query to trigger driver init (PRAGMA statements).
69
+ // Then clear the tracking arrays so tests only see their own SQL.
70
+ await db
71
+ .selectFrom('tasks' as any)
72
+ .selectAll()
73
+ .execute()
74
+ .catch(() => {});
75
+ fakeDb.getAllSql.length = 0;
76
+ fakeDb.runSql.length = 0;
67
77
  });
68
78
 
69
79
  it('routes UPDATE ... RETURNING through getAllSync and returns rows', async () => {
@@ -94,3 +104,142 @@ describe('expo sqlite dialect RETURNING behavior', () => {
94
104
  await db.destroy();
95
105
  });
96
106
  });
107
+
108
+ /**
109
+ * Adapter that wraps bun:sqlite Database to match ExpoSqliteDatabaseLike,
110
+ * so we can run real SQLite concurrency tests without expo-sqlite.
111
+ */
112
+ class BunSqliteAdapter implements ExpoSqliteDatabaseLike {
113
+ readonly #db: Database;
114
+
115
+ constructor(path = ':memory:') {
116
+ this.#db = new Database(path);
117
+ }
118
+
119
+ getAllSync<R>(sqlStr: string, params: ExpoSqliteBindParams): R[];
120
+ getAllSync<R>(sqlStr: string, ...params: readonly ExpoSqliteBindValue[]): R[];
121
+ getAllSync<R>(
122
+ sqlStr: string,
123
+ ...paramsOrFirst:
124
+ | readonly [ExpoSqliteBindParams]
125
+ | readonly ExpoSqliteBindValue[]
126
+ ): R[] {
127
+ const p = this.#resolveParams(paramsOrFirst);
128
+ return this.#db.prepare(sqlStr).all(...p) as R[];
129
+ }
130
+
131
+ runSync(sqlStr: string, params: ExpoSqliteBindParams): ExpoSqliteRunResult;
132
+ runSync(
133
+ sqlStr: string,
134
+ ...params: readonly ExpoSqliteBindValue[]
135
+ ): ExpoSqliteRunResult;
136
+ runSync(
137
+ sqlStr: string,
138
+ ...paramsOrFirst:
139
+ | readonly [ExpoSqliteBindParams]
140
+ | readonly ExpoSqliteBindValue[]
141
+ ): ExpoSqliteRunResult {
142
+ const p = this.#resolveParams(paramsOrFirst);
143
+ const result = this.#db.run(sqlStr, ...p);
144
+ return {
145
+ changes: result.changes,
146
+ lastInsertRowId: Number(result.lastInsertRowid),
147
+ };
148
+ }
149
+
150
+ closeSync(): void {
151
+ this.#db.close();
152
+ }
153
+
154
+ #resolveParams(
155
+ paramsOrFirst:
156
+ | readonly [ExpoSqliteBindParams]
157
+ | readonly ExpoSqliteBindValue[]
158
+ ): ExpoSqliteBindValue[] {
159
+ if (paramsOrFirst.length === 1 && Array.isArray(paramsOrFirst[0])) {
160
+ return paramsOrFirst[0] as ExpoSqliteBindValue[];
161
+ }
162
+ return paramsOrFirst as ExpoSqliteBindValue[];
163
+ }
164
+ }
165
+
166
+ interface ConcurrencyDb {
167
+ items: { id: number; value: string };
168
+ }
169
+
170
+ describe('expo sqlite dialect concurrency (real SQLite)', () => {
171
+ let adapter: BunSqliteAdapter;
172
+ let db: Kysely<ConcurrencyDb>;
173
+
174
+ beforeEach(async () => {
175
+ adapter = new BunSqliteAdapter();
176
+ db = createExpoSqliteDb<ConcurrencyDb>({ database: adapter });
177
+ await sql`create table items (id integer primary key, value text)`.execute(
178
+ db
179
+ );
180
+ });
181
+
182
+ afterEach(async () => {
183
+ await db.destroy();
184
+ });
185
+
186
+ it('concurrent transaction + read does not deadlock', async () => {
187
+ // Seed data
188
+ for (let i = 0; i < 100; i++) {
189
+ await db
190
+ .insertInto('items')
191
+ .values({ id: i, value: `v${i}` })
192
+ .execute();
193
+ }
194
+
195
+ // Simulate sync engine: long-running write transaction
196
+ const writeTx = db.transaction().execute(async (trx) => {
197
+ for (let i = 100; i < 200; i++) {
198
+ await trx
199
+ .insertInto('items')
200
+ .values({ id: i, value: `tx-${i}` })
201
+ .execute();
202
+ }
203
+ // Yield to allow the concurrent read to interleave
204
+ await new Promise((r) => setTimeout(r, 10));
205
+ for (let i = 200; i < 300; i++) {
206
+ await trx
207
+ .insertInto('items')
208
+ .values({ id: i, value: `tx-${i}` })
209
+ .execute();
210
+ }
211
+ });
212
+
213
+ // Simulate React hook: concurrent read while transaction is open
214
+ const readResult = db.selectFrom('items').select(['id', 'value']).execute();
215
+
216
+ // Both should complete without "database is locked"
217
+ const [, rows] = await Promise.all([writeTx, readResult]);
218
+ expect(rows.length).toBeGreaterThanOrEqual(100);
219
+ });
220
+
221
+ it('sequential write transactions complete without error', async () => {
222
+ await db.transaction().execute(async (trx) => {
223
+ for (let i = 0; i < 50; i++) {
224
+ await trx
225
+ .insertInto('items')
226
+ .values({ id: i, value: `a-${i}` })
227
+ .execute();
228
+ }
229
+ });
230
+
231
+ await db.transaction().execute(async (trx) => {
232
+ for (let i = 1000; i < 1050; i++) {
233
+ await trx
234
+ .insertInto('items')
235
+ .values({ id: i, value: `b-${i}` })
236
+ .execute();
237
+ }
238
+ });
239
+
240
+ const { rows } = await sql<{
241
+ cnt: number;
242
+ }>`select count(*) as cnt from items`.execute(db);
243
+ expect(rows[0].cnt).toBe(100);
244
+ });
245
+ });
package/src/index.ts CHANGED
@@ -6,7 +6,9 @@
6
6
  * SQLite-compatible — use with @syncular/server-dialect-sqlite.
7
7
  *
8
8
  * Implements a custom Kysely Driver that wraps expo-sqlite's sync API
9
- * into the promise-based interface Kysely expects.
9
+ * into the promise-based interface Kysely expects. All operations are
10
+ * serialized through a single connection to prevent "database is locked"
11
+ * errors from concurrent access on the native handle.
10
12
  */
11
13
 
12
14
  import { SerializePlugin } from '@syncular/core';
@@ -105,6 +107,34 @@ export function createSerializePlugin(): SerializePlugin {
105
107
  return new SerializePlugin();
106
108
  }
107
109
 
110
+ // ---------------------------------------------------------------------------
111
+ // Simple async mutex — serializes all DB access on a single native handle.
112
+ // ---------------------------------------------------------------------------
113
+
114
+ class Mutex {
115
+ #queue: Array<() => void> = [];
116
+ #locked = false;
117
+
118
+ async acquire(): Promise<void> {
119
+ if (!this.#locked) {
120
+ this.#locked = true;
121
+ return;
122
+ }
123
+ return new Promise<void>((resolve) => {
124
+ this.#queue.push(resolve);
125
+ });
126
+ }
127
+
128
+ release(): void {
129
+ const next = this.#queue.shift();
130
+ if (next) {
131
+ next();
132
+ } else {
133
+ this.#locked = false;
134
+ }
135
+ }
136
+ }
137
+
108
138
  // ---------------------------------------------------------------------------
109
139
  // Kysely Dialect implementation for expo-sqlite
110
140
  // ---------------------------------------------------------------------------
@@ -136,6 +166,8 @@ class ExpoSqliteDialect implements Dialect {
136
166
  class ExpoSqliteDriver implements Driver {
137
167
  readonly #options: ExpoSqliteOptions;
138
168
  #db: ExpoSqliteDatabaseLike | undefined;
169
+ #connection: ExpoSqliteConnection | undefined;
170
+ readonly #mutex = new Mutex();
139
171
 
140
172
  constructor(options: ExpoSqliteOptions) {
141
173
  this.#options = options;
@@ -143,17 +175,24 @@ class ExpoSqliteDriver implements Driver {
143
175
 
144
176
  async init(): Promise<void> {
145
177
  this.#db = this.#resolveDatabase();
178
+ // Enable WAL mode for better concurrency (allows concurrent reads
179
+ // while writing, prevents "database is locked" errors during sync).
180
+ this.#db.runSync('PRAGMA journal_mode = WAL', []);
181
+ // Wait up to 5s for locks to clear instead of failing immediately.
182
+ this.#db.runSync('PRAGMA busy_timeout = 5000', []);
183
+ this.#connection = new ExpoSqliteConnection(this.#db);
146
184
  }
147
185
 
148
186
  async acquireConnection(): Promise<DatabaseConnection> {
149
- return new ExpoSqliteConnection(this.#db!);
187
+ await this.#mutex.acquire();
188
+ return this.#connection!;
150
189
  }
151
190
 
152
191
  async beginTransaction(
153
192
  connection: DatabaseConnection,
154
193
  _settings: TransactionSettings
155
194
  ): Promise<void> {
156
- await connection.executeQuery(CompiledQuery.raw('begin'));
195
+ await connection.executeQuery(CompiledQuery.raw('begin immediate'));
157
196
  }
158
197
 
159
198
  async commitTransaction(connection: DatabaseConnection): Promise<void> {
@@ -165,7 +204,7 @@ class ExpoSqliteDriver implements Driver {
165
204
  }
166
205
 
167
206
  async releaseConnection(_connection: DatabaseConnection): Promise<void> {
168
- // Single-connection model — nothing to release.
207
+ this.#mutex.release();
169
208
  }
170
209
 
171
210
  async destroy(): Promise<void> {