Skip to content

Commit 79fbd61

Browse files
committed
feat(dialect-wasqlite-worker): add support for OPFS
1 parent f31a3b5 commit 79fbd61

File tree

14 files changed

+225
-277
lines changed

14 files changed

+225
-277
lines changed

packages/dialect-wasqlite-worker/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# WaSqlite Worker Dialect
22

3-
[kysely](https://github.com/kysely-org/kysely) dialect for [`wa-sqlite`](https://github.com/rhashimoto/wa-sqlite), execute sql in `Web Worker`, store data in IndexedDB
3+
[kysely](https://github.com/kysely-org/kysely) dialect for [`wa-sqlite`](https://github.com/rhashimoto/wa-sqlite), execute sql in `Web Worker`, store data in [OPFS](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system) or IndexedDB
44

55
## install
66

@@ -31,6 +31,10 @@ export interface WaSqliteWorkerDialectConfig {
3131
* ```
3232
*/
3333
worker?: Worker
34+
/**
35+
* prefer to store in OPFS, fallback to IndexedDB
36+
*/
37+
preferOPFS?: boolean
3438
onCreateConnection?: (connection: DatabaseConnection) => Promise<void>
3539
}
3640
```

packages/dialect-wasqlite-worker/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
"kysely": "^0.26"
4141
},
4242
"dependencies": {
43-
"zen-mitt": "^0.2.0"
43+
"zen-mitt": "^0.3.0"
4444
},
4545
"devDependencies": {
46-
"@subframe7536/wa-sqlite": "^0.9.6"
46+
"@subframe7536/sqlite-wasm": "^0.1.1"
4747
}
4848
}

packages/dialect-wasqlite-worker/src/driver.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ export class WaSqliteWorkerDriver implements Driver {
2424
}
2525
this.worker.postMessage({
2626
type: 'init',
27-
dbName: this.config.dbName,
27+
fileName: this.config.fileName,
2828
url: this.config.url,
29+
preferOPFS: this.config.preferOPFS,
2930
} satisfies MainMsg)
3031
await new Promise<void>((resolve, reject) => {
3132
this.mitt?.once('init', ({ err }) => {
@@ -118,12 +119,8 @@ class WaSqliteWorkerConnection implements DatabaseConnection {
118119

119120
async executeQuery<R>(compiledQuery: CompiledQuery<unknown>): Promise<QueryResult<R>> {
120121
const { parameters, sql, query } = compiledQuery
121-
const mode = query.kind === 'SelectQueryNode'
122-
? 'query'
123-
: query.kind === 'RawNode'
124-
? 'raw'
125-
: 'exec'
126-
this.worker.postMessage({ type: 'run', mode, sql, parameters } satisfies MainMsg)
122+
const isSelect = query.kind === 'SelectQueryNode'
123+
this.worker.postMessage({ type: 'run', isSelect, sql, parameters } satisfies MainMsg)
127124
return new Promise((resolve, reject) => {
128125
!this.mitt && reject('kysely instance has been destroyed')
129126

packages/dialect-wasqlite-worker/src/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import { SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler } from 'kysely'
33
import { WaSqliteWorkerDriver } from './driver'
44

55
export interface WaSqliteWorkerDialectConfig {
6-
dbName: string
6+
/**
7+
* db file name
8+
*/
9+
fileName: string
710
/**
811
* the URL of `wa-sqlite` WASM
912
* @example
@@ -22,6 +25,10 @@ export interface WaSqliteWorkerDialectConfig {
2225
* ```
2326
*/
2427
worker?: Worker
28+
/**
29+
* prefer to use OPFS, fallback to IndexedDB
30+
*/
31+
preferOPFS?: boolean
2532
onCreateConnection?: (connection: DatabaseConnection) => Promise<void>
2633
}
2734
export class WaSqliteWorkerDialect implements Dialect {

packages/dialect-wasqlite-worker/src/type.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
11
import type { QueryResult } from 'kysely'
22

3-
export type Promisable<T> = T | Promise<T>
4-
5-
export type RunMode = 'exec' | 'query' | 'raw'
6-
73
export type MainMsg =
84
| {
95
type: 'run'
10-
mode: RunMode
6+
isSelect: boolean
117
sql: string
128
parameters?: readonly unknown[]
139
}
@@ -17,7 +13,8 @@ export type MainMsg =
1713
| {
1814
type: 'init'
1915
url?: string
20-
dbName: string
16+
fileName: string
17+
preferOPFS?: boolean
2118
}
2219

2320
export type WorkerMsg = {

packages/dialect-wasqlite-worker/src/worker.ts

Lines changed: 25 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,29 @@
1-
import type { SQLiteAPI, SQLiteCompatibleType } from '@subframe7536/wa-sqlite'
2-
import { Factory, SQLITE_ROW } from '@subframe7536/wa-sqlite'
3-
import SQLiteAsyncESMFactory from '@subframe7536/wa-sqlite/dist/wa-sqlite-async.mjs'
4-
import { IDBBatchAtomicVFS } from '@subframe7536/wa-sqlite/src/examples/IDBBatchAtomicVFS'
5-
import type { MainMsg, RunMode, WorkerMsg } from './type'
6-
7-
let sqlite: SQLiteAPI
8-
let db: number
9-
10-
async function init(dbName: string, url?: string) {
11-
const option = url ? { locateFile: () => url } : {}
12-
const SQLiteAsyncModule = await SQLiteAsyncESMFactory(option)
13-
14-
sqlite = Factory(SQLiteAsyncModule)
15-
sqlite.vfs_register(new IDBBatchAtomicVFS(dbName, { durability: 'relaxed' }))
16-
db = await sqlite.open_v2(
17-
dbName,
18-
undefined,
19-
dbName,
20-
)
21-
}
22-
23-
async function run(sql: string, parameters?: readonly unknown[]) {
24-
const str = sqlite.str_new(db, sql)
25-
const prepared = await sqlite.prepare_v2(
26-
db,
27-
sqlite.str_value(str),
28-
)
29-
30-
if (prepared === null) {
31-
return [] as any[]
1+
import type { SQLiteDB } from '@subframe7536/sqlite-wasm'
2+
import { initSQLite, isOpfsSupported } from '@subframe7536/sqlite-wasm'
3+
import type { QueryResult } from 'kysely'
4+
import type { MainMsg, WorkerMsg } from './type'
5+
6+
let db: SQLiteDB
7+
8+
async function init(fileName: string, url?: string, preferOPFS?: boolean) {
9+
let storage
10+
if (isOpfsSupported() && preferOPFS) {
11+
storage = (await import('@subframe7536/sqlite-wasm/opfs')).useOpfsStorage(fileName, { url })
12+
} else {
13+
storage = (await import('@subframe7536/sqlite-wasm/idb')).useIdbStorage(fileName, { url })
3214
}
3315

34-
const stmt = prepared.stmt
35-
try {
36-
if (typeof parameters !== 'undefined') {
37-
sqlite.bind_collection(
38-
stmt,
39-
parameters as any[],
40-
)
41-
}
42-
43-
const rows: Record<string, SQLiteCompatibleType>[] = []
44-
let cols: string[] = []
45-
46-
while ((await sqlite.step(stmt)) === SQLITE_ROW) {
47-
cols = cols.length === 0 ? sqlite.column_names(stmt) : cols
48-
const row = sqlite.row(stmt)
49-
rows.push(cols.reduce((acc, key, i) => {
50-
acc[key] = row[i]
51-
return acc
52-
}, {} as Record<string, SQLiteCompatibleType>))
53-
}
54-
return rows
55-
} finally {
56-
await sqlite.finalize(stmt)
57-
}
16+
db = await initSQLite(storage)
5817
}
59-
60-
async function exec(mode: RunMode, sql: string, parameters?: readonly unknown[]) {
61-
const rows = await run(sql, parameters)
62-
if (mode === 'query') {
18+
async function exec(isSelect: boolean, sql: string, parameters?: readonly unknown[]): Promise<QueryResult<any>> {
19+
const rows = await db.run(sql, parameters)
20+
if (isSelect) {
6321
return { rows }
6422
}
65-
const v = await run('SELECT last_insert_rowid() as id')
66-
return {
67-
insertId: BigInt(v[0].id),
68-
numAffectedRows: BigInt(sqlite.changes(db)),
69-
rows: mode === 'raw' ? rows : [],
70-
}
71-
}
72-
73-
async function close() {
74-
await sqlite.close(db)
23+
const insertId = BigInt(await db.lastInsertRowId())
24+
const numAffectedRows = BigInt(db.changes())
25+
return { rows, insertId, numAffectedRows }
7526
}
76-
7727
onmessage = async (ev: MessageEvent<MainMsg>) => {
7828
const data = ev.data
7929
const ret: WorkerMsg = {
@@ -83,14 +33,14 @@ onmessage = async (ev: MessageEvent<MainMsg>) => {
8333
}
8434
try {
8535
switch (data.type) {
36+
case 'init':
37+
await init(data.fileName, data.url, data.preferOPFS)
38+
break
8639
case 'run':
87-
ret.data = await exec(data.mode, data.sql, data.parameters)
40+
ret.data = await exec(data.isSelect, data.sql, data.parameters)
8841
break
8942
case 'close':
90-
await close()
91-
break
92-
case 'init':
93-
await init(data.dbName, data.url)
43+
await db.close()
9444
break
9545
}
9646
} catch (error) {

packages/dialect-wasqlite-worker/tsup.config.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export default defineConfig([
1111
shims: true,
1212
dts: true,
1313
external: ['kysely'],
14-
noExternal: ['wa-sqlite'],
1514
treeshake: true,
1615
},
1716
{
@@ -21,17 +20,22 @@ export default defineConfig([
2120
clean: false,
2221
format: ['cjs', 'esm'],
2322
dts: true,
24-
noExternal: ['wa-sqlite'],
2523
treeshake: true,
2624
minify: true,
2725
plugins: [
2826
{
2927
name: 'copy',
30-
async buildEnd(this) {
31-
this.format === 'esm' && await copyFile(
32-
'./node_modules/@subframe7536/wa-sqlite/dist/wa-sqlite-async.wasm',
33-
'./dist/wa-sqlite-async.wasm',
34-
)
28+
buildEnd(this) {
29+
this.format === 'esm' && Promise.all([
30+
copyFile(
31+
'./node_modules/@subframe7536/sqlite-wasm/dist/wa-sqlite.wasm',
32+
'./dist/wa-sqlite.wasm',
33+
),
34+
copyFile(
35+
'./node_modules/@subframe7536/sqlite-wasm/dist/wa-sqlite-async.wasm',
36+
'./dist/wa-sqlite-async.wasm',
37+
),
38+
])
3539
},
3640
},
3741
],

playground/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
{
22
"scripts": {
33
"dev": "vite",
4-
"build": "vite build"
4+
"build": "vite build",
5+
"preview": "pnpm build && vite preview"
56
},
67
"dependencies": {
8+
"@subframe7536/sqlite-wasm": "^0.1.1",
79
"kysely-sqlite-builder": "workspace:*",
810
"kysely-wasm": "workspace:*",
911
"kysely-wasqlite-worker": "workspace:*",

0 commit comments

Comments
 (0)