happy-opfs
Version:
A browser-compatible fs module inspired by the Deno fs and @std/fs APIs, based on OPFS implementation.
1,020 lines (1,002 loc) • 32.1 kB
JavaScript
import invariant from 'tiny-invariant';
import { dirname, basename, SEPARATOR, join, extname } from '@std/path/posix';
import { Ok, RESULT_VOID, Err, RESULT_FALSE } from 'happy-rusty';
import { ABORT_ERROR, fetchT, TIMEOUT_ERROR } from '@happy-ts/fetch-t';
export { ABORT_ERROR, TIMEOUT_ERROR } from '@happy-ts/fetch-t';
import * as fflate from 'fflate/browser';
import { Future } from 'tiny-future';
const NOT_FOUND_ERROR = "NotFoundError";
const ROOT_DIR = "/";
const CURRENT_DIR = ".";
const TMP_DIR = "/tmp";
function assertAbsolutePath(path) {
invariant(typeof path === "string", () => `Path must be a string but received ${path}`);
invariant(path[0] === ROOT_DIR, () => `Path must start with / but received ${path}`);
}
function assertFileUrl(fileUrl) {
invariant(typeof fileUrl === "string", () => `File url must be a string but received ${fileUrl}`);
}
let fsRoot;
async function getFsRoot() {
fsRoot ??= await navigator.storage.getDirectory();
return fsRoot;
}
function isRootPath(path) {
return path === ROOT_DIR;
}
async function getChildDirHandle(dirHandle, dirName, options) {
try {
const handle = await dirHandle.getDirectoryHandle(dirName, options);
return Ok(handle);
} catch (e) {
const err = e;
const error = new Error(`${err.name}: ${err.message} When get child directory '${dirName}' from directory '${dirHandle.name || ROOT_DIR}'.`);
error.name = err.name;
return Err(error);
}
}
async function getChildFileHandle(dirHandle, fileName, options) {
try {
const handle = await dirHandle.getFileHandle(fileName, options);
return Ok(handle);
} catch (e) {
const err = e;
const error = new Error(`${err.name}: ${err.message} When get child file '${fileName}' from directory '${dirHandle.name || ROOT_DIR}'.`);
error.name = err.name;
return Err(error);
}
}
async function getDirHandle(dirPath, options) {
let dirHandle = await getFsRoot();
if (isRootPath(dirPath)) {
return Ok(dirHandle);
}
let childDirPath = dirPath.slice(1);
while (childDirPath) {
let dirName = "";
const index = childDirPath.indexOf(SEPARATOR);
if (index === -1) {
dirName = childDirPath;
childDirPath = "";
} else {
dirName = childDirPath.slice(0, index);
childDirPath = childDirPath.slice(index + 1);
if (index === 0) {
continue;
}
}
const dirHandleRes = await getChildDirHandle(dirHandle, dirName, options);
if (dirHandleRes.isErr()) {
return dirHandleRes;
}
dirHandle = dirHandleRes.unwrap();
}
return Ok(dirHandle);
}
async function getFileHandle(filePath, options) {
const isCreate = options?.create ?? false;
const dirPath = dirname(filePath);
const fileName = basename(filePath);
const dirHandleRes = await getDirHandle(dirPath, {
create: isCreate
});
return dirHandleRes.andThenAsync((dirHandle) => {
return getChildFileHandle(dirHandle, fileName, {
create: isCreate
});
});
}
function isNotFoundError(err) {
return err.name === NOT_FOUND_ERROR;
}
async function getFinalResult(tasks) {
const allRes = await Promise.all(tasks);
const fail = allRes.find((x) => x.isErr());
return fail ?? RESULT_VOID;
}
function createAbortError() {
const error = new Error();
error.name = ABORT_ERROR;
return error;
}
function generateTempPath(options) {
const {
isDirectory = false,
basename = "tmp",
extname = ""
} = options ?? {};
const base = basename ? `${basename}-` : "";
const ext = isDirectory ? "" : extname;
return join(TMP_DIR, `${base}${crypto.randomUUID()}${ext}`);
}
function isTempPath(path) {
return path.startsWith(`${TMP_DIR}${SEPARATOR}`);
}
async function toFileSystemHandleLike(handle) {
const { name, kind } = handle;
if (isFileHandle(handle)) {
const file = await handle.getFile();
const { size, lastModified, type } = file;
const fileHandle = {
name,
kind,
type,
size,
lastModified
};
return fileHandle;
}
const handleLike = {
name,
kind
};
return handleLike;
}
function isFileHandle(handle) {
return handle.kind === "file";
}
function isDirectoryHandle(handle) {
return handle.kind === "directory";
}
function isFileHandleLike(handle) {
return handle.kind === "file";
}
async function getFileDataByHandle(handle) {
const file = await handle.getFile();
const ab = await file.arrayBuffer();
return new Uint8Array(ab);
}
async function createFile(filePath) {
assertAbsolutePath(filePath);
const fileHandleRes = await getFileHandle(filePath, {
create: true
});
return fileHandleRes.and(RESULT_VOID);
}
async function mkdir(dirPath) {
assertAbsolutePath(dirPath);
const dirHandleRes = await getDirHandle(dirPath, {
create: true
});
return dirHandleRes.and(RESULT_VOID);
}
async function readDir(dirPath, options) {
assertAbsolutePath(dirPath);
const dirHandleRes = await getDirHandle(dirPath);
async function* read(dirHandle, subDirPath) {
const entries = dirHandle.entries();
for await (const [name, handle] of entries) {
const path = subDirPath === dirPath ? name : join(subDirPath, name);
yield {
path,
handle
};
if (isDirectoryHandle(handle) && options?.recursive) {
yield* read(await dirHandle.getDirectoryHandle(name), path);
}
}
}
return dirHandleRes.andThen((x) => Ok(read(x, dirPath)));
}
async function readFile(filePath, options) {
assertAbsolutePath(filePath);
const fileHandleRes = await getFileHandle(filePath);
return fileHandleRes.andThenAsync(async (fileHandle) => {
const file = await fileHandle.getFile();
switch (options?.encoding) {
case "blob": {
return Ok(file);
}
case "utf8": {
const text = await file.text();
return Ok(text);
}
default: {
const data = await file.arrayBuffer();
return Ok(data);
}
}
});
}
async function remove(path) {
assertAbsolutePath(path);
const dirPath = dirname(path);
const childName = basename(path);
const dirHandleRes = await getDirHandle(dirPath);
return (await dirHandleRes.andThenAsync(async (dirHandle) => {
try {
if (isRootPath(dirPath) && isRootPath(childName)) {
await dirHandle.remove({
recursive: true
});
} else {
await dirHandle.removeEntry(childName, {
recursive: true
});
}
} catch (e) {
return Err(e);
}
return RESULT_VOID;
})).orElse((err) => {
return isNotFoundError(err) ? RESULT_VOID : Err(err);
});
}
async function stat(path) {
assertAbsolutePath(path);
const dirPath = dirname(path);
const childName = basename(path);
const dirHandleRes = await getDirHandle(dirPath);
if (!childName) {
return dirHandleRes;
}
return dirHandleRes.andThenAsync(async (dirHandle) => {
for await (const [name, handle] of dirHandle.entries()) {
if (name === childName) {
return Ok(handle);
}
}
const err = new Error(`${NOT_FOUND_ERROR}: '${childName}' does not exist. Full path is '${path}'.`);
err.name = NOT_FOUND_ERROR;
return Err(err);
});
}
async function writeFile(filePath, contents, options) {
assertAbsolutePath(filePath);
const { append = false, create = true } = options ?? {};
const fileHandleRes = await getFileHandle(filePath, {
create
});
return fileHandleRes.andThenAsync(async (fileHandle) => {
const writable = await fileHandle.createWritable({
keepExistingData: append
});
const params = {
type: "write",
data: contents
};
if (append) {
const { size } = await fileHandle.getFile();
params.position = size;
}
await writable.write(params);
await writable.close();
return RESULT_VOID;
});
}
function downloadFile(fileUrl, filePath, requestInit) {
assertFileUrl(fileUrl);
let saveToTemp = false;
if (typeof filePath === "string") {
assertAbsolutePath(filePath);
} else {
requestInit = filePath;
filePath = generateTempPath({
extname: extname(fileUrl)
});
saveToTemp = true;
}
let aborted = false;
const fetchTask = fetchT(fileUrl, {
redirect: "follow",
...requestInit,
abortable: true
});
const response = (async () => {
const responseRes = await fetchTask.response;
return responseRes.andThenAsync(async (response2) => {
const blob = await response2.blob();
if (aborted) {
return Err(createAbortError());
}
const writeRes = await writeFile(filePath, blob);
return writeRes.and(Ok(response2));
});
})();
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
abort(reason) {
aborted = true;
fetchTask.abort(reason);
},
get aborted() {
return aborted;
},
get response() {
return saveToTemp ? response.then((res) => {
return res.map((rawResponse) => {
return {
tempFilePath: filePath,
rawResponse
};
});
}) : response;
}
};
}
async function moveHandle(fileHandle, newPath) {
const newDirPath = dirname(newPath);
return (await getDirHandle(newDirPath, {
create: true
})).andThenAsync(async (newDirHandle) => {
const newName = basename(newPath);
try {
await fileHandle.move(newDirHandle, newName);
return RESULT_VOID;
} catch (e) {
return Err(e);
}
});
}
async function mkDestFromSrc(srcPath, destPath, handler, overwrite = true) {
assertAbsolutePath(destPath);
return (await stat(srcPath)).andThenAsync(async (srcHandle) => {
let destExists = false;
const destHandleRes = await stat(destPath);
if (destHandleRes.isErr()) {
if (!isNotFoundError(destHandleRes.unwrapErr())) {
return destHandleRes.asErr();
}
} else {
destExists = true;
const destHandle = destHandleRes.unwrap();
if (!(isFileHandle(srcHandle) && isFileHandle(destHandle) || isDirectoryHandle(srcHandle) && isDirectoryHandle(destHandle))) {
return Err(new Error(`Both 'srcPath' and 'destPath' must both be a file or directory.`));
}
}
if (isFileHandle(srcHandle)) {
return overwrite || !destExists ? await handler(srcHandle, destPath) : RESULT_VOID;
}
const readDirRes = await readDir(srcPath, {
recursive: true
});
return readDirRes.andThenAsync(async (entries) => {
const tasks = [
// make sure new dir created
mkdir(destPath)
];
for await (const { path, handle } of entries) {
const newEntryPath = join(destPath, path);
let newPathExists = false;
if (destExists) {
const existsRes = await exists(newEntryPath);
if (existsRes.isErr()) {
tasks.push(Promise.resolve(existsRes.asErr()));
continue;
}
newPathExists = existsRes.unwrap();
}
const res = isFileHandle(handle) ? overwrite || !newPathExists ? handler(handle, newEntryPath) : Promise.resolve(RESULT_VOID) : mkdir(newEntryPath);
tasks.push(res);
}
return getFinalResult(tasks);
});
});
}
function appendFile(filePath, contents) {
return writeFile(filePath, contents, {
append: true
});
}
async function copy(srcPath, destPath, options) {
const {
overwrite = true
} = options ?? {};
return mkDestFromSrc(srcPath, destPath, async (srcHandle, destPath2) => {
return await writeFile(destPath2, await srcHandle.getFile());
}, overwrite);
}
async function emptyDir(dirPath) {
const readDirRes = await readDir(dirPath);
if (readDirRes.isErr()) {
return isNotFoundError(readDirRes.unwrapErr()) ? mkdir(dirPath) : readDirRes.asErr();
}
const tasks = [];
for await (const { path } of readDirRes.unwrap()) {
tasks.push(remove(join(dirPath, path)));
}
return getFinalResult(tasks);
}
async function exists(path, options) {
const { isDirectory = false, isFile = false } = options ?? {};
invariant(!(isDirectory && isFile), () => "ExistsOptions.isDirectory and ExistsOptions.isFile must not be true together.");
const statRes = await stat(path);
return statRes.andThen((handle) => {
const notExist = isDirectory && isFileHandle(handle) || isFile && isDirectoryHandle(handle);
return Ok(!notExist);
}).orElse((err) => {
return isNotFoundError(err) ? RESULT_FALSE : statRes.asErr();
});
}
async function move(srcPath, destPath, options) {
const {
overwrite = true
} = options ?? {};
return (await mkDestFromSrc(srcPath, destPath, moveHandle, overwrite)).andThenAsync(() => {
return remove(srcPath);
});
}
function readBlobFile(filePath) {
return readFile(filePath, {
encoding: "blob"
});
}
async function readJsonFile(filePath) {
return (await readTextFile(filePath)).andThenAsync(async (contents) => {
try {
return Ok(JSON.parse(contents));
} catch (e) {
return Err(e);
}
});
}
function readTextFile(filePath) {
return readFile(filePath, {
encoding: "utf8"
});
}
async function mkTemp(options) {
const {
isDirectory = false
} = options ?? {};
const path = generateTempPath(options);
const res = await (isDirectory ? mkdir : createFile)(path);
return res.and(Ok(path));
}
function deleteTemp() {
return remove(TMP_DIR);
}
async function pruneTemp(expired) {
invariant(expired instanceof Date, () => `Expired must be a Date but received ${expired}`);
const readDirRes = await readDir(TMP_DIR, {
recursive: true
});
return readDirRes.andThenAsync(async (entries) => {
try {
for await (const { handle } of entries) {
if (isFileHandle(handle) && (await handle.getFile()).lastModified <= expired.getTime()) {
await handle.remove();
}
}
} catch (e) {
return Err(e);
}
return RESULT_VOID;
});
}
async function unzipBufferToTarget(buffer, targetPath) {
const data = new Uint8Array(buffer);
const future = new Future();
fflate.unzip(data, async (err, unzipped) => {
if (err) {
future.resolve(Err(err));
return;
}
const tasks = [];
for (const path in unzipped) {
if (path.at(-1) !== SEPARATOR) {
tasks.push(writeFile(join(targetPath, path), unzipped[path]));
}
}
future.resolve(getFinalResult(tasks));
});
return await future.promise;
}
async function unzip(zipFilePath, targetPath) {
assertAbsolutePath(targetPath);
const fileRes = await readFile(zipFilePath);
return fileRes.andThenAsync((buffer) => {
return unzipBufferToTarget(buffer, targetPath);
});
}
async function unzipFromUrl(zipFileUrl, targetPath, requestInit) {
assertFileUrl(zipFileUrl);
assertAbsolutePath(targetPath);
const fetchRes = await fetchT(zipFileUrl, {
redirect: "follow",
...requestInit,
responseType: "arraybuffer",
abortable: false
});
return fetchRes.andThenAsync((buffer) => {
return unzipBufferToTarget(buffer, targetPath);
});
}
function uploadFile(filePath, fileUrl, requestInit) {
assertFileUrl(fileUrl);
let aborted = false;
let fetchTask;
const response = (async () => {
const fileRes = await readBlobFile(filePath);
return fileRes.andThenAsync(async (file) => {
if (aborted) {
return Err(createAbortError());
}
const {
// default file name
filename = basename(filePath),
...rest
} = requestInit ?? {};
const formData = new FormData();
formData.append(filename, file, filename);
fetchTask = fetchT(fileUrl, {
method: "POST",
...rest,
abortable: true,
body: formData
});
return fetchTask.response;
});
})();
return {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
abort(reason) {
aborted = true;
fetchTask?.abort(reason);
},
get aborted() {
return aborted;
},
get response() {
return response;
}
};
}
async function zipTo(zippable, zipFilePath) {
const future = new Future();
fflate.zip(zippable, {
consume: true
}, async (err, u8a) => {
if (err) {
future.resolve(Err(err));
return;
}
if (zipFilePath) {
const res = await writeFile(zipFilePath, u8a);
future.resolve(res);
} else {
future.resolve(Ok(u8a));
}
});
return await future.promise;
}
async function zip(sourcePath, zipFilePath, options) {
if (typeof zipFilePath === "string") {
assertAbsolutePath(zipFilePath);
} else {
options = zipFilePath;
zipFilePath = void 0;
}
const statRes = await stat(sourcePath);
return statRes.andThenAsync(async (handle) => {
const sourceName = basename(sourcePath);
const zippable = {};
if (isFileHandle(handle)) {
const data = await getFileDataByHandle(handle);
zippable[sourceName] = data;
} else {
const readDirRes = await readDir(sourcePath, {
recursive: true
});
if (readDirRes.isErr()) {
return readDirRes.asErr();
}
const preserveRoot = options?.preserveRoot ?? true;
for await (const { path, handle: handle2 } of readDirRes.unwrap()) {
if (isFileHandle(handle2)) {
const entryName = preserveRoot ? join(sourceName, path) : path;
const data = await getFileDataByHandle(handle2);
zippable[entryName] = data;
}
}
}
return zipTo(zippable, zipFilePath);
});
}
async function zipFromUrl(sourceUrl, zipFilePath, requestInit) {
assertFileUrl(sourceUrl);
if (typeof zipFilePath === "string") {
assertAbsolutePath(zipFilePath);
} else {
requestInit = zipFilePath;
zipFilePath = void 0;
}
const fetchRes = await fetchT(sourceUrl, {
redirect: "follow",
...requestInit,
responseType: "arraybuffer",
abortable: false
});
return fetchRes.andThenAsync((buffer) => {
const sourceName = basename(sourceUrl);
const zippable = {};
zippable[sourceName] = new Uint8Array(buffer);
return zipTo(zippable, zipFilePath);
});
}
function isOPFSSupported() {
return typeof navigator?.storage?.getDirectory === "function";
}
function serializeError(error) {
return error ? {
name: error.name,
message: error.message
} : error;
}
function deserializeError(error) {
const err = new Error(error.message);
err.name = error.name;
return err;
}
async function serializeFile(file) {
const ab = await file.arrayBuffer();
return {
name: file.name,
type: file.type,
lastModified: file.lastModified,
size: ab.byteLength,
data: ab
};
}
let globalOpTimeout = 1e3;
function setGlobalOpTimeout(timeout) {
globalOpTimeout = timeout;
}
function sleepUntil(condition) {
const start = Date.now();
while (!condition()) {
if (Date.now() - start > globalOpTimeout) {
const error = new Error("Operating Timeout");
error.name = TIMEOUT_ERROR;
throw error;
}
}
}
var WorkerAsyncOp = /* @__PURE__ */ ((WorkerAsyncOp2) => {
WorkerAsyncOp2[WorkerAsyncOp2["createFile"] = 0] = "createFile";
WorkerAsyncOp2[WorkerAsyncOp2["mkdir"] = 1] = "mkdir";
WorkerAsyncOp2[WorkerAsyncOp2["move"] = 2] = "move";
WorkerAsyncOp2[WorkerAsyncOp2["readDir"] = 3] = "readDir";
WorkerAsyncOp2[WorkerAsyncOp2["remove"] = 4] = "remove";
WorkerAsyncOp2[WorkerAsyncOp2["stat"] = 5] = "stat";
WorkerAsyncOp2[WorkerAsyncOp2["writeFile"] = 6] = "writeFile";
WorkerAsyncOp2[WorkerAsyncOp2["appendFile"] = 7] = "appendFile";
WorkerAsyncOp2[WorkerAsyncOp2["copy"] = 8] = "copy";
WorkerAsyncOp2[WorkerAsyncOp2["emptyDir"] = 9] = "emptyDir";
WorkerAsyncOp2[WorkerAsyncOp2["exists"] = 10] = "exists";
WorkerAsyncOp2[WorkerAsyncOp2["deleteTemp"] = 11] = "deleteTemp";
WorkerAsyncOp2[WorkerAsyncOp2["mkTemp"] = 12] = "mkTemp";
WorkerAsyncOp2[WorkerAsyncOp2["pruneTemp"] = 13] = "pruneTemp";
WorkerAsyncOp2[WorkerAsyncOp2["readBlobFile"] = 14] = "readBlobFile";
WorkerAsyncOp2[WorkerAsyncOp2["unzip"] = 15] = "unzip";
WorkerAsyncOp2[WorkerAsyncOp2["zip"] = 16] = "zip";
return WorkerAsyncOp2;
})(WorkerAsyncOp || {});
const MAIN_LOCK_INDEX = 0;
const WORKER_LOCK_INDEX = 1;
const DATA_INDEX = 2;
const MAIN_LOCKED = 1;
const MAIN_UNLOCKED = 0;
const WORKER_LOCKED = MAIN_UNLOCKED;
const WORKER_UNLOCKED = MAIN_LOCKED;
let encoder;
let decoder;
function getEncoder() {
encoder ??= new TextEncoder();
return encoder;
}
function getDecoder() {
decoder ??= new TextDecoder();
return decoder;
}
function encodeToBuffer(data) {
const str = JSON.stringify(data);
return getEncoder().encode(str);
}
function decodeFromBuffer(data) {
const str = decodeToString(data);
return JSON.parse(str);
}
function decodeToString(data) {
return getDecoder().decode(data);
}
class SyncMessenger {
// View of SharedArrayBuffer, used to communicate between main thread and worker.
i32a;
// View of the same SharedArrayBuffer, used to read and write binary data.
u8a;
// 4 int: MAIN_LOCK_INDEX WORKER_LOCK_INDEX DATA_INDEX NOT_USE
headerLength = 4 * 4;
// maximum length of data to be sent. If data is longer than this, it will throw an error.
maxDataLength;
constructor(sab) {
this.i32a = new Int32Array(sab);
this.u8a = new Uint8Array(sab);
this.maxDataLength = sab.byteLength - this.headerLength;
}
}
function callWorkerFromMain(messenger, data) {
const { i32a, u8a, headerLength, maxDataLength } = messenger;
const requestLength = data.byteLength;
if (requestLength > maxDataLength) {
throw new RangeError(`Request is too large: ${requestLength} > ${maxDataLength}. Consider grow the size of SharedArrayBuffer.`);
}
Atomics.store(i32a, MAIN_LOCK_INDEX, MAIN_LOCKED);
i32a[DATA_INDEX] = requestLength;
u8a.set(data, headerLength);
Atomics.store(i32a, WORKER_LOCK_INDEX, WORKER_UNLOCKED);
sleepUntil(() => Atomics.load(i32a, MAIN_LOCK_INDEX) === MAIN_UNLOCKED);
const responseLength = i32a[DATA_INDEX];
const response = u8a.slice(headerLength, headerLength + responseLength);
return response;
}
async function respondToMainFromWorker(messenger, transfer) {
const { i32a, u8a, headerLength, maxDataLength } = messenger;
while (true) {
if (Atomics.load(i32a, WORKER_LOCK_INDEX) === WORKER_UNLOCKED) {
break;
}
}
const requestLength = i32a[DATA_INDEX];
const data = u8a.slice(headerLength, headerLength + requestLength);
let response = await transfer(data);
const responseLength = response.byteLength;
if (responseLength > maxDataLength) {
const message = `Response is too large: ${responseLength} > ${maxDataLength}. Consider grow the size of SharedArrayBuffer.`;
response = encodeToBuffer([{
name: "RangeError",
message
}]);
if (response.byteLength > maxDataLength) {
Atomics.store(i32a, WORKER_LOCK_INDEX, WORKER_LOCKED);
throw new RangeError(message);
}
}
i32a[DATA_INDEX] = response.byteLength;
u8a.set(response, headerLength);
Atomics.store(i32a, WORKER_LOCK_INDEX, WORKER_LOCKED);
Atomics.store(i32a, MAIN_LOCK_INDEX, MAIN_UNLOCKED);
}
const asyncOps = {
[WorkerAsyncOp.createFile]: createFile,
[WorkerAsyncOp.mkdir]: mkdir,
[WorkerAsyncOp.move]: move,
[WorkerAsyncOp.readDir]: readDir,
[WorkerAsyncOp.remove]: remove,
[WorkerAsyncOp.stat]: stat,
[WorkerAsyncOp.writeFile]: writeFile,
[WorkerAsyncOp.appendFile]: appendFile,
[WorkerAsyncOp.copy]: copy,
[WorkerAsyncOp.emptyDir]: emptyDir,
[WorkerAsyncOp.exists]: exists,
[WorkerAsyncOp.deleteTemp]: deleteTemp,
[WorkerAsyncOp.mkTemp]: mkTemp,
[WorkerAsyncOp.pruneTemp]: pruneTemp,
[WorkerAsyncOp.readBlobFile]: readBlobFile,
[WorkerAsyncOp.unzip]: unzip,
[WorkerAsyncOp.zip]: zip
};
let messenger$1;
function startSyncAgent() {
if (typeof window !== "undefined") {
throw new Error("Only can use in worker");
}
if (messenger$1) {
throw new Error("Worker messenger already started");
}
addEventListener("message", (event) => {
const sab = event.data;
if (!(sab instanceof SharedArrayBuffer)) {
throw new TypeError("Only can post SharedArrayBuffer to Worker");
}
messenger$1 = new SyncMessenger(sab);
postMessage(true);
runWorkerLoop();
});
}
async function runWorkerLoop() {
while (true) {
try {
await respondToMainFromWorker(messenger$1, async (data) => {
const [op, ...args] = decodeFromBuffer(data);
if (op === WorkerAsyncOp.writeFile || op === WorkerAsyncOp.appendFile) {
if (Array.isArray(args[1])) {
args[1] = new Uint8Array(args[1]);
}
} else if (op === WorkerAsyncOp.pruneTemp) {
args[0] = new Date(args[0]);
}
let response;
const handle = asyncOps[op];
try {
const res = await handle(...args);
if (res.isErr()) {
response = encodeToBuffer([serializeError(res.unwrapErr())]);
} else {
let rawResponse;
if (op === WorkerAsyncOp.readBlobFile) {
const file = res.unwrap();
const fileLike = await serializeFile(file);
rawResponse = {
...fileLike,
// for serialize
data: [...new Uint8Array(fileLike.data)]
};
} else if (op === WorkerAsyncOp.readDir) {
const iterator = res.unwrap();
const entries = [];
for await (const { path, handle: handle2 } of iterator) {
const handleLike = await toFileSystemHandleLike(handle2);
entries.push({
path,
handle: handleLike
});
}
rawResponse = entries;
} else if (op === WorkerAsyncOp.stat) {
const handle2 = res.unwrap();
const data2 = await toFileSystemHandleLike(handle2);
rawResponse = data2;
} else if (op === WorkerAsyncOp.zip) {
const data2 = res.unwrap();
rawResponse = data2 instanceof Uint8Array ? [...data2] : data2;
} else {
rawResponse = res.unwrap();
}
response = encodeToBuffer([null, rawResponse]);
}
} catch (e) {
response = encodeToBuffer([serializeError(e)]);
}
return response;
});
} catch (err) {
console.error(err instanceof Error ? err.stack : err);
}
}
}
let messenger;
function connectSyncAgent(options) {
if (typeof window === "undefined") {
throw new Error("Only can use in main thread");
}
if (messenger) {
throw new Error("Main messenger already started");
}
return new Promise((resolve) => {
const {
worker,
bufferLength = 1024 * 1024,
opTimeout = 1e3
} = options;
invariant(worker instanceof Worker || worker instanceof URL || typeof worker === "string" && worker, () => "worker must be Worker or valid URL(string)");
invariant(bufferLength > 16 && bufferLength % 4 === 0, () => "bufferLength must be a multiple of 4");
invariant(Number.isInteger(opTimeout) && opTimeout > 0, () => "opTimeout must be integer and greater than 0");
setGlobalOpTimeout(opTimeout);
const workerAdapter = worker instanceof Worker ? worker : new Worker(worker);
const sab = new SharedArrayBuffer(bufferLength);
workerAdapter.addEventListener("message", (event) => {
if (event.data) {
messenger = new SyncMessenger(sab);
resolve();
}
});
workerAdapter.postMessage(sab);
});
}
function getSyncMessenger() {
return messenger;
}
function setSyncMessenger(syncMessenger) {
invariant(syncMessenger != null, () => "syncMessenger is null or undefined");
messenger = syncMessenger;
}
function callWorkerOp(op, ...args) {
if (!messenger) {
return Err(new Error("Worker not initialized. Come back later."));
}
const request = [op, ...args];
const requestData = encodeToBuffer(request);
try {
const response = callWorkerFromMain(messenger, requestData);
const decodedResponse = decodeFromBuffer(response);
const err = decodedResponse[0];
const result = err ? Err(deserializeError(err)) : Ok(decodedResponse[1] ?? void 0);
return result;
} catch (err) {
return Err(err);
}
}
function createFileSync(filePath) {
return callWorkerOp(WorkerAsyncOp.createFile, filePath);
}
function mkdirSync(dirPath) {
return callWorkerOp(WorkerAsyncOp.mkdir, dirPath);
}
function moveSync(srcPath, destPath, options) {
return callWorkerOp(WorkerAsyncOp.move, srcPath, destPath, options);
}
function readDirSync(dirPath, options) {
return callWorkerOp(WorkerAsyncOp.readDir, dirPath, options);
}
function readFileSync(filePath, options) {
const res = callWorkerOp(WorkerAsyncOp.readBlobFile, filePath);
return res.map((file) => {
const u8a = new Uint8Array(file.data);
file.data = u8a.buffer.slice(u8a.byteOffset, u8a.byteOffset + u8a.byteLength);
switch (options?.encoding) {
case "blob": {
return file;
}
case "utf8": {
return decodeToString(new Uint8Array(file.data));
}
default: {
return file.data;
}
}
});
}
function removeSync(path) {
return callWorkerOp(WorkerAsyncOp.remove, path);
}
function statSync(path) {
return callWorkerOp(WorkerAsyncOp.stat, path);
}
function serializeWriteContents(contents) {
return contents instanceof ArrayBuffer ? [...new Uint8Array(contents)] : ArrayBuffer.isView(contents) ? [...new Uint8Array(contents.buffer)] : contents;
}
function writeFileSync(filePath, contents, options) {
return callWorkerOp(WorkerAsyncOp.writeFile, filePath, serializeWriteContents(contents), options);
}
function appendFileSync(filePath, contents) {
return callWorkerOp(WorkerAsyncOp.appendFile, filePath, serializeWriteContents(contents));
}
function copySync(srcPath, destPath, options) {
return callWorkerOp(WorkerAsyncOp.copy, srcPath, destPath, options);
}
function emptyDirSync(dirPath) {
return callWorkerOp(WorkerAsyncOp.emptyDir, dirPath);
}
function existsSync(path, options) {
return callWorkerOp(WorkerAsyncOp.exists, path, options);
}
function deleteTempSync() {
return callWorkerOp(WorkerAsyncOp.deleteTemp);
}
function mkTempSync(options) {
return callWorkerOp(WorkerAsyncOp.mkTemp, options);
}
function pruneTempSync(expired) {
return callWorkerOp(WorkerAsyncOp.pruneTemp, expired);
}
function readBlobFileSync(filePath) {
return readFileSync(filePath, {
encoding: "blob"
});
}
function readJsonFileSync(filePath) {
return readTextFileSync(filePath).andThen((contents) => {
try {
return Ok(JSON.parse(contents));
} catch (e) {
return Err(e);
}
});
}
function readTextFileSync(filePath) {
return readFileSync(filePath, {
encoding: "utf8"
});
}
function unzipSync(zipFilePath, targetPath) {
return callWorkerOp(WorkerAsyncOp.unzip, zipFilePath, targetPath);
}
function zipSync(sourcePath, zipFilePath, options) {
const res = callWorkerOp(WorkerAsyncOp.zip, sourcePath, zipFilePath, options);
return res.map((data) => {
return data ? new Uint8Array(data) : data;
});
}
export { CURRENT_DIR, NOT_FOUND_ERROR, ROOT_DIR, TMP_DIR, appendFile, appendFileSync, assertAbsolutePath, assertFileUrl, connectSyncAgent, copy, copySync, createFile, createFileSync, deleteTemp, deleteTempSync, downloadFile, emptyDir, emptyDirSync, exists, existsSync, generateTempPath, getFileDataByHandle, getSyncMessenger, isDirectoryHandle, isFileHandle, isFileHandleLike, isOPFSSupported, isTempPath, mkTemp, mkTempSync, mkdir, mkdirSync, move, moveSync, pruneTemp, pruneTempSync, readBlobFile, readBlobFileSync, readDir, readDirSync, readFile, readFileSync, readJsonFile, readJsonFileSync, readTextFile, readTextFileSync, remove, removeSync, setSyncMessenger, startSyncAgent, stat, statSync, toFileSystemHandleLike, unzip, unzipFromUrl, unzipSync, uploadFile, writeFile, writeFileSync, zip, zipFromUrl, zipSync };
//# sourceMappingURL=main.mjs.map