@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,271 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CollectionConfig,
|
|
3
|
+
SyncConfig,
|
|
4
|
+
InsertMutationFnParams,
|
|
5
|
+
UpdateMutationFnParams,
|
|
6
|
+
DeleteMutationFnParams,
|
|
7
|
+
UtilsRecord,
|
|
8
|
+
BaseCollectionConfig,
|
|
9
|
+
InferSchemaOutput,
|
|
10
|
+
InferSchemaInput,
|
|
11
|
+
} from "@tanstack/db";
|
|
12
|
+
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
13
|
+
|
|
14
|
+
interface WebSocketMessage<T> {
|
|
15
|
+
type: "insert" | "update" | "delete" | "sync" | "transaction" | "ack";
|
|
16
|
+
data?: T | T[];
|
|
17
|
+
mutations?: Array<{
|
|
18
|
+
type: "insert" | "update" | "delete";
|
|
19
|
+
data: T;
|
|
20
|
+
id?: string;
|
|
21
|
+
}>;
|
|
22
|
+
transactionId?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface WebSocketCollectionConfig<TSchema extends StandardSchemaV1>
|
|
27
|
+
extends Omit<
|
|
28
|
+
BaseCollectionConfig<InferSchemaOutput<TSchema>, string | number, TSchema>,
|
|
29
|
+
"onInsert" | "onUpdate" | "onDelete" | "sync" | "schema"
|
|
30
|
+
> {
|
|
31
|
+
url: string;
|
|
32
|
+
reconnectInterval?: number;
|
|
33
|
+
schema: TSchema;
|
|
34
|
+
|
|
35
|
+
// Note: onInsert/onUpdate/onDelete are handled by the WebSocket connection
|
|
36
|
+
// Users don't provide these handlers
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface WebSocketUtils extends UtilsRecord {
|
|
40
|
+
reconnect: () => void;
|
|
41
|
+
getConnectionState: () => "connected" | "disconnected" | "connecting";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function webSocketCollectionOptions<TSchema extends StandardSchemaV1>(
|
|
45
|
+
config: WebSocketCollectionConfig<TSchema>,
|
|
46
|
+
): CollectionConfig<InferSchemaOutput<TSchema>, string | number, TSchema> & {
|
|
47
|
+
utils: WebSocketUtils;
|
|
48
|
+
schema: TSchema;
|
|
49
|
+
} {
|
|
50
|
+
type TItem = InferSchemaOutput<TSchema>;
|
|
51
|
+
|
|
52
|
+
let ws: WebSocket | null = null;
|
|
53
|
+
let reconnectTimer: NodeJS.Timeout | null = null;
|
|
54
|
+
let connectionState: "connected" | "disconnected" | "connecting" =
|
|
55
|
+
"disconnected";
|
|
56
|
+
|
|
57
|
+
// Track pending transactions awaiting acknowledgment
|
|
58
|
+
const pendingTransactions = new Map<
|
|
59
|
+
string,
|
|
60
|
+
{
|
|
61
|
+
resolve: () => void;
|
|
62
|
+
reject: (error: Error) => void;
|
|
63
|
+
timeout: NodeJS.Timeout;
|
|
64
|
+
}
|
|
65
|
+
>();
|
|
66
|
+
|
|
67
|
+
const handlers: {
|
|
68
|
+
onOpen?: () => void;
|
|
69
|
+
onMessage?: (event: MessageEvent) => void;
|
|
70
|
+
onError?: (error: Event) => void;
|
|
71
|
+
onClose?: () => void;
|
|
72
|
+
} = {};
|
|
73
|
+
|
|
74
|
+
const connect = () => {
|
|
75
|
+
connectionState = "connecting";
|
|
76
|
+
ws = new WebSocket(config.url);
|
|
77
|
+
|
|
78
|
+
ws.onopen = () => {
|
|
79
|
+
handlers.onOpen?.();
|
|
80
|
+
};
|
|
81
|
+
ws.onmessage = (event) => {
|
|
82
|
+
handlers.onMessage?.(event);
|
|
83
|
+
};
|
|
84
|
+
ws.onerror = (error) => {
|
|
85
|
+
handlers.onError?.(error);
|
|
86
|
+
};
|
|
87
|
+
ws.onclose = () => {
|
|
88
|
+
handlers.onClose?.();
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sync: SyncConfig<TItem>["sync"] = (params) => {
|
|
93
|
+
const { begin, write, commit, markReady } = params;
|
|
94
|
+
|
|
95
|
+
handlers.onOpen = () => {
|
|
96
|
+
if (!ws) return;
|
|
97
|
+
|
|
98
|
+
connectionState = "connected";
|
|
99
|
+
// Request initial sync
|
|
100
|
+
ws.send(JSON.stringify({ type: "sync" }));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
handlers.onMessage = (event) => {
|
|
104
|
+
if (!ws) return;
|
|
105
|
+
|
|
106
|
+
const message: WebSocketMessage<TItem> = JSON.parse(event.data);
|
|
107
|
+
|
|
108
|
+
switch (message.type) {
|
|
109
|
+
case "sync":
|
|
110
|
+
// Initial sync with array of items
|
|
111
|
+
begin();
|
|
112
|
+
if (Array.isArray(message.data)) {
|
|
113
|
+
for (const item of message.data) {
|
|
114
|
+
write({ type: "insert", value: item });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
commit();
|
|
118
|
+
markReady();
|
|
119
|
+
break;
|
|
120
|
+
|
|
121
|
+
case "insert":
|
|
122
|
+
case "update":
|
|
123
|
+
case "delete":
|
|
124
|
+
// Real-time updates from other clients
|
|
125
|
+
begin();
|
|
126
|
+
write({
|
|
127
|
+
type: message.type,
|
|
128
|
+
value: message.data as InferSchemaInput<TSchema>,
|
|
129
|
+
});
|
|
130
|
+
commit();
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case "ack":
|
|
134
|
+
// Server acknowledged our transaction
|
|
135
|
+
if (message.transactionId) {
|
|
136
|
+
const pending = pendingTransactions.get(message.transactionId);
|
|
137
|
+
if (pending) {
|
|
138
|
+
clearTimeout(pending.timeout);
|
|
139
|
+
pendingTransactions.delete(message.transactionId);
|
|
140
|
+
pending.resolve();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case "transaction":
|
|
146
|
+
// Server sending back the actual data after processing our transaction
|
|
147
|
+
if (message.mutations) {
|
|
148
|
+
begin();
|
|
149
|
+
for (const mutation of message.mutations) {
|
|
150
|
+
write({
|
|
151
|
+
type: mutation.type,
|
|
152
|
+
value: mutation.data,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
commit();
|
|
156
|
+
}
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
handlers.onError = (error) => {
|
|
162
|
+
console.error("WebSocket error:", error);
|
|
163
|
+
connectionState = "disconnected";
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
handlers.onClose = () => {
|
|
167
|
+
connectionState = "disconnected";
|
|
168
|
+
// Auto-reconnect
|
|
169
|
+
if (!reconnectTimer) {
|
|
170
|
+
reconnectTimer = setTimeout(() => {
|
|
171
|
+
reconnectTimer = null;
|
|
172
|
+
connect();
|
|
173
|
+
}, config.reconnectInterval || 5000);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// Start connection
|
|
178
|
+
connect();
|
|
179
|
+
|
|
180
|
+
// Return cleanup function
|
|
181
|
+
return () => {
|
|
182
|
+
if (reconnectTimer) {
|
|
183
|
+
clearTimeout(reconnectTimer);
|
|
184
|
+
reconnectTimer = null;
|
|
185
|
+
}
|
|
186
|
+
if (ws) {
|
|
187
|
+
ws.close();
|
|
188
|
+
ws = null;
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// Helper function to send transaction and wait for server acknowledgment
|
|
194
|
+
const sendTransaction = async (
|
|
195
|
+
params:
|
|
196
|
+
| InsertMutationFnParams<TItem>
|
|
197
|
+
| UpdateMutationFnParams<TItem>
|
|
198
|
+
| DeleteMutationFnParams<TItem>,
|
|
199
|
+
): Promise<void> => {
|
|
200
|
+
if (ws?.readyState !== WebSocket.OPEN) {
|
|
201
|
+
throw new Error("WebSocket not connected");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const transactionId = crypto.randomUUID();
|
|
205
|
+
|
|
206
|
+
// Convert all mutations in the transaction to the wire format
|
|
207
|
+
const mutations = params.transaction.mutations.map((mutation) => ({
|
|
208
|
+
type: mutation.type,
|
|
209
|
+
id: mutation.key,
|
|
210
|
+
data:
|
|
211
|
+
mutation.type === "delete"
|
|
212
|
+
? undefined
|
|
213
|
+
: mutation.type === "update"
|
|
214
|
+
? mutation.changes
|
|
215
|
+
: mutation.modified,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
// Send the entire transaction at once
|
|
219
|
+
ws.send(
|
|
220
|
+
JSON.stringify({
|
|
221
|
+
type: "transaction",
|
|
222
|
+
transactionId,
|
|
223
|
+
mutations,
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// Wait for server acknowledgment
|
|
228
|
+
return new Promise<void>((resolve, reject) => {
|
|
229
|
+
const timeout = setTimeout(() => {
|
|
230
|
+
pendingTransactions.delete(transactionId);
|
|
231
|
+
reject(new Error(`Transaction ${transactionId} timed out`));
|
|
232
|
+
}, 10000); // 10 second timeout
|
|
233
|
+
|
|
234
|
+
pendingTransactions.set(transactionId, {
|
|
235
|
+
resolve,
|
|
236
|
+
reject,
|
|
237
|
+
timeout,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// All mutation handlers use the same transaction sender
|
|
243
|
+
const onInsert = async (params: InsertMutationFnParams<TItem>) => {
|
|
244
|
+
await sendTransaction(params);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const onUpdate = async (params: UpdateMutationFnParams<TItem>) => {
|
|
248
|
+
await sendTransaction(params);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const onDelete = async (params: DeleteMutationFnParams<TItem>) => {
|
|
252
|
+
await sendTransaction(params);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
id: config.id,
|
|
257
|
+
schema: config.schema,
|
|
258
|
+
getKey: config.getKey,
|
|
259
|
+
sync: { sync },
|
|
260
|
+
onInsert,
|
|
261
|
+
onUpdate,
|
|
262
|
+
onDelete,
|
|
263
|
+
utils: {
|
|
264
|
+
reconnect: () => {
|
|
265
|
+
if (ws) ws.close();
|
|
266
|
+
connect();
|
|
267
|
+
},
|
|
268
|
+
getConnectionState: () => connectionState,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import type { DrizzleSqliteContextValue } from "./DrizzleSqliteProvider";
|
|
3
|
+
import {
|
|
4
|
+
DrizzleSqliteContext,
|
|
5
|
+
useSqliteCollection,
|
|
6
|
+
} from "./DrizzleSqliteProvider";
|
|
7
|
+
import type { ValidTableNames } from "../collections/sqlite-collection";
|
|
8
|
+
|
|
9
|
+
export type UseDrizzleSqliteReturn<TSchema extends Record<string, unknown>> = {
|
|
10
|
+
drizzle: DrizzleSqliteContextValue<TSchema>["drizzle"];
|
|
11
|
+
useCollection: <TTableName extends string & ValidTableNames<TSchema>>(
|
|
12
|
+
tableName: TTableName,
|
|
13
|
+
) => ReturnType<typeof useSqliteCollection<TSchema, TTableName>>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useDrizzleSqlite<
|
|
17
|
+
TSchema extends Record<string, unknown>,
|
|
18
|
+
>(): UseDrizzleSqliteReturn<TSchema> {
|
|
19
|
+
const context = useContext(
|
|
20
|
+
DrizzleSqliteContext,
|
|
21
|
+
) as DrizzleSqliteContextValue<TSchema> | null;
|
|
22
|
+
|
|
23
|
+
if (!context) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
"useDrizzleSqlite must be used within a DrizzleSqliteProvider",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
drizzle: context.drizzle,
|
|
31
|
+
useCollection: <TTableName extends string & ValidTableNames<TSchema>>(
|
|
32
|
+
tableName: TTableName,
|
|
33
|
+
) => useSqliteCollection(context, tableName),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Database } from "@sqlite.org/sqlite-wasm";
|
|
2
|
+
import type { DrizzleConfig } from "drizzle-orm";
|
|
3
|
+
import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
|
|
4
|
+
import { handleRemoteCallback } from "./handle-callback";
|
|
5
|
+
|
|
6
|
+
export const drizzleSqliteWasm = <
|
|
7
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
8
|
+
>(
|
|
9
|
+
sqliteDb: Database,
|
|
10
|
+
config: DrizzleConfig<TSchema> = {},
|
|
11
|
+
debug?: boolean,
|
|
12
|
+
) => {
|
|
13
|
+
return drizzleSqliteProxy<TSchema>(async (sql, params, method) => {
|
|
14
|
+
const result = await handleRemoteCallback({
|
|
15
|
+
sqliteDb,
|
|
16
|
+
sql,
|
|
17
|
+
params,
|
|
18
|
+
method,
|
|
19
|
+
debug,
|
|
20
|
+
});
|
|
21
|
+
if (result.success) {
|
|
22
|
+
return result.result;
|
|
23
|
+
}
|
|
24
|
+
// If the callback failed, throw an error for drizzle to handle
|
|
25
|
+
throw new Error(result.error);
|
|
26
|
+
}, config);
|
|
27
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {
|
|
2
|
+
exhaustiveGuard,
|
|
3
|
+
fail,
|
|
4
|
+
success,
|
|
5
|
+
type MaybeError,
|
|
6
|
+
} from "@firtoz/maybe-error";
|
|
7
|
+
import type { Database } from "@sqlite.org/sqlite-wasm";
|
|
8
|
+
|
|
9
|
+
export const handleRemoteCallback = async ({
|
|
10
|
+
sqliteDb,
|
|
11
|
+
sql,
|
|
12
|
+
params,
|
|
13
|
+
method,
|
|
14
|
+
debug: _debug = false,
|
|
15
|
+
}: {
|
|
16
|
+
sqliteDb: Database;
|
|
17
|
+
sql: string;
|
|
18
|
+
// biome-ignore lint/suspicious/noExplicitAny: This is what drizzle-orm expects.
|
|
19
|
+
params: any[];
|
|
20
|
+
method: "run" | "all" | "values" | "get";
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
}): Promise<MaybeError<{ rows: unknown[] }, string>> => {
|
|
23
|
+
switch (method) {
|
|
24
|
+
case "run": {
|
|
25
|
+
// For INSERT, UPDATE, DELETE operations
|
|
26
|
+
try {
|
|
27
|
+
sqliteDb.exec({
|
|
28
|
+
sql,
|
|
29
|
+
bind: params,
|
|
30
|
+
callback: () => {},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return success({ rows: [] });
|
|
34
|
+
} catch (e: unknown) {
|
|
35
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
36
|
+
console.error("Error executing run query:", errorMsg);
|
|
37
|
+
return fail(errorMsg);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
case "get": {
|
|
41
|
+
// For getting a single row
|
|
42
|
+
const columnNames: string[] = [];
|
|
43
|
+
let rowData: unknown[] = [];
|
|
44
|
+
let callbackReceived = false;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// Get column names and data in one go
|
|
48
|
+
sqliteDb.exec({
|
|
49
|
+
sql,
|
|
50
|
+
bind: params,
|
|
51
|
+
columnNames,
|
|
52
|
+
callback: (row) => {
|
|
53
|
+
callbackReceived = true;
|
|
54
|
+
if (Array.isArray(row)) {
|
|
55
|
+
// Store the first row's values
|
|
56
|
+
rowData = row;
|
|
57
|
+
} else {
|
|
58
|
+
// Convert object to array if needed
|
|
59
|
+
rowData = columnNames.map((col) => row[col]);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
} catch (e: unknown) {
|
|
64
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
65
|
+
console.error("Error getting row data:", errorMsg);
|
|
66
|
+
return fail(errorMsg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!callbackReceived) {
|
|
70
|
+
const errorMsg = "No callback received for get method";
|
|
71
|
+
console.error(errorMsg);
|
|
72
|
+
return fail(errorMsg);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// For get method, return a single array of values
|
|
76
|
+
return success({ rows: rowData });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
case "all":
|
|
80
|
+
case "values": {
|
|
81
|
+
// For getting multiple rows
|
|
82
|
+
const columnNames: string[] = [];
|
|
83
|
+
const rowsData: unknown[][] = [];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Get column names and data in one go
|
|
87
|
+
sqliteDb.exec({
|
|
88
|
+
sql,
|
|
89
|
+
bind: params,
|
|
90
|
+
columnNames,
|
|
91
|
+
callback: (row) => {
|
|
92
|
+
if (Array.isArray(row)) {
|
|
93
|
+
// Convert all values to strings
|
|
94
|
+
rowsData.push(row);
|
|
95
|
+
} else {
|
|
96
|
+
// Convert object to array if needed
|
|
97
|
+
rowsData.push(columnNames.map((col) => row[col]));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
} catch (e: unknown) {
|
|
102
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
103
|
+
console.error("Error getting all/values data:", errorMsg);
|
|
104
|
+
return fail(errorMsg);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// For all/values methods, return an array of arrays
|
|
108
|
+
return success({ rows: rowsData });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return exhaustiveGuard(method);
|
|
113
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { DrizzleConfig } from "drizzle-orm";
|
|
2
|
+
import { drizzle as drizzleSqliteProxy } from "drizzle-orm/sqlite-proxy";
|
|
3
|
+
import type { ISqliteWorkerClient } from "../worker/client";
|
|
4
|
+
|
|
5
|
+
export const drizzleSqliteWasmWorker = <
|
|
6
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
7
|
+
>(
|
|
8
|
+
client: ISqliteWorkerClient,
|
|
9
|
+
config: DrizzleConfig<TSchema> = {},
|
|
10
|
+
) => {
|
|
11
|
+
return drizzleSqliteProxy<TSchema>(async (sql, params, method) => {
|
|
12
|
+
return new Promise<{ rows: unknown[] }>((resolve, reject) => {
|
|
13
|
+
client.performRemoteCallback(
|
|
14
|
+
{
|
|
15
|
+
sql,
|
|
16
|
+
params,
|
|
17
|
+
method,
|
|
18
|
+
},
|
|
19
|
+
resolve,
|
|
20
|
+
reject,
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
}, config);
|
|
24
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
customSqliteMigrate,
|
|
4
|
+
type DurableSqliteMigrationConfig,
|
|
5
|
+
} from "@firtoz/drizzle-sqlite-wasm/sqlite-wasm-migrator";
|
|
6
|
+
import { drizzleSqliteWasmWorker } from "@firtoz/drizzle-sqlite-wasm/drizzle-sqlite-wasm-worker";
|
|
7
|
+
import type { ISqliteWorkerClient } from "../worker/manager";
|
|
8
|
+
import {
|
|
9
|
+
initializeSqliteWorker,
|
|
10
|
+
isSqliteWorkerInitialized,
|
|
11
|
+
} from "../worker/global-manager";
|
|
12
|
+
|
|
13
|
+
export const useDrizzleSqliteDb = <TSchema extends Record<string, unknown>>(
|
|
14
|
+
WorkerConstructor: new () => Worker,
|
|
15
|
+
dbName: string,
|
|
16
|
+
schema: TSchema,
|
|
17
|
+
migrations: DurableSqliteMigrationConfig,
|
|
18
|
+
debug?: boolean,
|
|
19
|
+
) => {
|
|
20
|
+
const resolveRef = useRef<null | (() => void)>(null);
|
|
21
|
+
const rejectRef = useRef<null | ((error: unknown) => void)>(null);
|
|
22
|
+
const [sqliteClient, setSqliteClient] = useState<ISqliteWorkerClient | null>(
|
|
23
|
+
null,
|
|
24
|
+
);
|
|
25
|
+
const sqliteClientRef = useRef<ISqliteWorkerClient | null>(null);
|
|
26
|
+
|
|
27
|
+
const readyPromise = useMemo(() => {
|
|
28
|
+
return new Promise<void>((resolve, reject) => {
|
|
29
|
+
resolveRef.current = resolve;
|
|
30
|
+
rejectRef.current = reject;
|
|
31
|
+
});
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
// Initialize the global manager and get db instance
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (typeof window === "undefined") {
|
|
37
|
+
// SSR stub
|
|
38
|
+
setSqliteClient({
|
|
39
|
+
performRemoteCallback: () => {},
|
|
40
|
+
checkpoint: () => Promise.resolve(),
|
|
41
|
+
onStarted: () => {},
|
|
42
|
+
terminate: () => {},
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let mounted = true;
|
|
48
|
+
|
|
49
|
+
const init = async () => {
|
|
50
|
+
// Initialize manager if not already initialized
|
|
51
|
+
if (!isSqliteWorkerInitialized()) {
|
|
52
|
+
await initializeSqliteWorker(WorkerConstructor);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Get manager and create db instance
|
|
56
|
+
const { getSqliteWorkerManager } = await import(
|
|
57
|
+
"../worker/global-manager"
|
|
58
|
+
);
|
|
59
|
+
const manager = getSqliteWorkerManager();
|
|
60
|
+
const instance = await manager.getDbInstance(dbName);
|
|
61
|
+
|
|
62
|
+
if (mounted) {
|
|
63
|
+
sqliteClientRef.current = instance;
|
|
64
|
+
setSqliteClient(instance);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
init();
|
|
69
|
+
|
|
70
|
+
return () => {
|
|
71
|
+
mounted = false;
|
|
72
|
+
};
|
|
73
|
+
}, [dbName, WorkerConstructor]);
|
|
74
|
+
|
|
75
|
+
// Create drizzle instance with a callback-based approach that waits for the client
|
|
76
|
+
const drizzle = useMemo(() => {
|
|
77
|
+
if (debug) {
|
|
78
|
+
console.log(`[DEBUG] ${dbName} - creating drizzle proxy wrapper`);
|
|
79
|
+
}
|
|
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
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{ schema },
|
|
112
|
+
);
|
|
113
|
+
}, [schema, dbName]); // Using ref for sqliteClient to avoid recreating drizzle
|
|
114
|
+
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!sqliteClient) {
|
|
117
|
+
if (debug) {
|
|
118
|
+
console.log(`[DEBUG] ${dbName} - waiting for sqliteClient...`);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
sqliteClient.onStarted(async () => {
|
|
124
|
+
try {
|
|
125
|
+
await customSqliteMigrate(drizzle, migrations);
|
|
126
|
+
resolveRef.current?.();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error(`Migration error for ${dbName}:`, error);
|
|
129
|
+
rejectRef.current?.(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
sqliteClient.terminate();
|
|
135
|
+
};
|
|
136
|
+
}, [sqliteClient, drizzle, migrations, dbName]);
|
|
137
|
+
|
|
138
|
+
return { drizzle, readyPromise, sqliteClient };
|
|
139
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { drizzleSqliteWasm } from "./drizzle/direct";
|
|
2
|
+
export { sqliteCollectionOptions as drizzleCollectionOptions } from "./collections/sqlite-collection";
|
|
3
|
+
export { syncableTable } from "@firtoz/drizzle-utils";
|
|
4
|
+
export { makeId } from "@firtoz/drizzle-utils";
|
|
5
|
+
export type {
|
|
6
|
+
IdOf,
|
|
7
|
+
TableId,
|
|
8
|
+
Branded,
|
|
9
|
+
SelectSchema,
|
|
10
|
+
InsertSchema,
|
|
11
|
+
} from "@firtoz/drizzle-utils";
|
|
12
|
+
export { useDrizzleSqliteDb } from "./hooks/useDrizzleSqliteDb";
|
|
13
|
+
// SQLite WASM Provider
|
|
14
|
+
export {
|
|
15
|
+
DrizzleSqliteProvider,
|
|
16
|
+
DrizzleSqliteContext,
|
|
17
|
+
useSqliteCollection,
|
|
18
|
+
} from "./context/DrizzleSqliteProvider";
|
|
19
|
+
export type { DrizzleSqliteContextValue } from "./context/DrizzleSqliteProvider";
|
|
20
|
+
export { useDrizzleSqlite } from "./context/useDrizzleSqlite";
|
|
21
|
+
export type { UseDrizzleSqliteReturn } from "./context/useDrizzleSqlite";
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
initializeSqliteWorker,
|
|
25
|
+
getSqliteWorkerManager,
|
|
26
|
+
isSqliteWorkerInitialized,
|
|
27
|
+
resetSqliteWorkerManager,
|
|
28
|
+
} from "./worker/global-manager";
|
|
29
|
+
export { SqliteWorkerManager, DbInstance } from "./worker/manager";
|
|
30
|
+
export type { ISqliteWorkerClient } from "./worker/manager";
|
|
31
|
+
export { customSqliteMigrate } from "./migration/migrator";
|
|
32
|
+
export type { DurableSqliteMigrationConfig } from "./migration/migrator";
|