hack imports of wasm due to the issues in Vite and Next.js build systems

This commit is contained in:
Nikita Sivukhin
2025-09-11 13:25:01 +04:00
parent ae3c1fc2a6
commit b086cba669
44 changed files with 25817 additions and 484 deletions

View File

@@ -0,0 +1,22 @@
import { setupMainThread } from "@tursodatabase/database-browser-common";
const __wasmUrl = new URL('./turso.wasm32-wasi.wasm', import.meta.url).href;
const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
export let MainWorker = null;
const napiModule = await setupMainThread(__wasmFile, () => {
const worker = new Worker(new URL('./worker.js', import.meta.url), {
name: 'turso-database',
type: 'module',
})
MainWorker = worker;
return worker
});
export default napiModule.exports
export const Database = napiModule.exports.Database
export const Opfs = napiModule.exports.Opfs
export const OpfsFile = napiModule.exports.OpfsFile
export const Statement = napiModule.exports.Statement
export const connect = napiModule.exports.connect
export const initThreadPool = napiModule.exports.initThreadPool

View File

@@ -0,0 +1,25 @@
import { setupMainThread } from "@tursodatabase/database-browser-common";
import { tursoWasm } from "./wasm-inline.js";
// Next (turbopack) has issues with loading wasm module: https://github.com/vercel/next.js/issues/82520
// So, we inline wasm binary in the source code in order to avoid issues with loading it from the file
const __wasmFile = await tursoWasm();
export let MainWorker = null;
const napiModule = await setupMainThread(__wasmFile, () => {
const worker = new Worker(new URL('./worker.js', import.meta.url), {
name: 'turso-database',
type: 'module',
})
MainWorker = worker;
return worker
});
export default napiModule.exports
export const Database = napiModule.exports.Database
export const Opfs = napiModule.exports.Opfs
export const OpfsFile = napiModule.exports.OpfsFile
export const Statement = napiModule.exports.Statement
export const connect = napiModule.exports.connect
export const initThreadPool = napiModule.exports.initThreadPool

View File

@@ -0,0 +1,41 @@
import { isWebWorker, setupMainThread, setupWebWorker } from "@tursodatabase/database-browser-common";
import { tursoWasm } from "./wasm-inline.js";
let napiModule = {
exports: {
Database: {} as any,
Opfs: {} as any,
OpfsFile: {} as any,
Statement: {} as any,
connect: {} as any,
initThreadPool: {} as any,
}
};
export let MainWorker = null;
if (isWebWorker()) {
setupWebWorker();
} else {
// Vite has issues with loading wasm modules and worker in dev server: https://github.com/vitejs/vite/issues/8427
// So, the mitigation for dev server only is:
// 1. inline wasm binary in the source code in order to avoid issues with loading it from the file
// 2. use same file as worker entry point
const __wasmFile = await tursoWasm();
napiModule = await setupMainThread(__wasmFile, () => {
const worker = new Worker(import.meta.url, {
name: 'turso-database',
type: 'module',
})
MainWorker = worker;
return worker
});
}
export default napiModule.exports
export const Database = napiModule.exports.Database
export const Opfs = napiModule.exports.Opfs
export const OpfsFile = napiModule.exports.OpfsFile
export const Statement = napiModule.exports.Statement
export const connect = napiModule.exports.connect
export const initThreadPool = napiModule.exports.initThreadPool

View File

@@ -1,68 +0,0 @@
import {
createOnMessage as __wasmCreateOnMessageForFsProxy,
getDefaultContext as __emnapiGetDefaultContext,
instantiateNapiModule as __emnapiInstantiateNapiModule,
WASI as __WASI,
} from '@napi-rs/wasm-runtime'
import { MainDummyImports } from "@tursodatabase/database-browser-common";
const __wasi = new __WASI({
version: 'preview1',
})
const __wasmUrl = new URL('./turso.wasm32-wasi.wasm', import.meta.url).href
const __emnapiContext = __emnapiGetDefaultContext()
const __sharedMemory = new WebAssembly.Memory({
initial: 4000,
maximum: 65536,
shared: true,
})
const __wasmFile = await fetch(__wasmUrl).then((res) => res.arrayBuffer())
export let MainWorker = null;
const {
instance: __napiInstance,
module: __wasiModule,
napiModule: __napiModule,
} = await __emnapiInstantiateNapiModule(__wasmFile, {
context: __emnapiContext,
asyncWorkPoolSize: 1,
wasi: __wasi,
onCreateWorker() {
const worker = new Worker(new URL('./worker.mjs', import.meta.url), {
type: 'module',
})
MainWorker = worker;
return worker
},
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
...MainDummyImports,
memory: __sharedMemory,
}
return importObject
},
beforeInit({ instance }) {
for (const name of Object.keys(instance.exports)) {
if (name.startsWith('__napi_register__')) {
instance.exports[name]()
}
}
},
})
export default __napiModule.exports
export const Database = __napiModule.exports.Database
export const Opfs = __napiModule.exports.Opfs
export const OpfsFile = __napiModule.exports.OpfsFile
export const Statement = __napiModule.exports.Statement
export const connect = __napiModule.exports.connect
export const initThreadPool = __napiModule.exports.initThreadPool

