@firtoz/drizzle-sqlite-wasm 0.2.16 → 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,28 @@
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
+
20
+ ## 0.2.17
21
+
22
+ ### Patch Changes
23
+
24
+ - [`8b839f2`](https://github.com/firtoz/fullstack-toolkit/commit/8b839f2227f50409af649aab87178e039aad55dc) Thanks [@firtoz](https://github.com/firtoz)! - Export collection helper types for Drizzle-backed TanStack DB collections so users can declare collection variables with preserved select and insert inference from table schemas.
25
+
3
26
  ## 0.2.16
4
27
 
5
28
  ### Patch Changes
package/README.md CHANGED
@@ -227,7 +227,9 @@ Create TanStack DB collections backed by SQLite:
227
227
 
228
228
  ```typescript
229
229
  import { createCollection } from "@tanstack/db";
230
- import { drizzleCollectionOptions } from "@firtoz/drizzle-sqlite-wasm/drizzleCollectionOptions";
230
+ import type { DrizzleSqliteTableCollection } from "@firtoz/drizzle-utils";
231
+ import { drizzleCollectionOptions } from "@firtoz/drizzle-sqlite-wasm";
232
+ import * as schema from "./schema";
231
233
 
232
234
  const collection = createCollection(
233
235
  drizzleCollectionOptions({
@@ -249,12 +251,16 @@ const completed = await collection.find({
249
251
  orderBy: { createdAt: "desc" },
250
252
  });
251
253
 
254
+ type TodosCollection = DrizzleSqliteTableCollection<typeof schema.todoTable>;
255
+
252
256
  // Subscribe to changes
253
257
  collection.subscribe((todos) => {
254
258
  console.log("Todos updated:", todos);
255
259
  });
256
260
  ```
257
261
 
262
+ Use `DrizzleSqliteTableCollection<TTable>` from `@firtoz/drizzle-utils` when you want a reusable collection type alias (shared with `@firtoz/drizzle-durable-sqlite`).
263
+
258
264
  ### Collection Options
259
265
 
260
266
  **Config:**
@@ -287,11 +293,27 @@ Context provider for SQLite WASM:
287
293
  - `dbName: string` - Name of the SQLite database
288
294
  - `schema: TSchema` - Drizzle schema object
289
295
  - `migrations: DurableSqliteMigrationConfig` - Migration configuration
296
+ - `workerOpenOptions?: SqliteWasmWorkerOpenOptions` - Optional `PRAGMA synchronous` / `journal_mode` on first DB open (see hook section below)
290
297
 
291
- #### `useDrizzleSqliteDb(worker, dbName, schema, migrations)`
298
+ #### `useDrizzleSqliteDb(worker, dbName, schema, migrations, debug?, interceptor?, workerOpenOptions?)`
292
299
 
293
300
  Hook to create a Drizzle instance with Web Worker:
294
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
+
295
317
  ```typescript
296
318
  function MyComponent() {
297
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.16",
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",
@@ -11,6 +11,7 @@ import type {
11
11
  InsertToSelectSchema,
12
12
  TableWithRequiredFields,
13
13
  BaseSyncConfig,
14
+ IdOf,
14
15
  } from "@firtoz/drizzle-utils";
15
16
  import {
16
17
  createSyncFunction,
@@ -67,8 +68,9 @@ export type ValidTableNames<TSchema extends Record<string, unknown>> = {
67
68
  export type SqliteCollectionConfig<TTable extends Table> = Omit<
68
69
  CollectionConfig<
69
70
  InferSchemaOutput<SelectSchema<TTable>>,
70
- string,
71
- InsertToSelectSchema<TTable>
71
+ IdOf<TTable>,
72
+ InsertToSelectSchema<TTable>,
73
+ CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>
72
74
  >,
73
75
  "utils"
74
76
  > & {
@@ -88,6 +90,9 @@ export function sqliteCollectionOptions<
88
90
 
89
91
  const table = config.drizzle?._.fullSchema[tableName] as TTable;
90
92
 
93
+ type TItem = InferSchemaOutput<SelectSchema<TTable>>;
94
+ const getKey = createGetKeyFunction<TTable>();
95
+
91
96
  const backend = createSqliteTableSyncBackend({
92
97
  drizzle: config.drizzle,
93
98
  table,
@@ -103,6 +108,7 @@ export function sqliteCollectionOptions<
103
108
  readyPromise: config.readyPromise,
104
109
  syncMode: config.syncMode,
105
110
  debug: config.debug,
111
+ getSyncPersistKey: (item: TItem) => String(getKey(item)),
106
112
  };
107
113
 
108
114
  const syncResult = createSyncFunction(baseSyncConfig, backend);
@@ -111,7 +117,7 @@ export function sqliteCollectionOptions<
111
117
 
112
118
  const collectionConfig = createCollectionConfig({
113
119
  schema,
114
- getKey: createGetKeyFunction<TTable>(),
120
+ getKey,
115
121
  syncResult,
116
122
  onInsert: config.debug
117
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,9 +1,11 @@
1
1
  export { drizzleSqliteWasm } from "./drizzle/direct";
2
2
  export {
3
3
  sqliteCollectionOptions as drizzleCollectionOptions,
4
+ type SqliteCollectionConfig,
4
5
  type SQLOperation,
5
6
  type SQLInterceptor,
6
7
  } from "./collections/sqlite-collection";
8
+ export { createSyncedSqliteCollection } from "./collections/synced-sqlite-collection";
7
9
  export { syncableTable } from "@firtoz/drizzle-utils";
8
10
  export { makeId } from "@firtoz/drizzle-utils";
9
11
  export type {
@@ -32,5 +34,15 @@ export {
32
34
  } from "./worker/global-manager";
33
35
  export { SqliteWorkerManager, DbInstance } from "./worker/manager";
34
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";
35
47
  export { customSqliteMigrate } from "./migration/migrator";
36
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: