mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-17 08:34:19 +01:00
- now, most of the work is happening on the main thread - for database in browser, we still have dedicated WebWorker - but it is used only for OPFS access and only for that - for syn in browser we still offload sync operations to the WebWorker
453 lines
15 KiB
TypeScript
453 lines
15 KiB
TypeScript
import {
|
|
createOnMessage as __wasmCreateOnMessageForFsProxy,
|
|
getDefaultContext as __emnapiGetDefaultContext,
|
|
instantiateNapiModule as __emnapiInstantiateNapiModule,
|
|
WASI as __WASI,
|
|
instantiateNapiModuleSync,
|
|
MessageHandler
|
|
} from '@tursodatabase/wasm-runtime'
|
|
|
|
function getUint8ArrayFromMemory(memory: WebAssembly.Memory, ptr: number, len: number): Uint8Array {
|
|
ptr = ptr >>> 0;
|
|
return new Uint8Array(memory.buffer).subarray(ptr, ptr + len);
|
|
}
|
|
|
|
function getStringFromMemory(memory: WebAssembly.Memory, ptr: number, len: number): string {
|
|
const shared = getUint8ArrayFromMemory(memory, ptr, len);
|
|
const copy = new Uint8Array(shared.length);
|
|
copy.set(shared);
|
|
const decoder = new TextDecoder('utf-8');
|
|
return decoder.decode(copy);
|
|
}
|
|
|
|
interface BrowserImports {
|
|
is_web_worker(): boolean;
|
|
lookup_file(ptr: number, len: number): number;
|
|
read(handle: number, ptr: number, len: number, offset: number): number;
|
|
read_async(handle: number, ptr: number, len: number, offset: number, c: number);
|
|
write(handle: number, ptr: number, len: number, offset: number): number;
|
|
write_async(handle: number, ptr: number, len: number, offset: number, c: number);
|
|
sync(handle: number): number;
|
|
sync_async(handle: number, c: number);
|
|
truncate(handle: number, len: number): number;
|
|
truncate_async(handle: number, len: number, c: number);
|
|
size(handle: number): number;
|
|
}
|
|
|
|
function panicMain(name): never {
|
|
throw new Error(`method ${name} must be invoked only from the worker thread`);
|
|
}
|
|
|
|
function panicWorker(name): never {
|
|
throw new Error(`method ${name} must be invoked only from the main thread`);
|
|
}
|
|
|
|
let completeOpfs: any = null;
|
|
|
|
function mainImports(worker: Worker): BrowserImports {
|
|
return {
|
|
is_web_worker(): boolean {
|
|
return false;
|
|
},
|
|
write_async(handle, ptr, len, offset, c) {
|
|
writeFileAtWorker(worker, handle, ptr, len, offset)
|
|
.then(result => {
|
|
completeOpfs(c, result);
|
|
}, err => {
|
|
console.error('write_async', err);
|
|
completeOpfs(c, -1);
|
|
});
|
|
},
|
|
sync_async(handle, c) {
|
|
syncFileAtWorker(worker, handle)
|
|
.then(result => {
|
|
completeOpfs(c, result);
|
|
}, err => {
|
|
console.error('sync_async', err);
|
|
completeOpfs(c, -1);
|
|
});
|
|
},
|
|
read_async(handle, ptr, len, offset, c) {
|
|
readFileAtWorker(worker, handle, ptr, len, offset)
|
|
.then(result => {
|
|
completeOpfs(c, result);
|
|
}, err => {
|
|
console.error('read_async', err);
|
|
completeOpfs(c, -1);
|
|
});
|
|
},
|
|
truncate_async(handle, len, c) {
|
|
truncateFileAtWorker(worker, handle, len)
|
|
.then(result => {
|
|
completeOpfs(c, result);
|
|
}, err => {
|
|
console.error('truncate_async', err);
|
|
completeOpfs(c, -1);
|
|
});
|
|
},
|
|
lookup_file(ptr, len): number {
|
|
panicMain("lookup_file")
|
|
},
|
|
read(handle, ptr, len, offset): number {
|
|
panicMain("read")
|
|
},
|
|
write(handle, ptr, len, offset): number {
|
|
panicMain("write")
|
|
},
|
|
sync(handle): number {
|
|
panicMain("sync")
|
|
},
|
|
truncate(handle, len): number {
|
|
panicMain("truncate")
|
|
},
|
|
size(handle): number {
|
|
panicMain("size")
|
|
}
|
|
};
|
|
};
|
|
|
|
function workerImports(opfs: OpfsDirectory, memory: WebAssembly.Memory): BrowserImports {
|
|
return {
|
|
is_web_worker(): boolean {
|
|
return true;
|
|
},
|
|
lookup_file(ptr, len): number {
|
|
try {
|
|
const handle = opfs.lookupFileHandle(getStringFromMemory(memory, ptr, len));
|
|
return handle == null ? -404 : handle;
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
},
|
|
read(handle, ptr, len, offset): number {
|
|
try {
|
|
return opfs.read(handle, getUint8ArrayFromMemory(memory, ptr, len), offset);
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
},
|
|
write(handle, ptr, len, offset): number {
|
|
try {
|
|
return opfs.write(handle, getUint8ArrayFromMemory(memory, ptr, len), offset)
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
},
|
|
sync(handle): number {
|
|
try {
|
|
return opfs.sync(handle);
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
},
|
|
truncate(handle, len): number {
|
|
try {
|
|
opfs.truncate(handle, len);
|
|
return 0;
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
},
|
|
size(handle): number {
|
|
try {
|
|
return opfs.size(handle);
|
|
} catch (e) {
|
|
return -1;
|
|
}
|
|
},
|
|
read_async(handle, ptr, len, offset, completion) {
|
|
panicWorker("read_async")
|
|
},
|
|
write_async(handle, ptr, len, offset, completion) {
|
|
panicWorker("write_async")
|
|
},
|
|
sync_async(handle, completion) {
|
|
panicWorker("sync_async")
|
|
},
|
|
truncate_async(handle, len, c) {
|
|
panicWorker("truncate_async")
|
|
},
|
|
}
|
|
}
|
|
|
|
class OpfsDirectory {
|
|
fileByPath: Map<String, { handle: number, sync: FileSystemSyncAccessHandle }>;
|
|
fileByHandle: Map<number, FileSystemSyncAccessHandle>;
|
|
fileHandleNo: number;
|
|
|
|
constructor() {
|
|
this.fileByPath = new Map();
|
|
this.fileByHandle = new Map();
|
|
this.fileHandleNo = 0;
|
|
}
|
|
|
|
async registerFile(path: string) {
|
|
if (this.fileByPath.has(path)) {
|
|
return;
|
|
}
|
|
const opfsRoot = await navigator.storage.getDirectory();
|
|
const opfsHandle = await opfsRoot.getFileHandle(path, { create: true });
|
|
const opfsSync = await opfsHandle.createSyncAccessHandle();
|
|
this.fileHandleNo += 1;
|
|
this.fileByPath.set(path, { handle: this.fileHandleNo, sync: opfsSync });
|
|
this.fileByHandle.set(this.fileHandleNo, opfsSync);
|
|
}
|
|
|
|
async unregisterFile(path: string) {
|
|
const file = this.fileByPath.get(path);
|
|
if (file == null) {
|
|
return;
|
|
}
|
|
this.fileByPath.delete(path);
|
|
this.fileByHandle.delete(file.handle);
|
|
file.sync.close();
|
|
}
|
|
lookupFileHandle(path: string): number | null {
|
|
try {
|
|
const file = this.fileByPath.get(path);
|
|
if (file == null) {
|
|
return null;
|
|
}
|
|
return file.handle;
|
|
} catch (e) {
|
|
console.error('lookupFile', path, e);
|
|
throw e;
|
|
}
|
|
}
|
|
read(handle: number, buffer: Uint8Array, offset: number): number {
|
|
try {
|
|
const file = this.fileByHandle.get(handle);
|
|
const result = file.read(buffer, { at: Number(offset) });
|
|
return result;
|
|
} catch (e) {
|
|
console.error('read', handle, buffer.length, offset, e);
|
|
throw e;
|
|
}
|
|
}
|
|
write(handle: number, buffer: Uint8Array, offset: number): number {
|
|
try {
|
|
const file = this.fileByHandle.get(handle);
|
|
const result = file.write(buffer, { at: Number(offset) });
|
|
return result;
|
|
} catch (e) {
|
|
console.error('write', handle, buffer.length, offset, e);
|
|
throw e;
|
|
}
|
|
}
|
|
sync(handle: number): number {
|
|
try {
|
|
const file = this.fileByHandle.get(handle);
|
|
file.flush();
|
|
return 0;
|
|
} catch (e) {
|
|
console.error('sync', handle, e);
|
|
throw e;
|
|
}
|
|
}
|
|
truncate(handle: number, size: number) {
|
|
try {
|
|
const file = this.fileByHandle.get(handle);
|
|
file.truncate(size);
|
|
return 0;
|
|
} catch (e) {
|
|
console.error('truncate', handle, size, e);
|
|
throw e;
|
|
}
|
|
}
|
|
size(handle: number): number {
|
|
try {
|
|
const file = this.fileByHandle.get(handle);
|
|
const size = file.getSize()
|
|
return size;
|
|
} catch (e) {
|
|
console.error('size', handle, e);
|
|
throw e;
|
|
}
|
|
}
|
|
}
|
|
|
|
let workerRequestId = 0;
|
|
function waitForWorkerResponse(worker: Worker, id: number): Promise<any> {
|
|
let waitResolve, waitReject;
|
|
const callback = msg => {
|
|
if (msg.data.id == id) {
|
|
if (msg.data.error != null) {
|
|
waitReject(msg.data.error)
|
|
} else {
|
|
waitResolve(msg.data.result)
|
|
}
|
|
cleanup();
|
|
}
|
|
};
|
|
const cleanup = () => worker.removeEventListener("message", callback);
|
|
|
|
worker.addEventListener("message", callback);
|
|
const result = new Promise((resolve, reject) => {
|
|
waitResolve = resolve;
|
|
waitReject = reject;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function readFileAtWorker(worker: Worker, handle: number, ptr: number, len: number, offset: number) {
|
|
workerRequestId += 1;
|
|
const currentId = workerRequestId;
|
|
const promise = waitForWorkerResponse(worker, currentId);
|
|
worker.postMessage({ __turso__: "read_async", handle: handle, ptr: ptr, len: len, offset: offset, id: currentId });
|
|
return promise;
|
|
}
|
|
|
|
function writeFileAtWorker(worker: Worker, handle: number, ptr: number, len: number, offset: number) {
|
|
workerRequestId += 1;
|
|
const currentId = workerRequestId;
|
|
const promise = waitForWorkerResponse(worker, currentId);
|
|
worker.postMessage({ __turso__: "write_async", handle: handle, ptr: ptr, len: len, offset: offset, id: currentId });
|
|
return promise;
|
|
}
|
|
|
|
function syncFileAtWorker(worker: Worker, handle: number) {
|
|
workerRequestId += 1;
|
|
const currentId = workerRequestId;
|
|
const promise = waitForWorkerResponse(worker, currentId);
|
|
worker.postMessage({ __turso__: "sync_async", handle: handle, id: currentId });
|
|
return promise;
|
|
}
|
|
|
|
function truncateFileAtWorker(worker: Worker, handle: number, len: number) {
|
|
workerRequestId += 1;
|
|
const currentId = workerRequestId;
|
|
const promise = waitForWorkerResponse(worker, currentId);
|
|
worker.postMessage({ __turso__: "truncate_async", handle: handle, len: len, id: currentId });
|
|
return promise;
|
|
}
|
|
|
|
function registerFileAtWorker(worker: Worker, path: string): Promise<void> {
|
|
workerRequestId += 1;
|
|
const currentId = workerRequestId;
|
|
const promise = waitForWorkerResponse(worker, currentId);
|
|
worker.postMessage({ __turso__: "register", path: path, id: currentId });
|
|
return promise;
|
|
}
|
|
|
|
function unregisterFileAtWorker(worker: Worker, path: string): Promise<void> {
|
|
workerRequestId += 1;
|
|
const currentId = workerRequestId;
|
|
const promise = waitForWorkerResponse(worker, currentId);
|
|
worker.postMessage({ __turso__: "unregister", path: path, id: currentId });
|
|
return promise;
|
|
}
|
|
|
|
function isWebWorker(): boolean {
|
|
return typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
|
|
}
|
|
|
|
function setupWebWorker() {
|
|
let opfs = new OpfsDirectory();
|
|
let 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;
|
|
} else if (e.data.__turso__ == 'read_async') {
|
|
let result = opfs.read(e.data.handle, getUint8ArrayFromMemory(memory, e.data.ptr, e.data.len), e.data.offset);
|
|
self.postMessage({ id: e.data.id, result: result });
|
|
} else if (e.data.__turso__ == 'write_async') {
|
|
let result = opfs.write(e.data.handle, getUint8ArrayFromMemory(memory, e.data.ptr, e.data.len), e.data.offset);
|
|
self.postMessage({ id: e.data.id, result: result });
|
|
} else if (e.data.__turso__ == 'sync_async') {
|
|
let result = opfs.sync(e.data.handle);
|
|
self.postMessage({ id: e.data.id, result: result });
|
|
} else if (e.data.__turso__ == 'truncate_async') {
|
|
let result = opfs.truncate(e.data.handle, e.data.len);
|
|
self.postMessage({ id: e.data.id, result: result });
|
|
}
|
|
handler.handle(e)
|
|
}
|
|
}
|
|
|
|
async function setupMainThread(wasmFile: ArrayBuffer, factory: () => Worker): Promise<any> {
|
|
const worker = factory();
|
|
const __emnapiContext = __emnapiGetDefaultContext()
|
|
const __wasi = new __WASI({
|
|
version: 'preview1',
|
|
})
|
|
const __sharedMemory = new WebAssembly.Memory({
|
|
initial: 4000,
|
|
maximum: 65536,
|
|
shared: true,
|
|
})
|
|
const {
|
|
instance: __napiInstance,
|
|
module: __wasiModule,
|
|
napiModule: __napiModule,
|
|
} = await __emnapiInstantiateNapiModule(wasmFile, {
|
|
context: __emnapiContext,
|
|
asyncWorkPoolSize: 1,
|
|
wasi: __wasi,
|
|
onCreateWorker() { return worker; },
|
|
overwriteImports(importObject) {
|
|
importObject.env = {
|
|
...importObject.env,
|
|
...importObject.napi,
|
|
...importObject.emnapi,
|
|
...mainImports(worker),
|
|
memory: __sharedMemory,
|
|
}
|
|
return importObject
|
|
},
|
|
beforeInit({ instance }) {
|
|
for (const name of Object.keys(instance.exports)) {
|
|
if (name.startsWith('__napi_register__')) {
|
|
instance.exports[name]()
|
|
}
|
|
}
|
|
},
|
|
});
|
|
completeOpfs = __napiModule.exports.completeOpfs;
|
|
return __napiModule;
|
|
}
|
|
|
|
export { OpfsDirectory, workerImports, mainImports as MainDummyImports, waitForWorkerResponse, registerFileAtWorker, unregisterFileAtWorker, isWebWorker, setupWebWorker, setupMainThread } |