UNPKG

@pgxsinkit/pgwasm

Version:
211 lines (176 loc) • 6.91 kB
import { BaseFilesystem } from "../fs/base-filesystem"; import type { FilesystemDescription, FsStats } from "../fs/base-filesystem"; import { DurabilityModeMismatchError, StoreLimitError } from "./core/errors"; import type { RepackedPort } from "./core/port"; import { RepackedVfs } from "./core/repacked-vfs"; import type { RepackedStat } from "./core/repacked-vfs"; /** Physical durability selected once for the lifetime of a store `createOpfsPgwasm` opens. */ export type RepackedDurability = "relaxed" | "strict"; export interface RepackedFilesystemOptions { /** Creation extent size: 8 KiB–16 MiB in 8 KiB steps. Defaults to 64 KiB. */ readonly extentSize?: number; /** Physical durability for this instance. Defaults to `"relaxed"`. */ readonly durability?: RepackedDurability; } const DESCRIPTION: FilesystemDescription = Object.freeze({ name: "opfs-repacked", persistent: true }); /** * The pgwasm filesystem adapter owned by `createOpfsPgwasm`. * * Direct construction is unsupported. The factory retains this adapter so it * can close all four handles when host initialization or shutdown fails. */ export abstract class OpfsRepackedFS extends BaseFilesystem { readonly #vfs: RepackedVfs; readonly #durability: RepackedDurability; protected constructor(vfs: RepackedVfs, durability: RepackedDurability) { super(); this.#vfs = vfs; this.#durability = durability; } /** An OPFS-repacked store: its data directory outlives the page. */ override get description(): FilesystemDescription { return DESCRIPTION; } override async initialSyncFs(): Promise<void> { this.#vfs.assertHealthy(); } override async syncToFs(relaxedDurability = false): Promise<void> { this.#vfs.assertHealthy(); if (relaxedDurability) this.#vfs.fail(new DurabilityModeMismatchError()); this.#vfs.runScheduledRepack(); if (this.#durability === "strict") this.#vfs.strictSync(); } /** Stabilize every preceding data and metadata operation in strict order. */ strictSync(): void { this.#vfs.strictSync(); } /** @internal Factory lifecycle cleanup after host initialization fails. */ override async cleanupFailedInit(): Promise<void> { this.#vfs.cleanupFailedInit(); } override async closeFs(): Promise<void> { this.#vfs.close(); } chmod(path: string, mode: number): void { this.#vfs.chmod(hostPath(path), mode, nowMs()); } close(fd: number): void { this.#vfs.close(fd); } fstat(fd: number): FsStats { return toFsStats(this.#vfs.fstat(fd)); } lstat(path: string): FsStats { return toFsStats(this.#vfs.lstat(hostPath(path))); } mkdir(path: string, options?: { recursive?: boolean; mode?: number }): void { this.#vfs.mkdir(hostPath(path), { ...(options?.recursive === undefined ? {} : { recursive: options.recursive }), ...(options?.mode === undefined ? {} : { mode: options.mode }), nowMs: nowMs(), }); } open(path: string, flags = "r+", mode = 0o100666): number { return this.#vfs.open(hostPath(path), flags, mode, nowMs()); } readdir(path: string): string[] { return this.#vfs.readdir(hostPath(path)); } read(fd: number, buffer: Uint8Array, offset: number, length: number, position: number): number { return this.#vfs.read(fd, buffer, offset, length, toFileOffset(position)); } rename(oldPath: string, newPath: string): void { this.#vfs.rename(hostPath(oldPath), hostPath(newPath), nowMs()); } rmdir(path: string): void { this.#vfs.rmdir(hostPath(path), nowMs()); } truncate(path: string, length: number): void { this.#vfs.truncate(hostPath(path), toFileOffset(length), nowMs()); } unlink(path: string): void { this.#vfs.unlink(hostPath(path), nowMs()); } utimes(path: string, atime: number, mtime: number): void { this.#vfs.utimes(hostPath(path), toTimestamp(atime), toTimestamp(mtime), nowMs()); } writeFile( path: string, data: string | Uint8Array, options?: { encoding?: string; mode?: number; flag?: string }, ): void { this.#vfs.writeFile(hostPath(path), data, { ...(options?.encoding === undefined ? {} : { encoding: options.encoding }), ...(options?.mode === undefined ? {} : { mode: options.mode }), ...(options?.flag === undefined ? {} : { flag: options.flag }), nowMs: nowMs(), }); } write(fd: number, buffer: Uint8Array, offset: number, length: number, position: number): number { if (buffer instanceof Uint8Array) { return this.#vfs.write(fd, buffer, offset, length, toFileOffset(position), nowMs()); } const source = new Uint8Array(buffer as unknown as ArrayBuffer, offset, length); return this.#vfs.write(fd, source, 0, source.byteLength, toFileOffset(position), nowMs()); } } class ConcreteOpfsRepackedFS extends OpfsRepackedFS { constructor(vfs: RepackedVfs, durability: RepackedDurability) { super(vfs, durability); } } export async function openOpfsRepackedFsForPort( port: RepackedPort, options: RepackedFilesystemOptions = {}, ): Promise<OpfsRepackedFS> { const durability = options.durability ?? "relaxed"; if (durability !== "relaxed" && durability !== "strict") { throw new TypeError(`unsupported OPFS repacked durability: ${String(durability)}`); } const vfs = await RepackedVfs.open(port, { ...(options.extentSize === undefined ? {} : { extentSize: options.extentSize }), }); return new ConcreteOpfsRepackedFS(vfs, durability); } function nowMs(): bigint { return BigInt(Date.now()); } function hostPath(path: string): string { return path === "" ? "/" : path; } function toFileOffset(value: number): bigint { if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("file offset is not a non-negative safe integer"); return BigInt(value); } function toTimestamp(value: number): bigint { // Emscripten's utime path can pass fractional milliseconds (µs-precision // utimensat converted to ms); floor instead of rejecting the syscall. const floored = Math.floor(value); if (!Number.isSafeInteger(floored) || floored < 0) { throw new TypeError("timestamp is not a non-negative finite millisecond value"); } return BigInt(floored); } function toFsStats(stat: RepackedStat): FsStats { const size = toSafeNumber(stat.size, "file size"); const blksize = 4096; return { dev: 0, ino: 0, mode: stat.mode, nlink: 1, uid: 0, gid: 0, rdev: 0, size, blksize, blocks: Math.ceil(size / blksize), atime: toSafeNumber(stat.atimeMs, "access time"), mtime: toSafeNumber(stat.mtimeMs, "modification time"), ctime: toSafeNumber(stat.ctimeMs, "change time"), }; } function toSafeNumber(value: bigint, label: string): number { if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new StoreLimitError(`${label} exceeds safe integer range`); return Number(value); }