opfs for sync in one commit!

This commit is contained in:
Nikita Sivukhin
2025-09-10 03:01:37 +04:00
parent 8ab8b31cb1
commit d55026f84f
75 changed files with 3553 additions and 5535 deletions

View File

@@ -0,0 +1,8 @@
## About
This package is the Turso embedded database common JS library which is shared between final builds for Node and Browser.
Do not use this package directly - instead you must use `@tursodatabase/database` or `@tursodatabase/database-browser`.
> **⚠️ Warning:** This software is ALPHA, only use for development, testing, and experimentation. We are working to make it production ready, but do not use it for critical data right now.

View File

@@ -0,0 +1,239 @@
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;
write(handle: number, ptr: number, len: number, offset: number): number;
sync(handle: number): number;
truncate(handle: number, len: number): number;
size(handle: number): number;
}
function panic(name): never {
throw new Error(`method ${name} must be invoked only from the main thread`);
}
const MainDummyImports: BrowserImports = {
is_web_worker: function (): boolean {
return false;
},
lookup_file: function (ptr: number, len: number): number {
panic("lookup_file")
},
read: function (handle: number, ptr: number, len: number, offset: number): number {
panic("read")
},
write: function (handle: number, ptr: number, len: number, offset: number): number {
panic("write")
},
sync: function (handle: number): number {
panic("sync")
},
truncate: function (handle: number, len: number): number {
panic("truncate")
},
size: function (handle: number): number {
panic("size")
}
};
function workerImports(opfs: OpfsDirectory, memory: WebAssembly.Memory): BrowserImports {
return {
is_web_worker: function (): boolean {
return true;
},
lookup_file: function (ptr: number, len: number): number {
try {
const handle = opfs.lookupFileHandle(getStringFromMemory(memory, ptr, len));
return handle == null ? -404 : handle;
} catch (e) {
return -1;
}
},
read: function (handle: number, ptr: number, len: number, offset: number): number {
try {
return opfs.read(handle, getUint8ArrayFromMemory(memory, ptr, len), offset);
} catch (e) {
return -1;
}
},
write: function (handle: number, ptr: number, len: number, offset: number): number {
try {
return opfs.write(handle, getUint8ArrayFromMemory(memory, ptr, len), offset)
} catch (e) {
return -1;
}
},
sync: function (handle: number): number {
try {
opfs.sync(handle);
return 0;
} catch (e) {
return -1;
}
},
truncate: function (handle: number, len: number): number {
try {
opfs.truncate(handle, len);
return 0;
} catch (e) {
return -1;
}
},
size: function (handle: number): number {
try {
return opfs.size(handle);
} catch (e) {
return -1;
}
}
}
}
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) {
try {
const file = this.fileByHandle.get(handle);
file.flush();
} catch (e) {
console.error('sync', handle, e);
throw e;
}
}
truncate(handle: number, size: number) {
try {
const file = this.fileByHandle.get(handle);
const result = file.truncate(size);
return result;
} 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;
}
}
}
var 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()
}
cleanup();
}
};
const cleanup = () => worker.removeEventListener("message", callback);
worker.addEventListener("message", callback);
const result = new Promise((resolve, reject) => {
waitResolve = resolve;
waitReject = reject;
});
return result;
}
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;
}
export { OpfsDirectory, workerImports, MainDummyImports, waitForWorkerResponse, registerFileAtWorker, unregisterFileAtWorker }

View File

@@ -0,0 +1,25 @@
{
"name": "@tursodatabase/database-browser-common",
"version": "0.1.5",
"repository": {
"type": "git",
"url": "https://github.com/tursodatabase/turso"
},
"type": "module",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"packageManager": "yarn@4.9.2",
"files": [
"dist/**",
"README.md"
],
"devDependencies": {
"typescript": "^5.9.2"
},
"scripts": {
"tsc-build": "npm exec tsc",
"build": "npm run tsc-build",
"test": "echo 'no tests'"
}
}

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
"module": "esnext",
"target": "esnext",
"outDir": "dist/",
"lib": [
"es2020",
"DOM",
"WebWorker"
],
},
"include": [
"*"
]
}