/**
 * OPFS SAH‑pool environment implemented with a Dedicated Worker.
 *
 * Spawns a Worker and forwards calls via a minimal RPC while keeping the
 * environment async surface. Use one instance per logical database key.
 * @example
 * // Static helper: clear all OPFS data for this origin (destructive)
 * await OpfsSahEnvironment.clear()
 */
export class OpfsSahEnvironment {
    static _instances = new Set();
    static _openByKey = new Map();
    worker;
    nextId = 0;
    inflight = new Map();
    eventHandler;
    dbKey;
    isOpen = false;
    actorWorkers = new Set();
    constructor(opts) {
        this.dbKey = opts?.key || "default";
        this.worker = new Worker(new URL("./opfs-sah.worker.js", import.meta.url), {
            type: "module",
        });
        OpfsSahEnvironment._instances.add(this);
        this.worker.onmessage = (ev) => {
            const msg = ev.data;
            if (msg && msg.type === "event" && msg.event) {
                this.eventHandler?.(msg.event);
                return;
            }
            const cb = this.inflight.get(msg?.id);
            if (cb) {
                this.inflight.delete(msg.id);
                cb(msg);
            }
        };
    }
    send(op, payload, transfer) {
        const id = String(++this.nextId);
        return new Promise((resolve, reject) => {
            this.inflight.set(id, (msg) => {
                if (msg.ok)
                    resolve(msg.result);
                else
                    reject(Object.assign(new Error(msg.error?.message ?? "Worker error"), msg.error));
            });
            this.worker.postMessage({ id, op, payload }, transfer ?? []);
        });
    }
    async open(initOpts) {
        if (this.isOpen)
            return {};
        const existing = OpfsSahEnvironment._openByKey.get(this.dbKey);
        if (existing && existing !== this) {
            const err = new Error(`OPFS environment for key "${this.dbKey}" is already open. Close the existing connection before opening a new one.`);
            err.code = "ENV_OPFS_ALREADY_OPEN";
            throw err;
        }
        this.eventHandler = initOpts.emit;
        const bootArgs = initOpts.boot.args;
        if (bootArgs.providePlugins && bootArgs.providePlugins.length > 0) {
            throw new Error("OpfsSahEnvironment runs in a Worker and cannot receive plugins via providePlugins. Import the plugin source (e.g. with ?raw) and pass it through providePluginsRaw instead.");
        }
        const payload = { name: this.dbKey, bootArgs: initOpts.boot.args };
        await this.send("open", payload);
        this.isOpen = true;
        OpfsSahEnvironment._openByKey.set(this.dbKey, this);
        return {};
    }
    /**
     * Invoke a named engine function inside the worker engine.
     */
    async call(name, args) {
        return this.send("call", { route: name, payload: args });
    }
    async create(createOpts) {
        const payload = {
            name: this.dbKey,
            blob: createOpts.blob,
        };
        await this.send("create", payload, [createOpts.blob]);
    }
    /**
     * Export the SQLite database image as raw bytes.
     *
     * Worker returns one of two shapes for performance:
     * - Fast path: `{ blob: ArrayBuffer }` (zero‑copy transfer)
     * - Fallback: `{ buffer, offset, length }` (single copy on host)
     */
    async export() {
        const res = await this.send("export", {});
        if ("blob" in res)
            return res.blob;
        const { buffer, offset, length } = res;
        if (offset === 0 && length === buffer.byteLength)
            return buffer;
        return buffer.slice(offset, offset + length);
    }
    async exists() {
        return this.send("exists", { name: this.dbKey });
    }
    async close() {
        if (this.isOpen) {
            try {
                await this.send("close", {});
            }
            finally {
                this.isOpen = false;
                if (OpfsSahEnvironment._openByKey.get(this.dbKey) === this) {
                    OpfsSahEnvironment._openByKey.delete(this.dbKey);
                }
            }
        }
        for (const worker of this.actorWorkers) {
            worker.terminate();
        }
        this.actorWorkers.clear();
        this.worker.terminate();
        OpfsSahEnvironment._instances.delete(this);
        this.eventHandler = undefined;
    }
    /**
     * Spawn a dedicated worker-based actor.
     *
     * Messages emitted by the worker before any subscribers attach are buffered
     * and replayed to the first subscriber so initialization events are never lost.
     *
     * @example
     * const actor = await env.spawnActor({ entryModule, name: "echo" });
     * const unsubscribe = actor.subscribe((message) => console.log(message));
     * actor.post({ type: "ping" });
     */
    async spawnActor(opts) {
        const workerUrl = new URL(opts.entryModule, import.meta.url);
        const actor = new Worker(workerUrl, {
            type: "module",
            name: opts.name,
        });
        this.actorWorkers.add(actor);
        const listeners = new Set();
        const pending = [];
        const dispatch = (message) => {
            if (listeners.size === 0) {
                pending.push(message);
                return;
            }
            for (const listener of listeners)
                listener(message);
        };
        const onMessage = (event) => {
            dispatch(event.data);
        };
        actor.addEventListener("message", onMessage);
        const handle = {
            post: (message, transfer) => {
                if (transfer && transfer.length > 0) {
                    actor.postMessage(message, transfer);
                    return;
                }
                actor.postMessage(message);
            },
            subscribe: (listener) => {
                listeners.add(listener);
                if (pending.length > 0) {
                    const buffered = pending.splice(0, pending.length);
                    for (const message of buffered) {
                        listener(message);
                    }
                }
                return () => {
                    listeners.delete(listener);
                };
            },
            terminate: async () => {
                this.actorWorkers.delete(actor);
                actor.removeEventListener("message", onMessage);
                listeners.clear();
                pending.length = 0;
                actor.terminate();
            },
        };
        if (opts.initialMessage !== undefined || (opts.transfer?.length ?? 0) > 0) {
            if (opts.transfer && opts.transfer.length > 0) {
                actor.postMessage(opts.initialMessage, opts.transfer);
            }
            else {
                actor.postMessage(opts.initialMessage);
            }
        }
        return handle;
    }
    /**
     * Cleans the entire OPFS by removing all files.
     * Useful for debugging and testing.
     *
     * WARNING! This will delete ALL files in OPFS, not just Lix files!
     */
    static async clear() {
        // Refuse to clear if any environment instances are still open to avoid races
        if (OpfsSahEnvironment._instances.size > 0) {
            const err = new Error(`Cannot clear OPFS: ${OpfsSahEnvironment._instances.size} database connection(s) are open. Close all connections before clearing.`);
            err.code = "ENV_OPFS_BUSY";
            throw err;
        }
        if (typeof navigator === "undefined" || !navigator.storage?.getDirectory) {
            throw new Error("OPFS not available: navigator.storage.getDirectory() is missing");
        }
        const root = await navigator.storage.getDirectory();
        // Prefer DirectoryHandle.values() if present (not always typed in libdom)
        const hasValues = typeof root.values === "function";
        const hasEntries = typeof root.entries === "function";
        const remove = async (name, kind) => {
            try {
                if (kind === "directory") {
                    await root.removeEntry(name, { recursive: true });
                }
                else {
                    await root.removeEntry(name);
                }
            }
            catch (e) {
                // Best effort: ignore failures on already-removed entries
            }
        };
        if (hasValues) {
            for await (const handle of root.values()) {
                await remove(handle.name, handle.kind);
            }
            return;
        }
        if (hasEntries) {
            for await (const [name, handle] of root.entries()) {
                await remove(name, handle.kind);
            }
            return;
        }
    }
}
//# sourceMappingURL=opfs-sah.js.map