1/**
2 * Runs the worker side of the browser SQLite WASM client that stores data in
3 * OPFS.
4 *
5 * This module opens `@effect/wa-sqlite` with the OPFS access-handle VFS, then
6 * listens on a `MessagePort`-compatible port for the protocol used by
7 * `SqliteClient`. It sends a ready message, executes SQL messages, imports and
8 * exports database bytes, forwards update-hook notifications, and closes when
9 * requested. It is meant to run in a dedicated worker or a `SharedWorker`.
10 *
11 * @since 4.0.0
12 */
13/// <reference lib="webworker" />
14// oxlint-disable-next-line effect/no-import-from-barrel-package
15import * as WaSqlite from "@effect/wa-sqlite"
16import SQLiteESMFactory from "@effect/wa-sqlite/dist/wa-sqlite.mjs"
17import { AccessHandlePoolVFS } from "@effect/wa-sqlite/src/examples/AccessHandlePoolVFS.js"
18import * as Effect from "effect/Effect"
19import { classifySqliteError, SqlError } from "effect/sql/SqlError"
20import type { OpfsWorkerMessage } from "./internal/opfsWorker.ts"
21
22const classifyError = (cause: unknown, message: string, operation: string) =>
23 classifySqliteError(cause, { message, operation })
24
25/**
26 * Configuration for the SQLite OPFS worker, including the message port used for the client protocol and the OPFS database name to open.
27 *
28 * @category models
29 * @since 4.0.0
30 */
31export interface OpfsWorkerConfig {
32 readonly port: EventTarget & Pick<MessagePort, "postMessage" | "close"> & Partial<Pick<MessagePort, "start">>
33 readonly dbName: string
34}
35
36/**
37 * Runs the SQLite OPFS worker loop, opening the configured database, posting a ready message, handling query/import/export/update-hook messages, and closing when a close message is received.
38 *
39 * @category running
40 * @since 4.0.0
41 */
42export const run = (
43 options: OpfsWorkerConfig
44): Effect.Effect<void, SqlError> =>
45 Effect.gen(function*() {
46 const factory = yield* Effect.promise(() => SQLiteESMFactory())
47 const sqlite3 = WaSqlite.Factory(factory)
48 const vfs = yield* Effect.acquireRelease(
49 Effect.promise(() => AccessHandlePoolVFS.create("opfs", factory)),
50 (vfs) => Effect.promise(() => vfs.close())
51 )
52 sqlite3.vfs_register(vfs, false)
53 const db = yield* Effect.acquireRelease(
54 Effect.try({
55 try: () => sqlite3.open_v2(options.dbName, undefined, "opfs"),
56 catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to open database", "openDatabase") })
57 }),
58 (db) => Effect.sync(() => sqlite3.close(db))
59 )
60
61 return yield* Effect.callback<void>((resume) => {
62 const onMessage = (event: any) => {
63 let messageId: number
64 const message = event.data as OpfsWorkerMessage
65 try {
66 switch (message[0]) {
67 case "close": {
68 options.port.close()
69 return resume(Effect.void)
70 }
71 case "import": {
72 const [, id, data] = message
73 messageId = id
74 sqlite3.deserialize(db, "main", data, data.length, data.length, 1 | 2)
75 options.port.postMessage([id, void 0, void 0])
76 return
77 }
78 case "export": {
79 const [, id] = message
80 messageId = id
81 const data = sqlite3.serialize(db, "main")
82 options.port.postMessage([id, undefined, data], [data.buffer])
83 return
84 }
85 case "update_hook": {
86 messageId = -1
87 sqlite3.update_hook(db, (_op, _db, table, rowid) => {
88 if (!table) return
89 options.port.postMessage(["update_hook", table, Number(rowid)])
90 })
91 return
92 }
93 default: {
94 const [id, sql, params] = message
95 messageId = id
96 const results: Array<any> = []
97 const columns: Array<Array<string>> = []
98 for (const stmt of sqlite3.statements(db, sql)) {
99 let statementColumns: Array<string> | undefined
100 sqlite3.bind_collection(stmt, params as any)
101 while (sqlite3.step(stmt) === WaSqlite.SQLITE_ROW) {
102 statementColumns = statementColumns ?? sqlite3.column_names(stmt)
103 const row = sqlite3.row(stmt)
104 results.push(row)
105 columns.push(statementColumns)
106 }
107 }
108 options.port.postMessage([id, undefined, [columns, results]])
109 return
110 }
111 }
112 } catch (e: any) {
113 const message = "message" in e ? e.message : String(e)
114 const error = typeof e.code === "number" ? { message, code: e.code } : message
115 options.port.postMessage([messageId!, error, undefined])
116 }
117 }
118 options.port.addEventListener("message", onMessage)
119 options.port.start?.()
120 options.port.postMessage(["ready", undefined, undefined])
121 return Effect.sync(() => {
122 options.port.removeEventListener("message", onMessage)
123 })
124 })
125 }).pipe(Effect.scoped)