View File

@@ -10,12 +10,21 @@
"main": "dist/promise.js",
"packageManager": "yarn@4.9.2",
"files": [
"index.js",
"worker.mjs",
"turso.wasm32-wasi.wasm",
"dist/**",
"README.md"
],
"exports": {
".": {
"default": "./dist/promise-default.js"
},
"./vite": {
"development": "./dist/promise-vite-dev-hack.js",
"default": "./dist/promise-default.js"
},
"./turbopack": {
"default": "./dist/promise-turbopack-hack.js"
}
},
"devDependencies": {
"@napi-rs/cli": "^3.1.5",
"@vitest/browser": "^3.2.4",
@@ -25,7 +34,7 @@
},
"scripts": {
"napi-build": "napi build --features browser --release --platform --target wasm32-wasip1-threads --no-js --manifest-path ../../Cargo.toml --output-dir . && rm index.d.ts turso.wasi* wasi* browser.js",
"tsc-build": "npm exec tsc",
"tsc-build": "npm exec tsc && cp turso.wasm32-wasi.wasm ./dist/turso.wasm32-wasi.wasm && WASM_FILE=turso.wasm32-wasi.wasm JS_FILE=./dist/wasm-inline.js node ../../scripts/inline-wasm-base64.js",
"build": "npm run napi-build && npm run tsc-build",
"test": "CI=1 vitest --browser=chromium --run && CI=1 vitest --browser=firefox --run"
},
@@ -35,11 +44,7 @@
"wasm32-wasip1-threads"
]
},
"imports": {
"#index": "./index.js"
},
"dependencies": {
"@napi-rs/wasm-runtime": "^1.0.3",
"@tursodatabase/database-browser-common": "^0.2.0-pre.1",
"@tursodatabase/database-common": "^0.2.0-pre.1"
}

View File

@@ -0,0 +1,22 @@
import { DatabaseOpts, SqliteError, } from "@tursodatabase/database-common"
import { connect as promiseConnect, Database } from "./promise.js";
import { connect as nativeConnect, initThreadPool, MainWorker } from "./index-default.js";
/**
* Creates a new database connection asynchronously.
*
* @param {string} path - Path to the database file.
* @param {Object} opts - Options for database behavior.
* @returns {Promise<Database>} - A promise that resolves to a Database instance.
*/
async function connect(path: string, opts: DatabaseOpts = {}): Promise<Database> {
return await promiseConnect(path, opts, nativeConnect, async () => {
await initThreadPool();
if (MainWorker == null) {
throw new Error("panic: MainWorker is not initialized");
}
return MainWorker;
});
}
export { connect, Database, SqliteError }

View File

@@ -0,0 +1,22 @@
import { DatabaseOpts, SqliteError, } from "@tursodatabase/database-common"
import { connect as promiseConnect, Database } from "./promise.js";
import { connect as nativeConnect, initThreadPool, MainWorker } from "./index-turbopack-hack.js";
/**
* Creates a new database connection asynchronously.
*
* @param {string} path - Path to the database file.
* @param {Object} opts - Options for database behavior.
* @returns {Promise<Database>} - A promise that resolves to a Database instance.
*/
async function connect(path: string, opts: DatabaseOpts = {}): Promise<Database> {
return await promiseConnect(path, opts, nativeConnect, async () => {
await initThreadPool();
if (MainWorker == null) {
throw new Error("panic: MainWorker is not initialized");
}
return MainWorker;
});
}
export { connect, Database, SqliteError }

View File

@@ -0,0 +1,22 @@
import { DatabaseOpts, SqliteError, } from "@tursodatabase/database-common"
import { connect as promiseConnect, Database } from "./promise.js";
import { connect as nativeConnect, initThreadPool, MainWorker } from "./index-vite-dev-hack.js";
/**
* Creates a new database connection asynchronously.
*
* @param {string} path - Path to the database file.
* @param {Object} opts - Options for database behavior.
* @returns {Promise<Database>} - A promise that resolves to a Database instance.
*/
async function connect(path: string, opts: DatabaseOpts = {}): Promise<Database> {
return await promiseConnect(path, opts, nativeConnect, async () => {
await initThreadPool();
if (MainWorker == null) {
throw new Error("panic: MainWorker is not initialized");
}
return MainWorker;
});
}
export { connect, Database, SqliteError }

View File

