@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.
- package/CHANGELOG.md +230 -0
- package/README.md +602 -0
- package/package.json +89 -0
- package/src/collections/sqlite-collection.ts +532 -0
- package/src/collections/websocket-collection.ts +271 -0
- package/src/context/useDrizzleSqlite.ts +35 -0
- package/src/drizzle/direct.ts +27 -0
- package/src/drizzle/handle-callback.ts +113 -0
- package/src/drizzle/worker.ts +24 -0
- package/src/hooks/useDrizzleSqliteDb.ts +139 -0
- package/src/index.ts +32 -0
- package/src/migration/migrator.ts +148 -0
- package/src/worker/client.ts +11 -0
- package/src/worker/global-manager.ts +78 -0
- package/src/worker/manager.ts +339 -0
- package/src/worker/schema.ts +111 -0
- package/src/worker/sqlite.worker.ts +253 -0
|
@@ -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();
|