UNPKG

@zenfs/core

Version:

A filesystem, anywhere

168 lines (144 loc) 4.7 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { Errno, ErrnoError, type ErrnoErrorJSON } from '../../error.js'; import type { FileSystem } from '../../filesystem.js'; import type { Backend, FilesystemOf } from '../backend.js'; import { handleRequest, PortFile, type PortFS } from './fs.js'; import type { FileOrFSRequest } from './fs.js'; type _MessageEvent<T = any> = T | { data: T }; /** @internal */ export interface Port { postMessage(value: unknown): void; on?(event: 'message', listener: (value: unknown) => void): this; off?(event: 'message', listener: (value: unknown) => void): this; addEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void; removeEventListener?(type: 'message', listener: (ev: _MessageEvent) => void): void; } export interface Options { /** * The target port that you want to connect to, or the current port if in a port context. */ port: Port; /** * How long to wait for a request to complete */ timeout?: number; } /** * An RPC message */ export interface Message { _zenfs: true; scope: 'fs' | 'file'; id: string; method: string; stack: string; } export interface Request extends Message { args: unknown[]; } interface _ResponseWithError extends Message { error: true; value: ErrnoErrorJSON | string; } interface _ResponseWithValue<T> extends Message { error: false; value: Awaited<T> extends File ? FileData : Awaited<T>; } export type Response<T = unknown> = _ResponseWithError | _ResponseWithValue<T>; export interface FileData { fd: number; path: string; position: number; } function isFileData(value: unknown): value is FileData { return typeof value == 'object' && value != null && 'fd' in value && 'path' in value && 'position' in value; } export { FileData as File }; // general types export function isMessage(arg: unknown): arg is Message { return typeof arg == 'object' && arg != null && '_zenfs' in arg && !!arg._zenfs; } type _Executor = Parameters<ConstructorParameters<typeof Promise<any>>[0]>; export interface Executor { resolve: _Executor[0]; reject: _Executor[1]; fs?: PortFS; } const executors: Map<string, Executor> = new Map(); export function request<const TRequest extends Request, TValue>( request: Omit<TRequest, 'id' | 'stack' | '_zenfs'>, { port, timeout = 1000, fs }: Partial<Options> & { fs?: PortFS } = {} ): Promise<TValue> { const stack = '\n' + new Error().stack!.slice('Error:'.length); if (!port) { throw ErrnoError.With('EINVAL'); } return new Promise<TValue>((resolve, reject) => { const id = Math.random().toString(16).slice(10); executors.set(id, { resolve, reject, fs }); port.postMessage({ ...request, _zenfs: true, id, stack }); const _ = setTimeout(() => { const error = new ErrnoError(Errno.EIO, 'RPC Failed'); error.stack += stack; reject(error); if (typeof _ == 'object') _.unref(); }, timeout); }); } export function handleResponse<const TResponse extends Response>(response: TResponse): void { if (!isMessage(response)) { return; } const { id, value, error, stack } = response; if (!executors.has(id)) { const error = new ErrnoError(Errno.EIO, 'Invalid RPC id:' + id); error.stack += stack; throw error; } const { resolve, reject, fs } = executors.get(id)!; if (error) { const e = typeof value == 'string' ? new Error(value) : ErrnoError.fromJSON(value); e.stack += stack; reject(e); executors.delete(id); return; } if (isFileData(value)) { const { fd, path, position } = value; const file = new PortFile(fs!, fd, path, position); resolve(file); executors.delete(id); return; } resolve(value); executors.delete(id); return; } export function attach<T extends Message>(port: Port, handler: (message: T) => unknown) { if (!port) { throw ErrnoError.With('EINVAL'); } port['on' in port ? 'on' : 'addEventListener']!('message', (message: T | _MessageEvent<T>) => { handler('data' in message ? message.data : message); }); } export function detach<T extends Message>(port: Port, handler: (message: T) => unknown) { if (!port) { throw ErrnoError.With('EINVAL'); } port['off' in port ? 'off' : 'removeEventListener']!('message', (message: T | _MessageEvent<T>) => { handler('data' in message ? message.data : message); }); } export function catchMessages<T extends Backend>(port: Port): (fs: FilesystemOf<T>) => void { const events: _MessageEvent[] = []; const handler = events.push.bind(events); attach(port, handler); return function (fs: FileSystem) { detach(port, handler); for (const event of events) { const request = ('data' in event ? event.data : event) as FileOrFSRequest; void handleRequest(port, fs, request); } }; }