@firtoz/drizzle-sqlite-wasm 0.2.1 → 0.2.3

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,30 @@
1
1
  # @firtoz/drizzle-sqlite-wasm
2
2
 
3
+ ## 0.2.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`d97681f`](https://github.com/firtoz/fullstack-toolkit/commit/d97681f56e103d033292005d31f298b03b4fa7ca) Thanks [@firtoz](https://github.com/firtoz)! - Add comprehensive Vite configuration documentation for OPFS support. Includes required COOP/COEP headers and a custom plugin to fix the sqlite-wasm 3.51.x OPFS proxy worker module format issue ("Unexpected token 'export'" error).
8
+
9
+ - Updated dependencies [[`b0f7893`](https://github.com/firtoz/fullstack-toolkit/commit/b0f789314c4ee85d8c08466b968baad2977a2581)]:
10
+ - @firtoz/worker-helper@1.1.0
11
+
12
+ ## 0.2.2
13
+
14
+ ### Patch Changes
15
+
16
+ - [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0) Thanks [@firtoz](https://github.com/firtoz)! - Fix critical bug where debug mode prevented database operations from executing. Debug handlers now properly wrap and call the actual backend handlers instead of replacing them.
17
+
18
+ Add cursor-based and offset-based pagination support to `loadSubset` operations, enabling efficient navigation through large datasets.
19
+
20
+ Add `SQLInterceptor` support to log all SQL queries, including direct Drizzle queries, with the new `createInstrumentedDrizzle` function. This provides comprehensive query visibility for debugging and monitoring.
21
+
22
+ Add explicit return type `SqliteCollectionConfig<TTable>` to `sqliteCollectionOptions` function, improving type safety and eliminating the `any` cast at the return statement.
23
+
24
+ - Updated dependencies [[`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0), [`8abab0a`](https://github.com/firtoz/fullstack-toolkit/commit/8abab0ae7a99320a4254cb128c0fd823726e58e0)]:
25
+ - @firtoz/maybe-error@1.5.2
26
+ - @firtoz/drizzle-utils@0.3.1
27
+
3
28
  ## 0.2.1
4
29
 
5
30
  ### Patch Changes
package/README.md CHANGED
@@ -111,6 +111,79 @@ import SqliteWorker from "@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker?worke
111
111
  const { drizzle } = useDrizzleSqliteDb(SqliteWorker, "mydb", schema, migrations);
112
112
  ```
113
113
 
114
+ #### Required Vite Configuration for OPFS Support
115
+
116
+ To enable OPFS (Origin Private File System) persistence with SQLite WASM, you need to configure Vite properly. Add the following to your `vite.config.ts`:
117
+
118
+ ```typescript
119
+ import { defineConfig } from "vite";
120
+
121
+ export default defineConfig({
122
+ plugins: [
123
+ // Required: Set COOP/COEP headers for SharedArrayBuffer support
124
+ {
125
+ name: "configure-response-headers",
126
+ configureServer: (server) => {
127
+ server.middlewares.use((_req, res, next) => {
128
+ res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
129
+ res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
130
+ next();
131
+ });
132
+ },
133
+ },
134
+ // Required: Fix for sqlite-wasm OPFS proxy worker module format
135
+ {
136
+ name: "sqlite-wasm-opfs-fix",
137
+ enforce: "pre",
138
+ transform(code, id) {
139
+ // Transform worker creation calls to use module type
140
+ if (
141
+ id.includes("@sqlite.org/sqlite-wasm") &&
142
+ code.includes("new Worker")
143
+ ) {
144
+ // This fixes the "Unexpected token 'export'" error in OPFS proxy worker
145
+ let transformed = code;
146
+ const workerRegex =
147
+ /new\s+Worker\s*\(\s*(new\s+URL\s*\([^)]+,[^)]+\))\s*\)/g;
148
+ transformed = transformed.replace(
149
+ workerRegex,
150
+ "new Worker($1, { type: 'module' })",
151
+ );
152
+
153
+ if (transformed !== code) {
154
+ return {
155
+ code: transformed,
156
+ map: null,
157
+ };
158
+ }
159
+ }
160
+ },
161
+ },
162
+ ],
163
+ server: {
164
+ headers: {
165
+ "Cross-Origin-Opener-Policy": "same-origin",
166
+ "Cross-Origin-Embedder-Policy": "require-corp",
167
+ },
168
+ },
169
+ optimizeDeps: {
170
+ exclude: ["@sqlite.org/sqlite-wasm"],
171
+ },
172
+ worker: {
173
+ format: "es",
174
+ },
175
+ });
176
+ ```
177
+
178
+ **Why is this needed?**
179
+
180
+ 1. **COOP/COEP Headers**: Required for `SharedArrayBuffer` support, which OPFS needs for synchronous file operations
181
+ 2. **Worker Module Fix**: sqlite-wasm 3.51.x creates workers without specifying `{ type: 'module' }`, causing syntax errors when the OPFS proxy worker (which uses ES module syntax) is loaded
182
+ 3. **Worker Format**: Ensures all workers are treated as ES modules
183
+ 4. **Exclude from Optimization**: Prevents Vite from pre-bundling sqlite-wasm, which can break worker creation
184
+
185
+ For a complete example, see [`tests/test-playground/vite.config.ts`](../../tests/test-playground/vite.config.ts) in the repository.
186
+
114
187
  ### Webpack 5+
115
188
 
116
189
  Use `new URL()` with `import.meta.url`:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firtoz/drizzle-sqlite-wasm",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Drizzle SQLite WASM bindings",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -43,9 +43,7 @@
43
43
  "typecheck": "tsc --noEmit -p ./tsconfig.json",
44
44
  "lint": "biome check --write src",
45
45
  "lint:ci": "biome ci src",
46
- "format": "biome format src --write",
47
- "test": "bun test --pass-with-no-tests",
48
- "test:watch": "bun test --watch"
46
+ "format": "biome format src --write"
49
47
  },
50
48
  "keywords": [
51
49
  "typescript",
@@ -71,19 +69,19 @@
71
69
  "access": "public"
72
70
  },
73
71
  "dependencies": {
74
- "@firtoz/drizzle-utils": "^0.3.0",
75
- "@firtoz/maybe-error": "^1.5.1",
76
- "@firtoz/worker-helper": "^1.0.0",
77
- "@sqlite.org/sqlite-wasm": "^3.51.1-build2",
78
- "@tanstack/db": "^0.5.10",
79
- "drizzle-orm": "^0.44.7",
72
+ "@firtoz/drizzle-utils": "^0.3.1",
73
+ "@firtoz/maybe-error": "^1.5.2",
74
+ "@firtoz/worker-helper": "^1.1.0",
75
+ "@sqlite.org/sqlite-wasm": "^3.51.2-build3",
76
+ "@tanstack/db": "^0.5.20",
77
+ "drizzle-orm": "^0.45.1",
80
78
  "drizzle-valibot": "^0.4.2",
81
- "react": "^19.2.0",
79
+ "react": "^19.2.3",
82
80
  "valibot": "^1.2.0",
83
- "zod": "^4.1.13"
81
+ "zod": "^4.3.5"
84
82
  },
85
83
  "devDependencies": {
86
- "@standard-schema/spec": "^1.0.0",
87
- "@types/react": "^19.2.7"
84
+ "@standard-schema/spec": "^1.1.0",
85
+ "@types/react": "^19.2.8"
88
86
  }
89
87
  }
@@ -1,4 +1,8 @@
1
- import type { InferSchemaOutput, SyncMode } from "@tanstack/db";
1
+ import type {
2
+ InferSchemaOutput,
3
+ SyncMode,
4
+ CollectionConfig,
5
+ } from "@tanstack/db";
2
6
  import type { IR } from "@tanstack/db";
3
7
  import {
4
8
  eq,
@@ -31,6 +35,7 @@ import type {
31
35
  TableWithRequiredFields,
32
36
  BaseSyncConfig,
33
37
  SyncBackend,
38
+ CollectionUtils,
34
39
  } from "@firtoz/drizzle-utils";
35
40
  import {
36
41
  createSyncFunction,
@@ -38,6 +43,7 @@ import {
38
43
  createGetKeyFunction,
39
44
  createCollectionConfig,
40
45
  } from "@firtoz/drizzle-utils";
46
+ import type * as v from "valibot";
41
47
 
42
48
  export type AnyDrizzleDatabase = BaseSQLiteDatabase<
43
49
  "async",
@@ -102,6 +108,16 @@ export type SQLOperation =
102
108
  tableName: string;
103
109
  sql?: string;
104
110
  timestamp: number;
111
+ }
112
+ | {
113
+ /** Raw SQL query executed directly via Drizzle (not through collection) */
114
+ type: "raw-query";
115
+ sql: string;
116
+ params?: unknown[];
117
+ method: string;
118
+ rowCount: number;
119
+ context: string;
120
+ timestamp: number;
105
121
  };
106
122
 
107
123
  /**
@@ -144,6 +160,25 @@ export type ValidTableNames<TSchema extends Record<string, unknown>> = {
144
160
  [K in keyof TSchema]: TSchema[K] extends TableWithRequiredFields ? K : never;
145
161
  }[keyof TSchema];
146
162
 
163
+ /**
164
+ * Return type for sqliteCollectionOptions - configuration object for creating SQLite collections
165
+ *
166
+ * Note: The third type parameter of CollectionConfig uses `any` to maintain compatibility with
167
+ * TanStack DB's type system, which expects different schema types in different contexts.
168
+ */
169
+ export type SqliteCollectionConfig<TTable extends Table> = Omit<
170
+ CollectionConfig<
171
+ InferSchemaOutput<SelectSchema<TTable>>,
172
+ string,
173
+ // biome-ignore lint/suspicious/noExplicitAny: Required for TanStack DB type compatibility
174
+ any
175
+ >,
176
+ "utils"
177
+ > & {
178
+ schema: v.GenericSchema<unknown>;
179
+ utils: CollectionUtils<InferSchemaOutput<SelectSchema<TTable>>>;
180
+ };
181
+
147
182
  /**
148
183
  * Converts TanStack DB IR BasicExpression to Drizzle SQL expression
149
184
  *
@@ -262,7 +297,9 @@ export function sqliteCollectionOptions<
262
297
  const TDrizzle extends AnyDrizzleDatabase,
263
298
  const TTableName extends string & ValidTableNames<DrizzleSchema<TDrizzle>>,
264
299
  TTable extends DrizzleSchema<TDrizzle>[TTableName] & TableWithRequiredFields,
265
- >(config: DrizzleCollectionConfig<TDrizzle, TTableName>) {
300
+ >(
301
+ config: DrizzleCollectionConfig<TDrizzle, TTableName>,
302
+ ): SqliteCollectionConfig<TTable> {
266
303
  const tableName = config.tableName as string &
267
304
  ValidTableNames<DrizzleSchema<TDrizzle>>;
268
305
 
@@ -286,7 +323,7 @@ export function sqliteCollectionOptions<
286
323
 
287
324
  // Create backend-specific implementation
288
325
  const backend: SyncBackend<TTable> = {
289
- initialLoad: async (write) => {
326
+ initialLoad: async () => {
290
327
  const items = (await config.drizzle
291
328
  .select()
292
329
  .from(table)) as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
@@ -315,25 +352,44 @@ export function sqliteCollectionOptions<
315
352
  });
316
353
  }
317
354
 
318
- for (const item of items) {
319
- write(item);
320
- }
355
+ return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
321
356
  },
322
357
 
323
- loadSubset: async (options, write) => {
324
- // Build the query with optional where, orderBy, and limit
358
+ loadSubset: async (options) => {
359
+ // Build the query with optional where, orderBy, limit, and offset
325
360
  // Use $dynamic() to enable dynamic query building
326
361
  let query = config.drizzle.select().from(table).$dynamic();
327
362
 
328
- // Convert TanStack DB IR expressions to Drizzle expressions
363
+ // Combine where with cursor expressions if present
364
+ // The cursor.whereFrom gives us rows after the cursor position
329
365
  let hasWhere = false;
330
- if (options.where) {
331
- const drizzleWhere = convertBasicExpressionToDrizzle(
332
- options.where,
333
- table,
334
- );
335
- query = query.where(drizzleWhere);
336
- hasWhere = true;
366
+ if (options.where || options.cursor?.whereFrom) {
367
+ let drizzleWhere: SQL | undefined;
368
+
369
+ if (options.where && options.cursor?.whereFrom) {
370
+ // Combine main where with cursor expression using AND
371
+ const mainWhere = convertBasicExpressionToDrizzle(
372
+ options.where,
373
+ table,
374
+ );
375
+ const cursorWhere = convertBasicExpressionToDrizzle(
376
+ options.cursor.whereFrom,
377
+ table,
378
+ );
379
+ drizzleWhere = and(mainWhere, cursorWhere);
380
+ } else if (options.where) {
381
+ drizzleWhere = convertBasicExpressionToDrizzle(options.where, table);
382
+ } else if (options.cursor?.whereFrom) {
383
+ drizzleWhere = convertBasicExpressionToDrizzle(
384
+ options.cursor.whereFrom,
385
+ table,
386
+ );
387
+ }
388
+
389
+ if (drizzleWhere) {
390
+ query = query.where(drizzleWhere);
391
+ hasWhere = true;
392
+ }
337
393
  }
338
394
 
339
395
  if (options.orderBy) {
@@ -345,6 +401,11 @@ export function sqliteCollectionOptions<
345
401
  query = query.limit(options.limit);
346
402
  }
347
403
 
404
+ // Apply offset for offset-based pagination
405
+ if (options.offset !== undefined && options.offset > 0) {
406
+ query = query.offset(options.offset);
407
+ }
408
+
348
409
  const items = (await query) as unknown as InferSchemaOutput<
349
410
  SelectSchema<TTable>
350
411
  >[];
@@ -358,6 +419,12 @@ export function sqliteCollectionOptions<
358
419
  if (options.limit !== undefined) {
359
420
  contextParts.push(`limit ${options.limit}`);
360
421
  }
422
+ if (options.offset !== undefined && options.offset > 0) {
423
+ contextParts.push(`offset ${options.offset}`);
424
+ }
425
+ if (options.cursor) {
426
+ contextParts.push("with cursor pagination");
427
+ }
361
428
 
362
429
  if (hasWhere) {
363
430
  config.interceptor.onOperation({
@@ -393,6 +460,9 @@ export function sqliteCollectionOptions<
393
460
  if (options.limit !== undefined) {
394
461
  contextParts.push(`limit ${options.limit}`);
395
462
  }
463
+ if (options.offset !== undefined && options.offset > 0) {
464
+ contextParts.push(`offset ${options.offset}`);
465
+ }
396
466
 
397
467
  config.interceptor.onOperation({
398
468
  type: "write",
@@ -404,22 +474,16 @@ export function sqliteCollectionOptions<
404
474
  });
405
475
  }
406
476
 
407
- for (const item of items) {
408
- write(item);
409
- }
477
+ return items as unknown as InferSchemaOutput<SelectSchema<TTable>>[];
410
478
  },
411
479
 
412
- handleInsert: async (mutations) => {
480
+ handleInsert: async (items) => {
413
481
  const results: Array<InferSchemaOutput<SelectSchema<TTable>>> = [];
414
482
 
415
483
  // Queue the transaction to serialize SQLite operations
416
484
  await queueTransaction(async () => {
417
485
  await config.drizzle.transaction(async (tx) => {
418
- for (const mutation of mutations) {
419
- // TanStack DB applies schema transform (including ID default) before calling this listener
420
- // So mutation.modified already has the ID from insertSchemaWithIdDefault
421
- const itemToInsert = mutation.modified;
422
-
486
+ for (const itemToInsert of items) {
423
487
  if (config.debug) {
424
488
  console.log(
425
489
  `[${new Date().toISOString()}] insertListener inserting`,
@@ -544,21 +608,29 @@ export function sqliteCollectionOptions<
544
608
  onInsert: config.debug
545
609
  ? async (params) => {
546
610
  console.log("onInsert", params);
611
+ // Call the actual handler from syncResult (always defined in createSyncFunction)
612
+ // biome-ignore lint/style/noNonNullAssertion: onInsert is always defined in SyncFunctionResult
613
+ await syncResult.onInsert!(params);
547
614
  }
548
615
  : undefined,
549
616
  onUpdate: config.debug
550
617
  ? async (params) => {
551
618
  console.log("onUpdate", params);
619
+ // Call the actual handler from syncResult (always defined in createSyncFunction)
620
+ // biome-ignore lint/style/noNonNullAssertion: onUpdate is always defined in SyncFunctionResult
621
+ await syncResult.onUpdate!(params);
552
622
  }
553
623
  : undefined,
554
624
  onDelete: config.debug
555
625
  ? async (params) => {
556
626
  console.log("onDelete", params);
627
+ // Call the actual handler from syncResult (always defined in createSyncFunction)
628
+ // biome-ignore lint/style/noNonNullAssertion: onDelete is always defined in SyncFunctionResult
629
+ await syncResult.onDelete!(params);
557
630
  }
558
631
  : undefined,
559
632
  syncMode: config.syncMode,
560
633
  });
561
634
 
562
- // biome-ignore lint/suspicious/noExplicitAny: Collection schema type needs to be flexible
563
- return collectionConfig as any;
635
+ return collectionConfig;
564
636
  }
@@ -1,4 +1,4 @@
1
- import type { Database } from "@sqlite.org/sqlite-wasm";
1
+ import type { Database } from "../types";
2
2
  import type { DrizzleConfig } from "drizzle-orm";
3
3
  import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
4
4
  import { handleRemoteCallback } from "./handle-callback";
@@ -4,7 +4,7 @@ import {
4
4
  success,
5
5
  type MaybeError,
6
6
  } from "@firtoz/maybe-error";
7
- import type { Database } from "@sqlite.org/sqlite-wasm";
7
+ import type { Database } from "../types";
8
8
 
9
9
  export const handleRemoteCallback = async ({
10
10
  sqliteDb,
@@ -1,6 +1,10 @@
1
1
  import type { DrizzleConfig } from "drizzle-orm";
2
2
  import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
3
3
  import type { ISqliteWorkerClient } from "../worker/client";
4
+ import type {
5
+ SQLInterceptor,
6
+ SQLOperation,
7
+ } from "../collections/sqlite-collection";
4
8
 
5
9
  export const drizzleSqliteWasmWorker = <
6
10
  TSchema extends Record<string, unknown> = Record<string, never>,
@@ -22,3 +26,139 @@ export const drizzleSqliteWasmWorker = <
22
26
  });
23
27
  }, config);
24
28
  };
29
+
30
+ /**
31
+ * Creates an instrumented Drizzle instance that logs all SQL queries.
32
+ * This wraps the standard drizzleSqliteWasmWorker to intercept every query.
33
+ */
34
+ export const createInstrumentedDrizzle = <
35
+ TSchema extends Record<string, unknown> = Record<string, never>,
36
+ >(
37
+ client: ISqliteWorkerClient,
38
+ config: DrizzleConfig<TSchema> = {},
39
+ interceptor?: SQLInterceptor,
40
+ ) => {
41
+ return drizzleSqliteProxy<TSchema>(async (sql, params, method) => {
42
+ const startTime = Date.now();
43
+
44
+ const result = await new Promise<{ rows: unknown[] }>((resolve, reject) => {
45
+ client.performRemoteCallback(
46
+ {
47
+ sql,
48
+ params,
49
+ method,
50
+ },
51
+ resolve,
52
+ reject,
53
+ );
54
+ });
55
+
56
+ // Log the operation if interceptor is provided
57
+ if (interceptor?.onOperation) {
58
+ // Parse SQL to determine context
59
+ const sqlLower = sql.toLowerCase().trim();
60
+ let context = "Direct Drizzle query";
61
+
62
+ if (sqlLower.startsWith("select")) {
63
+ // Extract table name from SELECT query
64
+ const fromMatch = sql.match(/from\s+["']?(\w+)["']?/i);
65
+ const tableName = fromMatch?.[1] || "unknown";
66
+
67
+ // Check for LIMIT/OFFSET - handle both literal values and ? placeholders
68
+ // Drizzle uses parameterized queries, so we need to check for `limit ?` style
69
+ const hasLimit = /limit\s+(\d+|\?|\$\d+)/i.test(sql);
70
+ const hasOffset = /offset\s+(\d+|\?|\$\d+)/i.test(sql);
71
+ const hasOrderBy = /order\s+by/i.test(sql);
72
+
73
+ if (hasLimit || hasOffset) {
74
+ // Extract actual values from params if using placeholders
75
+ // Drizzle typically puts LIMIT as second-to-last param, OFFSET as last
76
+ let limitVal = "?";
77
+ let offsetVal = "0";
78
+
79
+ // Try to extract literal values first
80
+ const literalLimit = sql.match(/limit\s+(\d+)/i);
81
+ const literalOffset = sql.match(/offset\s+(\d+)/i);
82
+
83
+ if (literalLimit) {
84
+ limitVal = literalLimit[1];
85
+ } else if (params && params.length > 0) {
86
+ // For parameterized queries, try to infer from params
87
+ // LIMIT/OFFSET are usually at the end
88
+ if (hasLimit && hasOffset && params.length >= 2) {
89
+ limitVal = String(params[params.length - 2]);
90
+ offsetVal = String(params[params.length - 1]);
91
+ } else if (hasLimit && params.length >= 1) {
92
+ limitVal = String(params[params.length - 1]);
93
+ }
94
+ }
95
+
96
+ if (literalOffset) {
97
+ offsetVal = literalOffset[1];
98
+ }
99
+
100
+ context = `SELECT with LIMIT ${limitVal} OFFSET ${offsetVal}`;
101
+ } else if (hasOrderBy) {
102
+ context = `SELECT all from ${tableName} (ordered)`;
103
+ } else {
104
+ context = `SELECT all from ${tableName}`;
105
+ }
106
+
107
+ const operation: SQLOperation = {
108
+ type: "raw-query",
109
+ sql,
110
+ params: params as unknown[],
111
+ method,
112
+ rowCount: result.rows?.length ?? 0,
113
+ context,
114
+ timestamp: startTime,
115
+ };
116
+ interceptor.onOperation(operation);
117
+ } else if (sqlLower.startsWith("insert")) {
118
+ const intoMatch = sql.match(/into\s+["']?(\w+)["']?/i);
119
+ context = `INSERT into ${intoMatch?.[1] || "unknown"}`;
120
+
121
+ const operation: SQLOperation = {
122
+ type: "raw-query",
123
+ sql,
124
+ params: params as unknown[],
125
+ method,
126
+ rowCount: 0,
127
+ context,
128
+ timestamp: startTime,
129
+ };
130
+ interceptor.onOperation(operation);
131
+ } else if (sqlLower.startsWith("update")) {
132
+ const tableMatch = sql.match(/update\s+["']?(\w+)["']?/i);
133
+ context = `UPDATE ${tableMatch?.[1] || "unknown"}`;
134
+
135
+ const operation: SQLOperation = {
136
+ type: "raw-query",
137
+ sql,
138
+ params: params as unknown[],
139
+ method,
140
+ rowCount: 0,
141
+ context,
142
+ timestamp: startTime,
143
+ };
144
+ interceptor.onOperation(operation);
145
+ } else if (sqlLower.startsWith("delete")) {
146
+ const fromMatch = sql.match(/from\s+["']?(\w+)["']?/i);
147
+ context = `DELETE from ${fromMatch?.[1] || "unknown"}`;
148
+
149
+ const operation: SQLOperation = {
150
+ type: "raw-query",
151
+ sql,
152
+ params: params as unknown[],
153
+ method,
154
+ rowCount: 0,
155
+ context,
156
+ timestamp: startTime,
157
+ };
158
+ interceptor.onOperation(operation);
159
+ }
160
+ }
161
+
162
+ return result;
163
+ }, config);
164
+ };
@@ -3,12 +3,16 @@ import {
3
3
  customSqliteMigrate,
4
4
  type DurableSqliteMigrationConfig,
5
5
  } from "@firtoz/drizzle-sqlite-wasm/sqlite-wasm-migrator";
6
- import { drizzleSqliteWasmWorker } from "@firtoz/drizzle-sqlite-wasm/drizzle-sqlite-wasm-worker";
6
+ import {
7
+ drizzleSqliteWasmWorker,
8
+ createInstrumentedDrizzle,
9
+ } from "@firtoz/drizzle-sqlite-wasm/drizzle-sqlite-wasm-worker";
7
10
  import type { ISqliteWorkerClient } from "../worker/manager";
8
11
  import {
9
12
  initializeSqliteWorker,
10
13
  isSqliteWorkerInitialized,
11
14
  } from "../worker/global-manager";
15
+ import type { SQLInterceptor } from "../collections/sqlite-collection";
12
16
 
13
17
  export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
14
18
  WorkerConstructor: new () => Worker,
@@ -16,6 +20,8 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
16
20
  schema: TSchema,
17
21
  migrations: DurableSqliteMigrationConfig,
18
22
  debug?: boolean,
23
+ /** Optional interceptor to log ALL SQL queries (including direct Drizzle queries) */
24
+ interceptor?: SQLInterceptor,
19
25
  ) => {
20
26
  const resolveRef = useRef<null | (() => void)>(null);
21
27
  const rejectRef = useRef<null | ((error: unknown) => void)>(null);
@@ -72,45 +78,66 @@ export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
72
78
  };
73
79
  }, [dbName, WorkerConstructor]);
74
80
 
81
+ // Store interceptor in a ref to avoid recreating drizzle on interceptor changes
82
+ const interceptorRef = useRef(interceptor);
83
+ interceptorRef.current = interceptor;
84
+
75
85
  // Create drizzle instance with a callback-based approach that waits for the client
86
+ // Use instrumented version if interceptor is provided to log ALL queries
76
87
  const drizzle = useMemo(() => {
77
88
  if (debug) {
78
89
  console.log(`[DEBUG] ${dbName} - creating drizzle proxy wrapper`);
79
90
  }
80
- return drizzleSqliteWasmWorker<TSchema>(
81
- {
82
- performRemoteCallback: (data, resolve, reject) => {
83
- const client = sqliteClientRef.current;
84
- if (!client) {
85
- console.error(
86
- `[DEBUG] ${dbName} - performRemoteCallback called but no sqliteClient yet`,
87
- );
88
- reject(
89
- new Error(
90
- `Database ${dbName} not ready yet - still initializing`,
91
- ),
92
- );
93
- return;
94
- }
95
- client.performRemoteCallback(data, resolve, reject);
96
- },
97
- onStarted: (callback) => {
98
- const client = sqliteClientRef.current;
99
- if (!client) {
100
- console.warn(
101
- `[DEBUG] ${dbName} - onStarted called but no sqliteClient yet`,
102
- );
103
- return;
104
- }
105
- client.onStarted(callback);
106
- },
107
- terminate: () => {
108
- sqliteClientRef.current?.terminate();
109
- },
91
+
92
+ const client: ISqliteWorkerClient = {
93
+ performRemoteCallback: (data, resolve, reject) => {
94
+ const actualClient = sqliteClientRef.current;
95
+ if (!actualClient) {
96
+ console.error(
97
+ `[DEBUG] ${dbName} - performRemoteCallback called but no sqliteClient yet`,
98
+ );
99
+ reject(
100
+ new Error(`Database ${dbName} not ready yet - still initializing`),
101
+ );
102
+ return;
103
+ }
104
+ actualClient.performRemoteCallback(data, resolve, reject);
105
+ },
106
+ onStarted: (callback) => {
107
+ const actualClient = sqliteClientRef.current;
108
+ if (!actualClient) {
109
+ console.warn(
110
+ `[DEBUG] ${dbName} - onStarted called but no sqliteClient yet`,
111
+ );
112
+ return;
113
+ }
114
+ actualClient.onStarted(callback);
110
115
  },
111
- { schema },
112
- );
113
- }, [schema, dbName]); // Using ref for sqliteClient to avoid recreating drizzle
116
+ terminate: () => {
117
+ sqliteClientRef.current?.terminate();
118
+ },
119
+ checkpoint: () => {
120
+ return sqliteClientRef.current?.checkpoint() ?? Promise.resolve();
121
+ },
122
+ };
123
+
124
+ // Use instrumented version if interceptor is provided
125
+ // Use a wrapper that accesses the ref so interceptor changes don't recreate drizzle
126
+ const interceptorWrapper: SQLInterceptor = {
127
+ onOperation: (op) => interceptorRef.current?.onOperation?.(op),
128
+ };
129
+
130
+ // Always use instrumented if initial interceptor was provided
131
+ if (interceptor) {
132
+ return createInstrumentedDrizzle<TSchema>(
133
+ client,
134
+ { schema },
135
+ interceptorWrapper,
136
+ );
137
+ }
138
+
139
+ return drizzleSqliteWasmWorker<TSchema>(client, { schema });
140
+ }, [schema, dbName, !!interceptor]); // Only recreate if interceptor presence changes, not on every render
114
141
 
115
142
  useEffect(() => {
116
143
  if (!sqliteClient) {
package/src/types.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type init from "@sqlite.org/sqlite-wasm";
2
+
3
+ export type Sqlite3Static = Awaited<ReturnType<typeof init>>;
4
+ export type Database = InstanceType<Sqlite3Static["oo1"]["DB"]>;
@@ -1,9 +1,3 @@
1
- import type {
2
- // type BindingSpec,
3
- Database,
4
- Sqlite3Static,
5
- } from "@sqlite.org/sqlite-wasm";
6
-
7
1
  import { WorkerHelper } from "@firtoz/worker-helper";
8
2
  import {
9
3
  SqliteWorkerClientMessageSchema,
@@ -18,6 +12,7 @@ import {
18
12
  } from "./schema";
19
13
  import { handleRemoteCallback } from "../drizzle/handle-callback";
20
14
  import { exhaustiveGuard } from "@firtoz/maybe-error";
15
+ import type { Sqlite3Static, Database } from "../types";
21
16
 
22
17
  // Declare self as DedicatedWorkerGlobalScope for TypeScript
23
18
  declare var self: DedicatedWorkerGlobalScope;
@@ -27,7 +22,13 @@ class SqliteWorkerHelper extends WorkerHelper<
27
22
  SqliteWorkerServerMessage
28
23
  > {
29
24
  private initPromise: Promise<Sqlite3Static>;
30
- private databases = new Map<DbId, { db: Database; initialized: boolean }>();
25
+ private databases = new Map<
26
+ DbId,
27
+ {
28
+ db: Database;
29
+ initialized: boolean;
30
+ }
31
+ >();
31
32
 
32
33
  constructor() {
33
34
  super(self, SqliteWorkerClientMessageSchema, sqliteWorkerServerMessage, {
@@ -50,10 +51,7 @@ class SqliteWorkerHelper extends WorkerHelper<
50
51
 
51
52
  this.initPromise = import("@sqlite.org/sqlite-wasm").then(
52
53
  async ({ default: sqlite3InitModule }) => {
53
- const result = await sqlite3InitModule({
54
- print: this.log.bind(this),
55
- printErr: this.error.bind(this),
56
- });
54
+ const result = await sqlite3InitModule();
57
55
 
58
56
  return result;
59
57
  },