@firtoz/drizzle-sqlite-wasm 0.2.17 → 1.0.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @firtoz/drizzle-sqlite-wasm
2
2
 
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#64](https://github.com/firtoz/fullstack-toolkit/pull/64) [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f) Thanks [@firtoz](https://github.com/firtoz)! - **Breaking:** Shared TanStack collection row type is `DrizzleSqliteTableCollection` from `@firtoz/drizzle-utils` — remove `DrizzleSqliteCollection` / `DurableSqliteCollection` type exports from the wasm and durable packages and import from `@firtoz/drizzle-utils` instead. Align bridge/session row types with `PartialSyncRowShape`.
8
+
9
+ **`@firtoz/drizzle-durable-sqlite`:** `SyncableDurableObject` / `QueryableDurableObject`, Drizzle partial sync store (`createDrizzlePartialSyncStore`) with scoped `changesSince`, `getRow`, visibility and `rangeReconcile` hooks, `PartialSyncMutationHandler`, `applyDurableMutationIntents`, queued WS message handling, optional `seedInBackground`, and integration with `PartialSyncServerBridge` / `SyncServerBridge` as documented in the package.
10
+
11
+ **`@firtoz/drizzle-sqlite-wasm`:** `createSyncedSqliteCollection`, optional `workerOpenOptions` for worker `Start` / provider hooks, table sync upsert on `id` for replayed inserts, and receive-sync persist key alignment with generic sync.
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [[`afb1873`](https://github.com/firtoz/fullstack-toolkit/commit/afb187331bebb1f0231f6615c5b74989191cf30d), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f), [`556555a`](https://github.com/firtoz/fullstack-toolkit/commit/556555a2e09030a8658be8c07b5881e72be64b2f)]:
16
+ - @firtoz/collection-sync@1.0.0
17
+ - @firtoz/db-helpers@2.1.0
18
+ - @firtoz/drizzle-utils@1.2.0
19
+
3
20
  ## 0.2.17
4
21
 
5
22
  ### Patch Changes
package/README.md CHANGED
@@ -227,10 +227,8 @@ Create TanStack DB collections backed by SQLite:
227
227
 
228
228
  ```typescript
229
229
  import { createCollection } from "@tanstack/db";
230
- import {
231
- drizzleCollectionOptions,
232
- type DrizzleSqliteCollection,
233
- } from "@firtoz/drizzle-sqlite-wasm";
230
+ import type { DrizzleSqliteTableCollection } from "@firtoz/drizzle-utils";
231
+ import { drizzleCollectionOptions } from "@firtoz/drizzle-sqlite-wasm";
234
232
  import * as schema from "./schema";
235
233
 
236
234
  const collection = createCollection(
@@ -253,7 +251,7 @@ const completed = await collection.find({
253
251
  orderBy: { createdAt: "desc" },
254
252
  });
255
253
 
256
- type TodosCollection = DrizzleSqliteCollection<typeof schema.todoTable>;
254
+ type TodosCollection = DrizzleSqliteTableCollection<typeof schema.todoTable>;
257
255
 
258
256
  // Subscribe to changes
259
257
  collection.subscribe((todos) => {
@@ -261,7 +259,7 @@ collection.subscribe((todos) => {
261
259
  });
262
260
  ```
263
261
 
264
- Use `DrizzleSqliteCollection<TTable>` when you want a reusable collection type alias that keeps inferred select/insert types from your table.
262
+ Use `DrizzleSqliteTableCollection<TTable>` from `@firtoz/drizzle-utils` when you want a reusable collection type alias (shared with `@firtoz/drizzle-durable-sqlite`).
265
263
 
266
264
  ### Collection Options
267
265
 
@@ -295,11 +293,27 @@ Context provider for SQLite WASM:
295
293
  - `dbName: string` - Name of the SQLite database
296
294
  - `schema: TSchema` - Drizzle schema object
297
295
  - `migrations: DurableSqliteMigrationConfig` - Migration configuration
296
+ - `workerOpenOptions?: SqliteWasmWorkerOpenOptions` - Optional `PRAGMA synchronous` / `journal_mode` on first DB open (see hook section below)
298
297
 
299
- #### `useDrizzleSqliteDb(worker, dbName, schema, migrations)`
298
+ #### `useDrizzleSqliteDb(worker, dbName, schema, migrations, debug?, interceptor?, workerOpenOptions?)`
300
299
 
301
300
  Hook to create a Drizzle instance with Web Worker:
302
301
 
302
+ Optional **`workerOpenOptions`** sets SQLite pragmas when the worker **first** opens that `dbName` (same global worker + same `dbName` ⇒ options from the first open win until the worker is reset):
303
+
304
+ ```typescript
305
+ import type { SqliteWasmWorkerOpenOptions } from "@firtoz/drizzle-sqlite-wasm";
306
+
307
+ const open: SqliteWasmWorkerOpenOptions = {
308
+ synchronous: "NORMAL", // default worker behavior if omitted: "FULL"
309
+ journalMode: "WAL", // default if omitted: "WAL"
310
+ };
311
+
312
+ useDrizzleSqliteDb(SqliteWorker, "my-app", schema, migrations, undefined, undefined, open);
313
+ ```
314
+
315
+ `DrizzleSqliteProvider` accepts the same shape as **`workerOpenOptions`**.
316
+
303
317
  ```typescript
304
318
  function MyComponent() {
305
319
  const { drizzle, readyPromise } = useDrizzleSqliteDb(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-sqlite-wasm",
3
- "version": "0.2.17",
3
+ "version": "1.0.0",
4
4
  "description": "Drizzle SQLite WASM bindings",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -40,7 +40,7 @@
40
40
  "CHANGELOG.md"
41
41
  ],
42
42
  "scripts": {
43
- "typecheck": "tsc --noEmit -p ./tsconfig.json",
43
+ "typecheck": "tsgo --noEmit -p ./tsconfig.json",
44
44
  "lint": "biome check --write src",
45
45
  "lint:ci": "biome ci src",
46
46
  "format": "biome format src --write"
@@ -69,13 +69,14 @@
69
69
  "access": "public"
70
70
  },
71
71
  "dependencies": {
72
- "@firtoz/db-helpers": "^2.0.0",
73
- "@firtoz/drizzle-utils": "^1.1.0",
72
+ "@firtoz/collection-sync": "^1.0.0",
73
+ "@firtoz/db-helpers": "^2.1.0",
74
+ "@firtoz/drizzle-utils": "^1.2.0",
74
75
  "@firtoz/maybe-error": "^1.5.2",
75
76
  "@firtoz/worker-helper": "^1.5.1",
76
77
  "@sqlite.org/sqlite-wasm": "^3.51.2-build8",
77
- "@tanstack/db": "^0.5.33",
78
- "drizzle-orm": "^0.45.1",
78
+ "@tanstack/db": "^0.6.1",
79
+ "drizzle-orm": "^0.45.2",
79
80
  "drizzle-valibot": "^0.4.2",
80
81
  "react": "^19.2.4",
81
82
  "valibot": "^1.3.1",
@@ -1,6 +1,4 @@
1
1
  import type {
2
- Collection,
3
- InferSchemaInput,
4
2
  InferSchemaOutput,
5
3
  SyncMode,
6
4
  CollectionConfig,
@@ -80,15 +78,6 @@ export type SqliteCollectionConfig<TTable extends Table> = Omit<
80
78
  utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
81
79
  };
82
80
 
83
- export type DrizzleSqliteCollection<TTable extends TableWithRequiredFields> =
84
- Collection<
85
- InferSchemaOutput<SelectSchema<TTable>>,
86
- IdOf<TTable>,
87
- CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>,
88
- InsertToSelectSchema<TTable>,
89
- InferSchemaInput<InsertToSelectSchema<TTable>>
90
- >;
91
-
92
81
  export function sqliteCollectionOptions<
93
82
  const TDrizzle extends AnyDrizzleDatabase,
94
83
  const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
@@ -101,6 +90,9 @@ export function sqliteCollectionOptions<
101
90
 
102
91
  const table = config.drizzle?._.fullSchema[tableName] as TTable;
103
92
 
93
+ type TItem = InferSchemaOutput<SelectSchema<TTable>>;
94
+ const getKey = createGetKeyFunction<TTable>();
95
+
104
96
  const backend = createSqliteTableSyncBackend({
105
97
  drizzle: config.drizzle,
106
98
  table,
@@ -116,6 +108,7 @@ export function sqliteCollectionOptions<
116
108
  readyPromise: config.readyPromise,
117
109
  syncMode: config.syncMode,
118
110
  debug: config.debug,
111
+ getSyncPersistKey: (item: TItem) => String(getKey(item)),
119
112
  };
120
113
 
121
114
  const syncResult = createSyncFunction(baseSyncConfig, backend);
@@ -124,7 +117,7 @@ export function sqliteCollectionOptions<
124
117
 
125
118
  const collectionConfig = createCollectionConfig({
126
119
  schema,
127
- getKey: createGetKeyFunction<TTable>(),
120
+ getKey,
128
121
  syncResult,
129
122
  onInsert: config.debug
130
123
  ? async (params) => {
@@ -0,0 +1,47 @@
1
+ import {
2
+ createSyncedCollection,
3
+ type SyncableCollectionItem,
4
+ type SyncClientBridge,
5
+ type SyncClientMessage,
6
+ type WithSyncOptions,
7
+ } from "@firtoz/collection-sync";
8
+ import type { Collection } from "@tanstack/db";
9
+ import type { TableWithRequiredFields } from "@firtoz/drizzle-utils";
10
+ import type { InferSelectModel } from "drizzle-orm";
11
+ import type {
12
+ AnyDrizzleDatabase,
13
+ DrizzleSchema,
14
+ DrizzleSqliteCollectionConfig,
15
+ ValidTableNames,
16
+ } from "./sqlite-collection";
17
+ import { sqliteCollectionOptions } from "./sqlite-collection";
18
+
19
+ /**
20
+ * Like {@link createSyncedCollection} from `@firtoz/collection-sync`, but row type uses Drizzle’s
21
+ * {@link InferSelectModel} so branded columns (e.g. ids) match `$inferSelect`, not Valibot schema output.
22
+ */
23
+ export function createSyncedSqliteCollection<
24
+ const TDrizzle extends AnyDrizzleDatabase,
25
+ const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
26
+ TTable extends DrizzleSchema<TDrizzle>[TTableName] & TableWithRequiredFields,
27
+ >(
28
+ config: DrizzleSqliteCollectionConfig<TDrizzle, TTableName>,
29
+ syncOptions?: WithSyncOptions,
30
+ ): {
31
+ collection: Collection<InferSelectModel<TTable>>;
32
+ bridge: SyncClientBridge<InferSelectModel<TTable> & SyncableCollectionItem>;
33
+ setTransportSend: (send: (msg: SyncClientMessage) => void) => void;
34
+ } {
35
+ type TRow = InferSelectModel<TTable>;
36
+ type TBridgeItem = TRow & SyncableCollectionItem;
37
+ const options = sqliteCollectionOptions(config);
38
+ const { collection, bridge, setTransportSend } = createSyncedCollection(
39
+ options,
40
+ syncOptions,
41
+ );
42
+ return {
43
+ collection: collection as unknown as Collection<TRow>,
44
+ bridge: bridge as unknown as SyncClientBridge<TBridgeItem>,
45
+ setTransportSend,
46
+ };
47
+ }
@@ -13,6 +13,7 @@ import {
13
13
  isSqliteWorkerInitialized,
14
14
  } from "../worker/global-manager";
15
15
  import type { SQLInterceptor } from "../collections/sqlite-collection";
16
+ import type { SqliteWasmWorkerOpenOptions } from "../worker/sqlite-open-options";
16
17
 
17
18
  export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
18
19
  WorkerConstructor: new () => Worker,
@@ -22,6 +23,11 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
22
23
  debug?: boolean,
23
24
  /** Optional interceptor to log ALL SQL queries (including direct Drizzle queries) */
24
25
  interceptor?: SQLInterceptor,
26
+ /**
27
+ * Pragmas applied when the worker first opens this `dbName` in the session.
28
+ * Ignored if that database was already started (same global worker + dbName).
29
+ */
30
+ workerOpenOptions?: SqliteWasmWorkerOpenOptions,
25
31
  ) => {
26
32
  const resolveRef = useRef<null | (() => void)>(null);
27
33
  const rejectRef = useRef<null | ((error: unknown) => void)>(null);
@@ -63,7 +69,7 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
63
69
  "../worker/global-manager"
64
70
  );
65
71
  const manager = getSqliteWorkerManager();
66
- const instance = await manager.getDbInstance(dbName);
72
+ const instance = await manager.getDbInstance(dbName, workerOpenOptions);
67
73
 
68
74
  if (mounted) {
69
75
  sqliteClientRef.current = instance;
@@ -76,7 +82,7 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
76
82
  return () => {
77
83
  mounted = false;
78
84
  };
79
- }, [dbName, WorkerConstructor]);
85
+ }, [dbName, WorkerConstructor, workerOpenOptions]);
80
86
 
81
87
  // Store interceptor in a ref to avoid recreating drizzle on interceptor changes
82
88
  const interceptorRef = useRef(interceptor);
package/src/index.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export { drizzleSqliteWasm } from "./drizzle/direct";
2
2
  export {
3
3
  sqliteCollectionOptions as drizzleCollectionOptions,
4
- type DrizzleSqliteCollection,
5
4
  type SqliteCollectionConfig,
6
5
  type SQLOperation,
7
6
  type SQLInterceptor,
8
7
  } from "./collections/sqlite-collection";
8
+ export { createSyncedSqliteCollection } from "./collections/synced-sqlite-collection";
9
9
  export { syncableTable } from "@firtoz/drizzle-utils";
10
10
  export { makeId } from "@firtoz/drizzle-utils";
11
11
  export type {
@@ -34,5 +34,15 @@ export {
34
34
  } from "./worker/global-manager";
35
35
  export { SqliteWorkerManager, DbInstance } from "./worker/manager";
36
36
  export type { ISqliteWorkerClient } from "./worker/manager";
37
+ export type {
38
+ SqliteWasmJournalMode,
39
+ SqliteWasmSynchronousMode,
40
+ SqliteWasmWorkerOpenOptions,
41
+ } from "./worker/sqlite-open-options";
42
+ export {
43
+ SqliteWasmJournalModeSchema,
44
+ SqliteWasmSynchronousModeSchema,
45
+ SqliteWasmWorkerOpenOptionsSchema,
46
+ } from "./worker/sqlite-open-options";
37
47
  export { customSqliteMigrate } from "./migration/migrator";
38
48
  export type { DurableSqliteMigrationConfig } from "./migration/migrator";
@@ -1,5 +1,6 @@
1
1
  import { exhaustiveGuard } from "@firtoz/maybe-error";
2
2
  import { WorkerClient } from "@firtoz/worker-helper/WorkerClient";
3
+ import type { SqliteWasmWorkerOpenOptions } from "./sqlite-open-options";
3
4
  import {
4
5
  type SqliteWorkerClientMessage,
5
6
  type SqliteWorkerServerMessage,
@@ -257,7 +258,10 @@ export class SqliteWorkerManager extends WorkerClient<
257
258
  /**
258
259
  * Get or create a database instance
259
260
  */
260
- public async getDbInstance(dbName: string): Promise<DbInstance> {
261
+ public async getDbInstance(
262
+ dbName: string,
263
+ openOptions?: SqliteWasmWorkerOpenOptions,
264
+ ): Promise<DbInstance> {
261
265
  // Check if instance already exists
262
266
  let instance = this.dbInstances.get(dbName);
263
267
  if (instance) {
@@ -283,6 +287,7 @@ export class SqliteWorkerManager extends WorkerClient<
283
287
  type: SqliteWorkerClientMessageType.Start,
284
288
  requestId: startRequestId,
285
289
  dbName: dbName,
290
+ ...(openOptions !== undefined ? { openOptions } : {}),
286
291
  });
287
292
 
288
293
  return instance;
@@ -1,4 +1,5 @@
1
1
  import z from "zod";
2
+ import { SqliteWasmWorkerOpenOptionsSchema } from "./sqlite-open-options";
2
3
 
3
4
  export const RemoteCallbackIdSchema = z.string().brand("remote-callback-id");
4
5
  export type RemoteCallbackId = z.infer<typeof RemoteCallbackIdSchema>;
@@ -55,6 +56,8 @@ export const SqliteWorkerClientMessageSchema = z.discriminatedUnion("type", [
55
56
  type: z.literal(SqliteWorkerClientMessageType.Start),
56
57
  requestId: StartRequestIdSchema,
57
58
  dbName: z.string(),
59
+ /** Applied on first open of this `dbName` in the worker process. */
60
+ openOptions: SqliteWasmWorkerOpenOptionsSchema.optional(),
58
61
  }),
59
62
  RemoteCallbackRequestSchema,
60
63
  CheckpointRequestSchema,
@@ -0,0 +1,41 @@
1
+ import z from "zod";
2
+
3
+ /**
4
+ * SQLite `PRAGMA synchronous` levels (see SQLite docs). Default worker behavior
5
+ * remains `FULL` for maximum durability with OPFS; `NORMAL` is often much faster
6
+ * for interactive UIs at the cost of a narrower crash window.
7
+ */
8
+ export const SqliteWasmSynchronousModeSchema = z.enum([
9
+ "OFF",
10
+ "NORMAL",
11
+ "FULL",
12
+ "EXTRA",
13
+ ]);
14
+ export type SqliteWasmSynchronousMode = z.infer<
15
+ typeof SqliteWasmSynchronousModeSchema
16
+ >;
17
+
18
+ /**
19
+ * SQLite `PRAGMA journal_mode` values the worker will pass through as
20
+ * `PRAGMA journal_mode=<value>;` (uppercase).
21
+ */
22
+ export const SqliteWasmJournalModeSchema = z.enum([
23
+ "WAL",
24
+ "DELETE",
25
+ "TRUNCATE",
26
+ "MEMORY",
27
+ "OFF",
28
+ ]);
29
+ export type SqliteWasmJournalMode = z.infer<typeof SqliteWasmJournalModeSchema>;
30
+
31
+ /** Options applied once when the worker opens a database file (OPFS or transient). */
32
+ export const SqliteWasmWorkerOpenOptionsSchema = z
33
+ .object({
34
+ synchronous: SqliteWasmSynchronousModeSchema.optional(),
35
+ journalMode: SqliteWasmJournalModeSchema.optional(),
36
+ })
37
+ .strict();
38
+
39
+ export type SqliteWasmWorkerOpenOptions = z.infer<
40
+ typeof SqliteWasmWorkerOpenOptionsSchema
41
+ >;
@@ -12,6 +12,7 @@ import {
12
12
  } from "./schema";
13
13
  import { handleRemoteCallback } from "../drizzle/handle-callback";
14
14
  import { exhaustiveGuard } from "@firtoz/maybe-error";
15
+ import type { SqliteWasmWorkerOpenOptions } from "./sqlite-open-options";
15
16
  import type { Sqlite3Static, Database } from "../types";
16
17
 
17
18
  // Declare self as DedicatedWorkerGlobalScope for TypeScript
@@ -133,10 +134,31 @@ class SqliteWorkerHelper extends WorkerHelper<
133
134
  }
134
135
  }
135
136
 
137
+ private applyOpenPragmas(
138
+ db: Database,
139
+ openOptions?: SqliteWasmWorkerOpenOptions,
140
+ ) {
141
+ const journalMode = openOptions?.journalMode ?? "WAL";
142
+ const synchronous = openOptions?.synchronous ?? "FULL";
143
+ try {
144
+ db.exec(`PRAGMA journal_mode=${journalMode};`);
145
+ db.exec(`PRAGMA synchronous=${synchronous};`);
146
+ this.log(
147
+ "PRAGMA journal_mode=",
148
+ journalMode,
149
+ "synchronous=",
150
+ synchronous,
151
+ );
152
+ } catch (e) {
153
+ this.error("Error applying open pragmas:", e);
154
+ }
155
+ }
156
+
136
157
  private async startDatabase(
137
158
  sqlite3: Sqlite3Static,
138
159
  dbName: string,
139
160
  requestId: StartRequestId,
161
+ openOptions?: SqliteWasmWorkerOpenOptions,
140
162
  ) {
141
163
  const dbId = DbIdSchema.parse(crypto.randomUUID());
142
164
 
@@ -146,24 +168,14 @@ class SqliteWorkerHelper extends WorkerHelper<
146
168
  if ("opfs" in sqlite3) {
147
169
  db = new sqlite3.oo1.OpfsDb(dbFileName);
148
170
  this.log("OPFS database created:", db.filename);
149
-
150
- // Configure database for reliable persistence
151
- try {
152
- // Ensure WAL mode is enabled
153
- db.exec("PRAGMA journal_mode=WAL;");
154
- // Use FULL synchronous mode to ensure data is written to persistent storage
155
- // before transactions are considered complete
156
- db.exec("PRAGMA synchronous=FULL;");
157
- this.log("Database configured with WAL mode and FULL synchronous");
158
- } catch (e) {
159
- this.error("Error configuring database:", e);
160
- }
171
+ this.applyOpenPragmas(db, openOptions);
161
172
  } else {
162
173
  db = new sqlite3.oo1.DB(dbFileName, "c");
163
174
  this.log(
164
175
  "OPFS is not available, created transient database",
165
176
  db.filename,
166
177
  );
178
+ this.applyOpenPragmas(db, openOptions);
167
179
  }
168
180
 
169
181
  // Store database with initialized flag
@@ -183,7 +195,12 @@ class SqliteWorkerHelper extends WorkerHelper<
183
195
  case SqliteWorkerClientMessageType.Start:
184
196
  {
185
197
  const sqlite3 = await this.initPromise;
186
- await this.startDatabase(sqlite3, data.dbName, data.requestId);
198
+ await this.startDatabase(
199
+ sqlite3,
200
+ data.dbName,
201
+ data.requestId,
202
+ data.openOptions,
203
+ );
187
204
  }
188
205
  break;
189
206
  case SqliteWorkerClientMessageType.RemoteCallbackRequest: