WIP: Node.js isolation primitive to run asynchronous worker-like operations without leaking async IO
1import { inspect } from 'node:util'; 2import { writeSync } from 'node:fs'; 3 4export const printf = (...args: unknown[]) => { 5 writeSync( 6 process.stderr.fd, 7 args 8 .map(arg => { 9 return typeof arg === 'object' && arg ? inspect(arg) : `${arg}`; 10 }) 11 .join(' ') + '\n' 12 ); 13}; 14 15const MAX_STACK_DEPTH = 10; 16 17const isNodeSite = (site: NodeJS.CallSite) => 18 !!site.getFileName()?.startsWith('node:'); 19const isNodeInternalSite = (site: NodeJS.CallSite) => 20 !!site.getFileName()?.startsWith('node:internal/'); 21 22export class StackFrame { 23 isToplevel: boolean; 24 isNative: boolean; 25 isConstructor: boolean; 26 typeName: string | null; 27 functionName: string | null; 28 methodName: string | null; 29 fileName: string | null; 30 lineNumber: number | null; 31 columnNumber: number | null; 32 constructor(stack: NodeJS.CallSite[], offset: number) { 33 let idx = offset; 34 let site: NodeJS.CallSite | undefined = stack[idx]; 35 while (idx < stack.length && isNodeSite((site = stack[idx]))) idx++; 36 this.fileName = site.getFileName() || null; 37 this.lineNumber = site.getLineNumber(); 38 this.columnNumber = site.getColumnNumber(); 39 this.functionName = site.getFunctionName(); 40 do { 41 this.isToplevel = site.isToplevel(); 42 this.isNative = site.isNative(); 43 this.isConstructor = site.isConstructor(); 44 this.typeName = site.getTypeName(); 45 this.functionName = site.getFunctionName(); 46 this.methodName = site.getMethodName(); 47 } while ( 48 idx > offset && 49 this.functionName === null && 50 this.methodName === null && 51 !isNodeInternalSite((site = stack[--idx])) 52 ); 53 } 54 toString() { 55 const prefix = this.isConstructor ? 'new ' : ''; 56 const namePrefix = 57 this.typeName !== null && this.typeName !== 'global' 58 ? `${this.typeName}.` 59 : ''; 60 const name = `${namePrefix}${this.functionName || this.methodName || '<anonymous>'}`; 61 let location = this.fileName || '<anonymous>'; 62 if (this.lineNumber != null) location += `:${this.lineNumber}`; 63 if (this.columnNumber != null) location += `:${this.columnNumber}`; 64 return `${prefix}${name}${!!name ? ' (' : ''}${location}${!!name ? ')' : ''}`; 65 } 66} 67 68export function getStackFrame(offset = 0): StackFrame | null { 69 const originalStackFormatter = Error.prepareStackTrace; 70 const originalStackTraceLimit = Error.stackTraceLimit; 71 try { 72 Error.stackTraceLimit = MAX_STACK_DEPTH + offset; 73 Error.prepareStackTrace = (_err, stack) => 74 new StackFrame(stack, 2 + offset); 75 return new Error().stack as any; 76 } finally { 77 Error.prepareStackTrace = originalStackFormatter; 78 Error.stackTraceLimit = originalStackTraceLimit; 79 } 80} 81 82export interface PromiseWithReject<T> { 83 promise: Promise<T>; 84 reject(reason?: any): void; 85} 86 87export function promiseWithReject<T>( 88 promise: Promise<T>, 89 onSettled: () => void 90): PromiseWithReject<T> { 91 let reject: PromiseWithReject<T>['reject']; 92 return { 93 promise: new Promise<T>((resolve, _reject) => { 94 promise.then( 95 result => { 96 resolve(result); 97 onSettled(); 98 }, 99 reason => { 100 _reject(reason); 101 onSettled(); 102 } 103 ); 104 reject = _reject; 105 }), 106 reject: reject!, 107 }; 108}