@@ -1,5 +1,5 @@
import { expect, test, afterEach } from 'vitest'
import { connect } from './promise.js'
import { connect } from './promise-default.js'
test('in-memory db', async () => {
const db = await connect(":memory:");

View File

@@ -1,18 +1,19 @@
import { registerFileAtWorker, unregisterFileAtWorker } from "@tursodatabase/database-browser-common"
import { DatabasePromise, NativeDatabase, DatabaseOpts, SqliteError, } from "@tursodatabase/database-common"
import { connect as nativeConnect, initThreadPool, MainWorker } from "#index";
class Database extends DatabasePromise {
path: string | null;
constructor(db: NativeDatabase, fsPath: string | null, opts: DatabaseOpts = {}) {
worker: Worker | null;
constructor(db: NativeDatabase, worker: Worker | null, fsPath: string | null, opts: DatabaseOpts = {}) {
super(db, opts)
this.path = fsPath;
this.worker = worker;
}
async close() {
if (this.path != null) {
if (this.path != null && this.worker != null) {
await Promise.all([
unregisterFileAtWorker(MainWorker, this.path),
unregisterFileAtWorker(MainWorker, `${this.path}-wal`)
unregisterFileAtWorker(this.worker, this.path),
unregisterFileAtWorker(this.worker, `${this.path}-wal`)
]);
}
this.db.close();
@@ -26,21 +27,18 @@ class Database extends DatabasePromise {
* @param {Object} opts - Options for database behavior.
* @returns {Promise<Database>} - A promise that resolves to a Database instance.
*/
async function connect(path: string, opts: DatabaseOpts = {}): Promise<Database> {
async function connect(path: string, opts: DatabaseOpts, connect: any, init: () => Promise<Worker>): Promise<Database> {
if (path == ":memory:") {
const db = await nativeConnect(path, { tracing: opts.tracing });
return new Database(db, null, opts);
}
await initThreadPool();
if (MainWorker == null) {
throw new Error("panic: MainWorker is not set");
const db = await connect(path, { tracing: opts.tracing });
return new Database(db, null, null, opts);
}
const worker = await init();
await Promise.all([
registerFileAtWorker(MainWorker, path),
registerFileAtWorker(MainWorker, `${path}-wal`)
registerFileAtWorker(worker, path),
registerFileAtWorker(worker, `${path}-wal`)
]);
const db = await nativeConnect(path, { tracing: opts.tracing });
return new Database(db, path, opts);
const db = await connect(path, { tracing: opts.tracing });
return new Database(db, worker, path, opts);
}
export { connect, Database, SqliteError }

View File

@@ -11,12 +11,7 @@
"es2020",
"DOM",
"WebWorker"
],
"paths": {
"#index": [
"./index.js"
]
}
]
},
"include": [
"*"

View File

@@ -0,0 +1,9 @@
const tursoWasmBase64 = '__PLACEHOLDER__';
async function convertBase64ToBinary(base64Url: string): Promise<ArrayBuffer> {
const blob = await fetch(base64Url).then(res => res.blob());
return await blob.arrayBuffer();
}
export async function tursoWasm(): Promise<ArrayBuffer> {
return await convertBase64ToBinary(tursoWasmBase64);
}

View File

@@ -1,55 +0,0 @@
import { instantiateNapiModuleSync, MessageHandler, WASI } from '@napi-rs/wasm-runtime'
import { OpfsDirectory, workerImports } from '@tursodatabase/database-browser-common';
var opfs = new OpfsDirectory();
var memory = null;
const handler = new MessageHandler({
onLoad({ wasmModule, wasmMemory }) {
memory = wasmMemory;
const wasi = new WASI({
print: function () {
// eslint-disable-next-line no-console
console.log.apply(console, arguments)
},
printErr: function () {
// eslint-disable-next-line no-console
console.error.apply(console, arguments)
},
})
return instantiateNapiModuleSync(wasmModule, {
childThread: true,
wasi,
overwriteImports(importObject) {
importObject.env = {
...importObject.env,
...importObject.napi,
...importObject.emnapi,
...workerImports(opfs, memory),
memory: wasmMemory,
}
},
})
},
})
globalThis.onmessage = async function (e) {
if (e.data.__turso__ == 'register') {
try {
await opfs.registerFile(e.data.path);
self.postMessage({ id: e.data.id });
} catch (error) {
self.postMessage({ id: e.data.id, error: error });
}
return;
} else if (e.data.__turso__ == 'unregister') {
try {
await opfs.unregisterFile(e.data.path);
self.postMessage({ id: e.data.id });
} catch (error) {
self.postMessage({ id: e.data.id, error: error });
}
return;
}
handler.handle(e)
}

View File

@@ -0,0 +1,2 @@
import { setupWebWorker } from "@tursodatabase/database-browser-common";
setupWebWorker();