@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,148 @@
|
|
|
1
|
+
// Adapted from https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/durable-sqlite/migrator.ts
|
|
2
|
+
// Adaptation date: 26/10/2025 20:28 commit 9cf0ed2
|
|
3
|
+
|
|
4
|
+
import { sql } from "drizzle-orm";
|
|
5
|
+
import type { MigrationMeta } from "drizzle-orm/migrator";
|
|
6
|
+
import type { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy";
|
|
7
|
+
import type { migrate as durableSqliteMigrate } from "drizzle-orm/durable-sqlite/migrator";
|
|
8
|
+
|
|
9
|
+
export type DurableSqliteMigrationConfig = Parameters<
|
|
10
|
+
typeof durableSqliteMigrate
|
|
11
|
+
>[1];
|
|
12
|
+
|
|
13
|
+
function readMigrationFiles({
|
|
14
|
+
journal,
|
|
15
|
+
migrations,
|
|
16
|
+
}: DurableSqliteMigrationConfig): MigrationMeta[] {
|
|
17
|
+
const migrationQueries: MigrationMeta[] = [];
|
|
18
|
+
|
|
19
|
+
for (const journalEntry of journal.entries) {
|
|
20
|
+
const query =
|
|
21
|
+
migrations[`m${journalEntry.idx.toString().padStart(4, "0")}`];
|
|
22
|
+
|
|
23
|
+
if (!query) {
|
|
24
|
+
throw new Error(`Missing migration: ${journalEntry.tag}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = query.split("--> statement-breakpoint").map((it) => {
|
|
29
|
+
return it;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
migrationQueries.push({
|
|
33
|
+
sql: result,
|
|
34
|
+
bps: journalEntry.breakpoints,
|
|
35
|
+
folderMillis: journalEntry.when,
|
|
36
|
+
hash: "",
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error(`Failed to parse migration: ${journalEntry.tag}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return migrationQueries;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function customSqliteMigrate<
|
|
47
|
+
TSchema extends Record<string, unknown>,
|
|
48
|
+
>(
|
|
49
|
+
db: SqliteRemoteDatabase<TSchema>,
|
|
50
|
+
config: DurableSqliteMigrationConfig,
|
|
51
|
+
debug: boolean = false,
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
if (debug) {
|
|
54
|
+
console.log(
|
|
55
|
+
`[${new Date().toISOString()}] [SqliteWasmMigrator] migrating database`,
|
|
56
|
+
config,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const migrations = readMigrationFiles(config);
|
|
61
|
+
let currentStatement: string | null = null;
|
|
62
|
+
|
|
63
|
+
let migrationCount = 0;
|
|
64
|
+
let success = true;
|
|
65
|
+
|
|
66
|
+
await db.transaction(async (tx) => {
|
|
67
|
+
try {
|
|
68
|
+
const migrationsTable = "__drizzle_migrations";
|
|
69
|
+
|
|
70
|
+
await tx.run(sql`
|
|
71
|
+
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
|
|
72
|
+
id SERIAL PRIMARY KEY,
|
|
73
|
+
hash text NOT NULL,
|
|
74
|
+
created_at numeric
|
|
75
|
+
)
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
const dbMigrations = await tx.values<[number, string, string]>(
|
|
79
|
+
sql`SELECT id, hash, created_at FROM ${sql.identifier(migrationsTable)} ORDER BY created_at DESC LIMIT 1`,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const lastDbMigration = dbMigrations[0] ?? undefined;
|
|
83
|
+
|
|
84
|
+
if (debug) {
|
|
85
|
+
console.log(
|
|
86
|
+
`[${new Date().toISOString()}] [SqliteWasmMigrator] last db migration`,
|
|
87
|
+
lastDbMigration,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const migration of migrations) {
|
|
92
|
+
if (
|
|
93
|
+
!lastDbMigration ||
|
|
94
|
+
Number(lastDbMigration[2]) < migration.folderMillis
|
|
95
|
+
) {
|
|
96
|
+
for (const stmt of migration.sql) {
|
|
97
|
+
currentStatement = stmt;
|
|
98
|
+
if (debug) {
|
|
99
|
+
console.log(
|
|
100
|
+
`[${new Date().toISOString()}] [SqliteWasmMigrator] running migration`,
|
|
101
|
+
stmt,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
await tx.run(sql.raw(stmt));
|
|
105
|
+
currentStatement = null;
|
|
106
|
+
}
|
|
107
|
+
await tx.run(
|
|
108
|
+
sql`INSERT INTO ${sql.identifier(
|
|
109
|
+
migrationsTable,
|
|
110
|
+
)} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`,
|
|
111
|
+
);
|
|
112
|
+
migrationCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (error: unknown) {
|
|
116
|
+
const e = error instanceof Error ? error : new Error(String(error));
|
|
117
|
+
console.error("[Sqlite WASM Migrator] Database migration failed:", {
|
|
118
|
+
error: e,
|
|
119
|
+
errorMessage: e.message,
|
|
120
|
+
errorStack: e.stack,
|
|
121
|
+
migrations: Object.keys(migrations),
|
|
122
|
+
...(currentStatement && { failedStatement: currentStatement }),
|
|
123
|
+
});
|
|
124
|
+
tx.rollback();
|
|
125
|
+
success = false;
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (debug) {
|
|
131
|
+
if (!success) {
|
|
132
|
+
console.log(
|
|
133
|
+
`[${new Date().toISOString()}] [SqliteWasmMigrator] migration failed.`,
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (migrationCount > 0) {
|
|
139
|
+
console.log(
|
|
140
|
+
`[${new Date().toISOString()}] [SqliteWasmMigrator] migration completed. migrations count: ${migrationCount} migrations applied.`,
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
console.log(
|
|
144
|
+
`[${new Date().toISOString()}] [SqliteWasmMigrator] no migrations applied.`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SqliteWorkerRemoteCallbackClientMessage } from "./schema";
|
|
2
|
+
|
|
3
|
+
export interface ISqliteWorkerClient {
|
|
4
|
+
performRemoteCallback: (
|
|
5
|
+
data: Omit<SqliteWorkerRemoteCallbackClientMessage, "type" | "id" | "dbId">,
|
|
6
|
+
resolve: (value: { rows: unknown[] }) => void,
|
|
7
|
+
reject: (error: Error) => void,
|
|
8
|
+
) => void;
|
|
9
|
+
onStarted: (callback: () => void) => void;
|
|
10
|
+
terminate: () => void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { SqliteWorkerManager } from "./manager";
|
|
2
|
+
|
|
3
|
+
let globalManager: SqliteWorkerManager | null = null;
|
|
4
|
+
let initPromise: Promise<SqliteWorkerManager> | null = null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Initialize the global SQLite worker manager.
|
|
8
|
+
* Should be called once, early in your app (e.g., in entry.client.tsx).
|
|
9
|
+
* Subsequent calls return the same manager instance.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* // In entry.client.tsx
|
|
14
|
+
* import { initializeSqliteWorker } from "@firtoz/drizzle-sqlite-wasm";
|
|
15
|
+
* import SqliteWorker from "@firtoz/drizzle-sqlite-wasm/worker/sqlite.worker?worker";
|
|
16
|
+
*
|
|
17
|
+
* initializeSqliteWorker(SqliteWorker);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function initializeSqliteWorker(
|
|
21
|
+
WorkerConstructor: new () => Worker,
|
|
22
|
+
debug: boolean = false,
|
|
23
|
+
): Promise<SqliteWorkerManager> {
|
|
24
|
+
// Return existing init promise if initialization is in progress
|
|
25
|
+
if (initPromise) {
|
|
26
|
+
return initPromise;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Return resolved promise if already initialized
|
|
30
|
+
if (globalManager) {
|
|
31
|
+
return Promise.resolve(globalManager);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Start initialization
|
|
35
|
+
initPromise = (async () => {
|
|
36
|
+
const worker = new WorkerConstructor();
|
|
37
|
+
const manager = new SqliteWorkerManager(worker, debug);
|
|
38
|
+
globalManager = manager;
|
|
39
|
+
|
|
40
|
+
// Wait for the worker to actually send its Ready message
|
|
41
|
+
await manager.ready;
|
|
42
|
+
|
|
43
|
+
return manager;
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
return initPromise;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the global SQLite worker manager.
|
|
51
|
+
* Throws an error if not initialized.
|
|
52
|
+
*/
|
|
53
|
+
export function getSqliteWorkerManager(): SqliteWorkerManager {
|
|
54
|
+
if (!globalManager) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"SQLite worker manager not initialized. Call initializeSqliteWorker() first.",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return globalManager;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if the global manager has been initialized
|
|
64
|
+
*/
|
|
65
|
+
export function isSqliteWorkerInitialized(): boolean {
|
|
66
|
+
return globalManager !== null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reset the global manager (mainly for testing)
|
|
71
|
+
*/
|
|
72
|
+
export function resetSqliteWorkerManager() {
|
|
73
|
+
if (globalManager) {
|
|
74
|
+
globalManager.terminate();
|
|
75
|
+
}
|
|
76
|
+
globalManager = null;
|
|
77
|
+
initPromise = null;
|
|
78
|
+
}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { exhaustiveGuard } from "@firtoz/maybe-error";
|
|
2
|
+
import { WorkerClient } from "@firtoz/worker-helper/WorkerClient";
|
|
3
|
+
import {
|
|
4
|
+
type SqliteWorkerClientMessage,
|
|
5
|
+
type SqliteWorkerServerMessage,
|
|
6
|
+
type RemoteCallbackId,
|
|
7
|
+
type DbId,
|
|
8
|
+
type StartRequestId,
|
|
9
|
+
type CheckpointId,
|
|
10
|
+
SqliteWorkerClientMessageSchema,
|
|
11
|
+
sqliteWorkerServerMessage,
|
|
12
|
+
SqliteWorkerServerMessageType,
|
|
13
|
+
SqliteWorkerClientMessageType,
|
|
14
|
+
type SqliteWorkerRemoteCallbackClientMessage,
|
|
15
|
+
RemoteCallbackIdSchema,
|
|
16
|
+
StartRequestIdSchema,
|
|
17
|
+
CheckpointIdSchema,
|
|
18
|
+
} from "./schema";
|
|
19
|
+
|
|
20
|
+
export interface ISqliteWorkerClient {
|
|
21
|
+
performRemoteCallback: (
|
|
22
|
+
data: Omit<SqliteWorkerRemoteCallbackClientMessage, "type" | "id" | "dbId">,
|
|
23
|
+
resolve: (value: { rows: unknown[] }) => void,
|
|
24
|
+
reject: (error: Error) => void,
|
|
25
|
+
) => void;
|
|
26
|
+
checkpoint: () => Promise<void>;
|
|
27
|
+
onStarted: (callback: () => void) => void;
|
|
28
|
+
terminate: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-database instance that can perform operations on a specific database
|
|
33
|
+
*/
|
|
34
|
+
export class DbInstance implements ISqliteWorkerClient {
|
|
35
|
+
private dbId: DbId | null = null;
|
|
36
|
+
private startedCallbacks: Array<() => void> = [];
|
|
37
|
+
private isStarted = false;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly manager: SqliteWorkerManager,
|
|
41
|
+
public readonly dbName: string,
|
|
42
|
+
private readonly debug: boolean = false,
|
|
43
|
+
) {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Internal method called by manager when database is started
|
|
47
|
+
*/
|
|
48
|
+
_setStarted(dbId: DbId) {
|
|
49
|
+
this.dbId = dbId;
|
|
50
|
+
this.isStarted = true;
|
|
51
|
+
|
|
52
|
+
// Call all pending callbacks
|
|
53
|
+
for (const callback of this.startedCallbacks) {
|
|
54
|
+
callback();
|
|
55
|
+
}
|
|
56
|
+
this.startedCallbacks = [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public performRemoteCallback(
|
|
60
|
+
data: Omit<SqliteWorkerRemoteCallbackClientMessage, "type" | "id" | "dbId">,
|
|
61
|
+
resolve: (value: { rows: unknown[] }) => void,
|
|
62
|
+
reject: (error: Error) => void,
|
|
63
|
+
) {
|
|
64
|
+
if (!this.dbId) {
|
|
65
|
+
reject(
|
|
66
|
+
new Error(`Database not started - dbId is null for ${this.dbName}`),
|
|
67
|
+
);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (this.debug) {
|
|
72
|
+
console.log(
|
|
73
|
+
`[${new Date().toISOString()}] [DbInstance:${this.dbName}] performing remote callback`,
|
|
74
|
+
data,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.manager.performRemoteCallback(this.dbId, data, resolve, reject);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public checkpoint(): Promise<void> {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
if (!this.dbId) {
|
|
84
|
+
reject(
|
|
85
|
+
new Error(`Database not started - dbId is null for ${this.dbName}`),
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.debug) {
|
|
91
|
+
console.log(
|
|
92
|
+
`[${new Date().toISOString()}] [DbInstance:${this.dbName}] checkpointing database`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
this.manager.checkpoint(this.dbId, resolve, reject);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
public onStarted(callback: () => void) {
|
|
101
|
+
if (this.isStarted) {
|
|
102
|
+
// Already started, call immediately
|
|
103
|
+
callback();
|
|
104
|
+
} else {
|
|
105
|
+
this.startedCallbacks.push(callback);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public terminate() {
|
|
110
|
+
// Per-db instances don't terminate the worker
|
|
111
|
+
// That's managed by the SqliteWorkerManager
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Main worker manager that can create multiple database instances
|
|
117
|
+
*/
|
|
118
|
+
export class SqliteWorkerManager extends WorkerClient<
|
|
119
|
+
SqliteWorkerClientMessage,
|
|
120
|
+
SqliteWorkerServerMessage
|
|
121
|
+
> {
|
|
122
|
+
private readonly remoteCallbacks = new Map<
|
|
123
|
+
RemoteCallbackId,
|
|
124
|
+
{
|
|
125
|
+
resolve: (value: { rows: unknown[] }) => void;
|
|
126
|
+
reject: (error: Error) => void;
|
|
127
|
+
}
|
|
128
|
+
>();
|
|
129
|
+
|
|
130
|
+
private readonly checkpointCallbacks = new Map<
|
|
131
|
+
CheckpointId,
|
|
132
|
+
{
|
|
133
|
+
resolve: () => void;
|
|
134
|
+
reject: (error: Error) => void;
|
|
135
|
+
}
|
|
136
|
+
>();
|
|
137
|
+
|
|
138
|
+
private readyResolve?: () => void;
|
|
139
|
+
private readyReject?: (error: Error) => void;
|
|
140
|
+
private readonly readyPromise: Promise<void>;
|
|
141
|
+
private isReady = false;
|
|
142
|
+
|
|
143
|
+
private readonly dbInstances = new Map<string, DbInstance>();
|
|
144
|
+
private readonly pendingStarts = new Map<
|
|
145
|
+
StartRequestId,
|
|
146
|
+
{ dbName: string; instance: DbInstance }
|
|
147
|
+
>();
|
|
148
|
+
|
|
149
|
+
constructor(
|
|
150
|
+
worker: Worker,
|
|
151
|
+
private readonly debug: boolean = false,
|
|
152
|
+
) {
|
|
153
|
+
super({
|
|
154
|
+
worker,
|
|
155
|
+
clientSchema: SqliteWorkerClientMessageSchema,
|
|
156
|
+
serverSchema: sqliteWorkerServerMessage,
|
|
157
|
+
onMessage: (message) => {
|
|
158
|
+
this.onMessage(message);
|
|
159
|
+
},
|
|
160
|
+
onValidationError: (error, rawMessage) => {
|
|
161
|
+
console.error(error, rawMessage);
|
|
162
|
+
// Reject promises if we get validation errors before being ready
|
|
163
|
+
if (!this.isReady && this.readyReject) {
|
|
164
|
+
this.readyReject(new Error(`Validation error: ${error.message}`));
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
onError: (event) => {
|
|
168
|
+
console.error(event);
|
|
169
|
+
// Reject promises if worker errors before being ready
|
|
170
|
+
if (!this.isReady && this.readyReject) {
|
|
171
|
+
this.readyReject(
|
|
172
|
+
new Error(`Worker error: ${event.message || "Unknown error"}`),
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
179
|
+
this.readyResolve = resolve;
|
|
180
|
+
this.readyReject = reject;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Promise that resolves when the worker sends its first Ready message
|
|
186
|
+
*/
|
|
187
|
+
public get ready(): Promise<void> {
|
|
188
|
+
return this.readyPromise;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private onMessage(message: SqliteWorkerServerMessage) {
|
|
192
|
+
const { type } = message;
|
|
193
|
+
switch (type) {
|
|
194
|
+
case SqliteWorkerServerMessageType.Ready:
|
|
195
|
+
{
|
|
196
|
+
this.isReady = true;
|
|
197
|
+
this.readyResolve?.();
|
|
198
|
+
if (this.debug) {
|
|
199
|
+
console.log("[SqliteWorkerManager] ready for databases");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
case SqliteWorkerServerMessageType.Started:
|
|
204
|
+
{
|
|
205
|
+
const pending = this.pendingStarts.get(message.requestId);
|
|
206
|
+
if (pending) {
|
|
207
|
+
pending.instance._setStarted(message.dbId);
|
|
208
|
+
this.pendingStarts.delete(message.requestId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
case SqliteWorkerServerMessageType.RemoteCallbackResponse:
|
|
213
|
+
{
|
|
214
|
+
const { id, rows } = message;
|
|
215
|
+
const remoteCallback = this.remoteCallbacks.get(id);
|
|
216
|
+
if (remoteCallback) {
|
|
217
|
+
remoteCallback.resolve({ rows });
|
|
218
|
+
this.remoteCallbacks.delete(id);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
break;
|
|
222
|
+
case SqliteWorkerServerMessageType.RemoteCallbackError:
|
|
223
|
+
{
|
|
224
|
+
const { id, error } = message;
|
|
225
|
+
const remoteCallback = this.remoteCallbacks.get(id);
|
|
226
|
+
if (remoteCallback) {
|
|
227
|
+
remoteCallback.reject(new Error(error));
|
|
228
|
+
this.remoteCallbacks.delete(id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
break;
|
|
232
|
+
case SqliteWorkerServerMessageType.CheckpointComplete:
|
|
233
|
+
{
|
|
234
|
+
const { id } = message;
|
|
235
|
+
const checkpointCallback = this.checkpointCallbacks.get(id);
|
|
236
|
+
if (checkpointCallback) {
|
|
237
|
+
checkpointCallback.resolve();
|
|
238
|
+
this.checkpointCallbacks.delete(id);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
242
|
+
case SqliteWorkerServerMessageType.CheckpointError:
|
|
243
|
+
{
|
|
244
|
+
const { id, error } = message;
|
|
245
|
+
const checkpointCallback = this.checkpointCallbacks.get(id);
|
|
246
|
+
if (checkpointCallback) {
|
|
247
|
+
checkpointCallback.reject(new Error(error));
|
|
248
|
+
this.checkpointCallbacks.delete(id);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
break;
|
|
252
|
+
default:
|
|
253
|
+
return exhaustiveGuard(type);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Get or create a database instance
|
|
259
|
+
*/
|
|
260
|
+
public async getDbInstance(dbName: string): Promise<DbInstance> {
|
|
261
|
+
// Check if instance already exists
|
|
262
|
+
let instance = this.dbInstances.get(dbName);
|
|
263
|
+
if (instance) {
|
|
264
|
+
return instance;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Check again after waiting (another call might have created it)
|
|
268
|
+
instance = this.dbInstances.get(dbName);
|
|
269
|
+
if (instance) {
|
|
270
|
+
return instance;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Create new instance
|
|
274
|
+
instance = new DbInstance(this, dbName, this.debug);
|
|
275
|
+
this.dbInstances.set(dbName, instance);
|
|
276
|
+
|
|
277
|
+
// Start the database
|
|
278
|
+
|
|
279
|
+
const startRequestId = StartRequestIdSchema.parse(crypto.randomUUID());
|
|
280
|
+
this.pendingStarts.set(startRequestId, { dbName, instance });
|
|
281
|
+
|
|
282
|
+
this.send({
|
|
283
|
+
type: SqliteWorkerClientMessageType.Start,
|
|
284
|
+
requestId: startRequestId,
|
|
285
|
+
dbName: dbName,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return instance;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Internal method for db instances to perform remote callbacks
|
|
293
|
+
*/
|
|
294
|
+
public performRemoteCallback(
|
|
295
|
+
dbId: DbId,
|
|
296
|
+
data: Omit<SqliteWorkerRemoteCallbackClientMessage, "type" | "id" | "dbId">,
|
|
297
|
+
resolve: (value: { rows: unknown[] }) => void,
|
|
298
|
+
reject: (error: Error) => void,
|
|
299
|
+
) {
|
|
300
|
+
if (this.debug) {
|
|
301
|
+
console.log(
|
|
302
|
+
`[${new Date().toISOString()}] [SqliteWorkerManager] performing remote callback for dbId: ${dbId}`,
|
|
303
|
+
data,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
const id = RemoteCallbackIdSchema.parse(crypto.randomUUID());
|
|
307
|
+
this.remoteCallbacks.set(id, { resolve, reject });
|
|
308
|
+
this.send({
|
|
309
|
+
type: SqliteWorkerClientMessageType.RemoteCallbackRequest,
|
|
310
|
+
id,
|
|
311
|
+
dbId,
|
|
312
|
+
sql: data.sql,
|
|
313
|
+
params: data.params,
|
|
314
|
+
method: data.method,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Internal method for db instances to checkpoint the database
|
|
320
|
+
*/
|
|
321
|
+
public checkpoint(
|
|
322
|
+
dbId: DbId,
|
|
323
|
+
resolve: () => void,
|
|
324
|
+
reject: (error: Error) => void,
|
|
325
|
+
) {
|
|
326
|
+
if (this.debug) {
|
|
327
|
+
console.log(
|
|
328
|
+
`[${new Date().toISOString()}] [SqliteWorkerManager] checkpointing database for dbId: ${dbId}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const id = CheckpointIdSchema.parse(crypto.randomUUID());
|
|
332
|
+
this.checkpointCallbacks.set(id, { resolve, reject });
|
|
333
|
+
this.send({
|
|
334
|
+
type: SqliteWorkerClientMessageType.Checkpoint,
|
|
335
|
+
id,
|
|
336
|
+
dbId,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
|
|
3
|
+
export const RemoteCallbackIdSchema = z.string().brand("remote-callback-id");
|
|
4
|
+
export type RemoteCallbackId = z.infer<typeof RemoteCallbackIdSchema>;
|
|
5
|
+
|
|
6
|
+
export const DbIdSchema = z.string().brand("db-id");
|
|
7
|
+
export type DbId = z.infer<typeof DbIdSchema>;
|
|
8
|
+
|
|
9
|
+
export const StartRequestIdSchema = z.string().brand("start-request-id");
|
|
10
|
+
export type StartRequestId = z.infer<typeof StartRequestIdSchema>;
|
|
11
|
+
|
|
12
|
+
export const CheckpointIdSchema = z.string().brand("checkpoint-id");
|
|
13
|
+
export type CheckpointId = z.infer<typeof CheckpointIdSchema>;
|
|
14
|
+
|
|
15
|
+
export enum SqliteWorkerClientMessageType {
|
|
16
|
+
Start = "start",
|
|
17
|
+
RemoteCallbackRequest = "remote-callback-request",
|
|
18
|
+
Checkpoint = "checkpoint",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export enum SqliteWorkerServerMessageType {
|
|
22
|
+
Ready = "ready",
|
|
23
|
+
Started = "started",
|
|
24
|
+
RemoteCallbackResponse = "remote-callback-response",
|
|
25
|
+
RemoteCallbackError = "remote-callback-error",
|
|
26
|
+
CheckpointComplete = "checkpoint-complete",
|
|
27
|
+
CheckpointError = "checkpoint-error",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const RemoteCallbackRequestSchema = z.object({
|
|
31
|
+
type: z.literal(SqliteWorkerClientMessageType.RemoteCallbackRequest),
|
|
32
|
+
// AsyncRemoteCallback
|
|
33
|
+
// sql: string, params: any[], method: 'run' | 'all' | 'values' | 'get'
|
|
34
|
+
id: RemoteCallbackIdSchema,
|
|
35
|
+
dbId: DbIdSchema,
|
|
36
|
+
sql: z.string(),
|
|
37
|
+
params: z.array(z.any()),
|
|
38
|
+
method: z.enum(["run", "all", "values", "get"]),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export type SqliteWorkerRemoteCallbackClientMessage = z.infer<
|
|
42
|
+
typeof RemoteCallbackRequestSchema
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
export const CheckpointRequestSchema = z.object({
|
|
46
|
+
type: z.literal(SqliteWorkerClientMessageType.Checkpoint),
|
|
47
|
+
id: CheckpointIdSchema,
|
|
48
|
+
dbId: DbIdSchema,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type CheckpointRequest = z.infer<typeof CheckpointRequestSchema>;
|
|
52
|
+
|
|
53
|
+
export const SqliteWorkerClientMessageSchema = z.discriminatedUnion("type", [
|
|
54
|
+
z.object({
|
|
55
|
+
type: z.literal(SqliteWorkerClientMessageType.Start),
|
|
56
|
+
requestId: StartRequestIdSchema,
|
|
57
|
+
dbName: z.string(),
|
|
58
|
+
}),
|
|
59
|
+
RemoteCallbackRequestSchema,
|
|
60
|
+
CheckpointRequestSchema,
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export const RemoteCallbackResponseSchema = z.object({
|
|
64
|
+
type: z.literal(SqliteWorkerServerMessageType.RemoteCallbackResponse),
|
|
65
|
+
id: RemoteCallbackIdSchema,
|
|
66
|
+
rows: z.array(z.any()),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export const RemoteCallbackErrorServerMessageSchema = z.object({
|
|
70
|
+
type: z.literal(SqliteWorkerServerMessageType.RemoteCallbackError),
|
|
71
|
+
id: RemoteCallbackIdSchema,
|
|
72
|
+
error: z.string(),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const CheckpointCompleteSchema = z.object({
|
|
76
|
+
type: z.literal(SqliteWorkerServerMessageType.CheckpointComplete),
|
|
77
|
+
id: CheckpointIdSchema,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const CheckpointErrorSchema = z.object({
|
|
81
|
+
type: z.literal(SqliteWorkerServerMessageType.CheckpointError),
|
|
82
|
+
id: CheckpointIdSchema,
|
|
83
|
+
error: z.string(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
export const sqliteWorkerServerMessage = z.discriminatedUnion("type", [
|
|
87
|
+
z.object({
|
|
88
|
+
type: z.literal(SqliteWorkerServerMessageType.Ready),
|
|
89
|
+
}),
|
|
90
|
+
z.object({
|
|
91
|
+
type: z.literal(SqliteWorkerServerMessageType.Started),
|
|
92
|
+
requestId: StartRequestIdSchema,
|
|
93
|
+
dbId: DbIdSchema,
|
|
94
|
+
}),
|
|
95
|
+
RemoteCallbackResponseSchema,
|
|
96
|
+
RemoteCallbackErrorServerMessageSchema,
|
|
97
|
+
CheckpointCompleteSchema,
|
|
98
|
+
CheckpointErrorSchema,
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
export type SqliteWorkerClientMessage = z.infer<
|
|
102
|
+
typeof SqliteWorkerClientMessageSchema
|
|
103
|
+
>;
|
|
104
|
+
|
|
105
|
+
export type SqliteWorkerServerMessage = z.infer<
|
|
106
|
+
typeof sqliteWorkerServerMessage
|
|
107
|
+
>;
|
|
108
|
+
|
|
109
|
+
export type SqliteClientRemoteCallbackServerMessage = z.infer<
|
|
110
|
+
typeof RemoteCallbackResponseSchema
|
|
111
|
+
>;
|