@firtoz/drizzle-sqlite-wasm 0.1.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.
@@ -0,0 +1,253 @@
1
+ import type {
2
+ // type BindingSpec,
3
+ Database,
4
+ Sqlite3Static,
5
+ } from "@sqlite.org/sqlite-wasm";
6
+
7
+ import { WorkerHelper } from "@firtoz/worker-helper";
8
+ import {
9
+ SqliteWorkerClientMessageSchema,
10
+ SqliteWorkerClientMessageType,
11
+ sqliteWorkerServerMessage,
12
+ SqliteWorkerServerMessageType,
13
+ type SqliteWorkerClientMessage,
14
+ type SqliteWorkerServerMessage,
15
+ type StartRequestId,
16
+ DbIdSchema,
17
+ type DbId,
18
+ } from "./schema";
19
+ import { handleRemoteCallback } from "../drizzle/handle-callback";
20
+ import { exhaustiveGuard } from "@firtoz/maybe-error";
21
+
22
+ // Declare self as DedicatedWorkerGlobalScope for TypeScript
23
+ declare var self: DedicatedWorkerGlobalScope;
24
+
25
+ class SqliteWorkerHelper extends WorkerHelper<
26
+ SqliteWorkerClientMessage,
27
+ SqliteWorkerServerMessage
28
+ > {
29
+ private initPromise: Promise<Sqlite3Static>;
30
+ private databases = new Map<DbId, { db: Database; initialized: boolean }>();
31
+
32
+ constructor() {
33
+ super(self, SqliteWorkerClientMessageSchema, sqliteWorkerServerMessage, {
34
+ handleMessage: (data) => {
35
+ this._handleMessage(data);
36
+ },
37
+ handleInputValidationError: (error, originalData) => {
38
+ console.error("Input validation error", { error, originalData });
39
+ throw new Error(`Invalid input: ${error.message}`);
40
+ },
41
+ handleOutputValidationError: (error, originalData) => {
42
+ console.error("Output validation error", { error, originalData });
43
+ throw new Error(`Invalid output: ${error.message}`);
44
+ },
45
+ handleProcessingError: (error, validatedData) => {
46
+ console.error("Processing error", { error, validatedData });
47
+ throw new Error(`Processing error: ${String(error)}`);
48
+ },
49
+ });
50
+
51
+ this.initPromise = import("@sqlite.org/sqlite-wasm").then(
52
+ async ({ default: sqlite3InitModule }) => {
53
+ const result = await sqlite3InitModule({
54
+ print: this.log.bind(this),
55
+ printErr: this.error.bind(this),
56
+ });
57
+
58
+ return result;
59
+ },
60
+ );
61
+
62
+ this.send({
63
+ type: SqliteWorkerServerMessageType.Ready,
64
+ });
65
+ }
66
+
67
+ private log(...args: unknown[]) {
68
+ console.log(`[${new Date().toISOString()}]`, ...args);
69
+ }
70
+
71
+ private error(...args: unknown[]) {
72
+ console.error(`[${new Date().toISOString()}]`, ...args);
73
+ }
74
+
75
+ // Helper method to process remote callback requests
76
+ private async processRemoteCallbackRequest(
77
+ data: Extract<
78
+ SqliteWorkerClientMessage,
79
+ { type: SqliteWorkerClientMessageType.RemoteCallbackRequest }
80
+ >,
81
+ sqliteDb: Database,
82
+ ): Promise<void> {
83
+ const result = await handleRemoteCallback({
84
+ sqliteDb,
85
+ sql: data.sql,
86
+ params: data.params,
87
+ method: data.method,
88
+ });
89
+
90
+ if (result.success) {
91
+ this.send({
92
+ type: SqliteWorkerServerMessageType.RemoteCallbackResponse,
93
+ id: data.id,
94
+ rows: result.result.rows,
95
+ });
96
+ } else {
97
+ console.error("Error handling remote callback", result.error);
98
+ this.send({
99
+ type: SqliteWorkerServerMessageType.RemoteCallbackError,
100
+ id: data.id,
101
+ error: result.error,
102
+ });
103
+ }
104
+ }
105
+
106
+ // Helper method to checkpoint the database (flush WAL to main DB file)
107
+ private async processCheckpointRequest(
108
+ data: Extract<
109
+ SqliteWorkerClientMessage,
110
+ { type: SqliteWorkerClientMessageType.Checkpoint }
111
+ >,
112
+ sqliteDb: Database,
113
+ ): Promise<void> {
114
+ try {
115
+ // Execute PRAGMA wal_checkpoint(TRUNCATE) to ensure all WAL data
116
+ // is written to the main database file and the WAL is truncated.
117
+ // This ensures persistence to OPFS before page reload.
118
+ sqliteDb.exec({
119
+ sql: "PRAGMA wal_checkpoint(TRUNCATE);",
120
+ callback: () => {},
121
+ });
122
+
123
+ this.send({
124
+ type: SqliteWorkerServerMessageType.CheckpointComplete,
125
+ id: data.id,
126
+ });
127
+ } catch (e: unknown) {
128
+ const errorMsg = e instanceof Error ? e.message : String(e);
129
+ this.error("Error checkpointing database:", errorMsg);
130
+ this.send({
131
+ type: SqliteWorkerServerMessageType.CheckpointError,
132
+ id: data.id,
133
+ error: errorMsg,
134
+ });
135
+ }
136
+ }
137
+
138
+ private async startDatabase(
139
+ sqlite3: Sqlite3Static,
140
+ dbName: string,
141
+ requestId: StartRequestId,
142
+ ) {
143
+ const dbId = DbIdSchema.parse(crypto.randomUUID());
144
+
145
+ const dbFileName = `${dbName}.sqlite3`;
146
+ let db: Database;
147
+
148
+ if ("opfs" in sqlite3) {
149
+ db = new sqlite3.oo1.OpfsDb(dbFileName);
150
+ this.log("OPFS database created:", db.filename);
151
+
152
+ // Configure database for reliable persistence
153
+ try {
154
+ // Ensure WAL mode is enabled
155
+ db.exec("PRAGMA journal_mode=WAL;");
156
+ // Use FULL synchronous mode to ensure data is written to persistent storage
157
+ // before transactions are considered complete
158
+ db.exec("PRAGMA synchronous=FULL;");
159
+ this.log("Database configured with WAL mode and FULL synchronous");
160
+ } catch (e) {
161
+ this.error("Error configuring database:", e);
162
+ }
163
+ } else {
164
+ db = new sqlite3.oo1.DB(dbFileName, "c");
165
+ this.log(
166
+ "OPFS is not available, created transient database",
167
+ db.filename,
168
+ );
169
+ }
170
+
171
+ // Store database with initialized flag
172
+ this.databases.set(dbId, { db, initialized: true });
173
+
174
+ // Send Started message with dbId and requestId
175
+ this.send({
176
+ type: SqliteWorkerServerMessageType.Started,
177
+ requestId,
178
+ dbId,
179
+ });
180
+ }
181
+
182
+ private async _handleMessage(data: SqliteWorkerClientMessage) {
183
+ const { type } = data;
184
+ switch (type) {
185
+ case SqliteWorkerClientMessageType.Start:
186
+ {
187
+ const sqlite3 = await this.initPromise;
188
+ await this.startDatabase(sqlite3, data.dbName, data.requestId);
189
+ }
190
+ break;
191
+ case SqliteWorkerClientMessageType.RemoteCallbackRequest:
192
+ {
193
+ // Get the database for this request
194
+ const dbEntry = this.databases.get(data.dbId);
195
+ if (!dbEntry) {
196
+ this.error(`Database not found for dbId: ${data.dbId}`);
197
+ this.send({
198
+ type: SqliteWorkerServerMessageType.RemoteCallbackError,
199
+ id: data.id,
200
+ error: `Database not found: ${data.dbId}`,
201
+ });
202
+ return;
203
+ }
204
+
205
+ if (!dbEntry.initialized) {
206
+ this.error(`Database not initialized for dbId: ${data.dbId}`);
207
+ this.send({
208
+ type: SqliteWorkerServerMessageType.RemoteCallbackError,
209
+ id: data.id,
210
+ error: `Database not initialized: ${data.dbId}`,
211
+ });
212
+ return;
213
+ }
214
+
215
+ // Process the request with the correct database
216
+ await this.processRemoteCallbackRequest(data, dbEntry.db);
217
+ }
218
+ break;
219
+ case SqliteWorkerClientMessageType.Checkpoint:
220
+ {
221
+ // Get the database for this request
222
+ const dbEntry = this.databases.get(data.dbId);
223
+ if (!dbEntry) {
224
+ this.error(`Database not found for dbId: ${data.dbId}`);
225
+ this.send({
226
+ type: SqliteWorkerServerMessageType.CheckpointError,
227
+ id: data.id,
228
+ error: `Database not found: ${data.dbId}`,
229
+ });
230
+ return;
231
+ }
232
+
233
+ if (!dbEntry.initialized) {
234
+ this.error(`Database not initialized for dbId: ${data.dbId}`);
235
+ this.send({
236
+ type: SqliteWorkerServerMessageType.CheckpointError,
237
+ id: data.id,
238
+ error: `Database not initialized: ${data.dbId}`,
239
+ });
240
+ return;
241
+ }
242
+
243
+ // Process the checkpoint request
244
+ await this.processCheckpointRequest(data, dbEntry.db);
245
+ }
246
+ break;
247
+ default:
248
+ return exhaustiveGuard(type);
249
+ }
250
+ }
251
+ }
252
+
253
+ new SqliteWorkerHelper();