WIP: Node.js isolation primitive to run asynchronous worker-like operations without leaking async IO
1import type { 2 AsyncResourceFiber, 3 AsyncResourceNode, 4} from './asyncResourceGraph'; 5 6export type FiberErrorCode = 7 | 'FOREIGN_ASYNC_TRIGGER' 8 | 'PARENT_ASYNC_TRIGGER' 9 | 'FOREIGN_ASYNC_ABORTED' 10 | 'FIBER_ABORTED' 11 | 'FIBER_STALL'; 12 13const codeToMessage = ( 14 code: FiberErrorCode, 15 fiber: AsyncResourceFiber, 16 node: AsyncResourceNode 17): string => { 18 switch (code) { 19 case 'FOREIGN_ASYNC_TRIGGER': 20 return ( 21 `${fiber} tried to create ${node} which will be triggered by async IO from a different fiber.\n` + 22 'Fibers are isolated and may only create and reference async resources they have created themselves.' 23 ); 24 case 'PARENT_ASYNC_TRIGGER': 25 return ( 26 `${fiber} tried to create ${node} which will be triggered by async IO of this fiber's parent context.\n` + 27 'Fibers are isolated and may only create and reference async resources they have created themselves.' 28 ); 29 case 'FOREIGN_ASYNC_ABORTED': 30 return ( 31 `${fiber} used ${node} from another fiber which was aborted and can never resolve.\n` + 32 'Fibers may not share async resources, and may accidentally prevent each other from resolving if they do.' 33 ); 34 case 'FIBER_ABORTED': 35 return ( 36 `${fiber}'s ${node} was aborted and will never resolve.\n` + 37 "If you see this message, you're observing an internal forceful cancellation of a fiber and this error is expected." 38 ); 39 case 'FIBER_STALL': 40 return ( 41 `${fiber} has finished all of its work but won't resolve and pass control back to the parent fiber.\n` + 42 'This usally happens if a Promise is unresolved or if its async IO has been cancelled without a callback being handled.\n' + 43 `${node} is the last async resource the fiber got stuck on.` 44 ); 45 } 46}; 47 48const traceNode = ( 49 node: AsyncResourceNode, 50 fiber: AsyncResourceFiber, 51 depth = 1 52): string => { 53 let trace = `${node}`; 54 let origin: AsyncResourceNode | null = node; 55 for (let idx = 1; origin && origin !== fiber.root && idx <= depth; idx++) { 56 if (origin.frame) trace += `\n at ${origin.frame}`; 57 origin = origin.executionOrigin; 58 } 59 return trace; 60}; 61 62export class FiberError extends Error { 63 static stackTraceLimit = 10; 64 65 readonly fiber: AsyncResourceFiber; 66 readonly node: AsyncResourceNode; 67 readonly code: FiberErrorCode; 68 69 constructor( 70 code: FiberErrorCode, 71 fiber: AsyncResourceFiber, 72 node: AsyncResourceNode 73 ) { 74 super(codeToMessage(code, fiber, node)); 75 this.fiber = fiber; 76 this.node = node; 77 this.code = code; 78 } 79 80 get trace(): string { 81 let trace = traceNode(this.node, this.fiber); 82 if (this.node.triggerOrigin) 83 trace += `\ntriggered by ${traceNode(this.node.triggerOrigin, this.fiber, FiberError.stackTraceLimit)}`; 84 if (this.node.executionOrigin) 85 trace += `\nexecuted in ${traceNode(this.node.executionOrigin, this.fiber, FiberError.stackTraceLimit)}`; 86 return trace; 87 } 88 89 toString() { 90 return `${this.message.trim()}\n\n${this.trace}`; 91 } 92}