UNPKG

@temporalio/worker

Version:
315 lines (302 loc) 13.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReusableVMWorkflow = exports.ReusableVMWorkflowCreator = void 0; const node_vm_1 = __importDefault(require("node:vm")); const common_1 = require("@temporalio/common"); const core_bridge_1 = require("@temporalio/core-bridge"); const vm_shared_1 = require("./vm-shared"); const bun_1 = require("./bun"); const callIntoVmScript = new node_vm_1.default.Script(`__TEMPORAL_CALL_INTO_SCOPE()`); function generateNodeCallIntoScopeScript() { return `{ const __TEMPORAL_CALL_INTO_SCOPE = () => { const [holder, fn, args] = globalThis.__temporal_args; delete globalThis.__temporal_args; if (globalThis.__TEMPORAL_BAG_HOLDER__ !== holder) { if (globalThis.__TEMPORAL_BAG_HOLDER__ !== undefined) { globalThis.__TEMPORAL_BAG_HOLDER__.bag = Object.getOwnPropertyDescriptors(globalThis); } const toBeDeleted = new Set(Reflect.ownKeys(globalThis)); for (const prop of Reflect.ownKeys(holder.bag)) { if (holder.bag[prop].value !== globalThis[prop]) { Object.defineProperty(globalThis, prop, holder.bag[prop]); } toBeDeleted.delete(prop); } for (const prop of toBeDeleted) { delete globalThis[prop]; } globalThis.__TEMPORAL_BAG_HOLDER__ = holder; } return __TEMPORAL__.api[fn](...args); }; Object.defineProperty(globalThis, '__TEMPORAL_CALL_INTO_SCOPE', { value: __TEMPORAL_CALL_INTO_SCOPE, writable: false, enumerable: false, configurable: false }); }`; } // This is a workaround for a bug in Bun where Object.getOwnPropertyDescriptor returns // stale values for numeric properties after modification. We must read/write numeric // properties directly. function generateBunCallIntoScopeScript() { return `{ const __TEMPORAL_IS_NUMERIC_KEY = (key) => { if (typeof key === 'number') return true; if (typeof key === 'string') { const num = Number(key); return Number.isInteger(num) && num >= 0 && String(num) === key; } return false; }; const __TEMPORAL_CALL_INTO_SCOPE = () => { const [holder, fn, args] = globalThis.__temporal_args; delete globalThis.__temporal_args; if (globalThis.__TEMPORAL_BAG_HOLDER__ !== holder) { if (globalThis.__TEMPORAL_BAG_HOLDER__ !== undefined) { const bag = Object.getOwnPropertyDescriptors(globalThis); for (const prop of Reflect.ownKeys(bag)) { if (__TEMPORAL_IS_NUMERIC_KEY(prop)) { bag[prop].value = globalThis[prop]; } } globalThis.__TEMPORAL_BAG_HOLDER__.bag = bag; } const toBeDeleted = new Set(Reflect.ownKeys(globalThis)); for (const prop of Reflect.ownKeys(holder.bag)) { if (holder.bag[prop].value !== globalThis[prop]) { if (__TEMPORAL_IS_NUMERIC_KEY(prop)) { globalThis[prop] = holder.bag[prop].value; } else { Object.defineProperty(globalThis, prop, holder.bag[prop]); } } toBeDeleted.delete(prop); } for (const prop of toBeDeleted) { delete globalThis[prop]; } globalThis.__TEMPORAL_BAG_HOLDER__ = holder; } return __TEMPORAL__.api[fn](...args); }; Object.defineProperty(globalThis, '__TEMPORAL_CALL_INTO_SCOPE', { value: __TEMPORAL_CALL_INTO_SCOPE, writable: false, enumerable: false, configurable: false }); }`; } /** * A WorkflowCreator that creates VMWorkflows in the current isolate */ class ReusableVMWorkflowCreator { workflowBundle; isolateExecutionTimeoutMs; registeredActivityNames; /** * TODO(bergundy): Get rid of this static state somehow */ static unhandledRejectionHandlerHasBeenSet = false; static workflowByRunId = new Map(); /** * Optional context - this attribute is deleted upon on {@link destroy} * * Use the {@link context} getter instead */ _context; pristineObj; constructor(script, workflowBundle, isolateExecutionTimeoutMs, /** Known activity names registered on the executing worker */ registeredActivityNames) { this.workflowBundle = workflowBundle; this.isolateExecutionTimeoutMs = isolateExecutionTimeoutMs; this.registeredActivityNames = registeredActivityNames; if (!ReusableVMWorkflowCreator.unhandledRejectionHandlerHasBeenSet) { (0, vm_shared_1.setUnhandledRejectionHandler)((runId) => ReusableVMWorkflowCreator.workflowByRunId.get(runId)); ReusableVMWorkflowCreator.unhandledRejectionHandlerHasBeenSet = true; } this._context = node_vm_1.default.createContext({}, { microtaskMode: 'afterEvaluate' }); node_vm_1.default.runInContext(bun_1.isBun ? generateBunCallIntoScopeScript() : generateNodeCallIntoScopeScript(), this._context, { timeout: isolateExecutionTimeoutMs, displayErrors: true, }); const sharedModules = new Map(); const __webpack_module_cache__ = new Proxy({}, { get: (_, p) => { // Try the shared modules first const sharedModule = sharedModules.get(p); if (sharedModule) { return sharedModule; } const moduleCache = this.context.__TEMPORAL_ACTIVATOR__?.moduleCache; return moduleCache?.get(p); }, set: (_, p, val) => { const moduleCache = this.context.__TEMPORAL_ACTIVATOR__?.moduleCache; if (moduleCache != null) { moduleCache.set(p, val); } else { // Workflow has not yet been loaded, share the module sharedModules.set(p, val); } return true; }, }); Object.defineProperty(this._context, '__webpack_module_cache__', { value: __webpack_module_cache__, writable: false, enumerable: false, configurable: false, }); this.injectGlobals(this._context); script.runInContext(this.context); // The V8 context is really composed of two distinct objects: the 'this._context' object on the outside, and another // internal object to which we only have access from the inside, which defines the built-in global properties. // Node makes some attempt at keeping the two in sync, but it's not perfect. To avoid various inconsistencies, // we capture the global variables from the inside of the V8 context. this.pristineObj = node_vm_1.default.runInContext(`Object.getOwnPropertyDescriptors(globalThis)`, this.context); for (const k of [ ...Object.getOwnPropertyNames(this.pristineObj), ...Object.getOwnPropertySymbols(this.pristineObj), ]) { if (k !== 'globalThis' && k !== '__temporal_globalSandboxDestructors') { const v = this.pristineObj[k]; v.value = deepFreeze(v.value); } } for (const v of sharedModules.values()) deepFreeze(v); } get context() { const { _context } = this; if (_context == null) { throw new common_1.IllegalStateError('Tried to use v8 context after Workflow creator was destroyed'); } return _context; } /** * Inject global objects as well as console.[log|...] into a vm context. * * Overridable for test purposes. */ injectGlobals(context) { (0, vm_shared_1.injectGlobals)(context); } /** * Create a workflow with given options */ async createWorkflow(options) { const context = this.context; const holder = { bag: this.pristineObj }; const { isolateExecutionTimeoutMs } = this; const workflowModule = new Proxy({}, { get(_, fn) { return (...args) => { // By the time we get out of this call, all microtasks will have been executed context.__temporal_args = [holder, fn, args]; return callIntoVmScript.runInContext(context, { timeout: isolateExecutionTimeoutMs, displayErrors: true, }); }; }, }); workflowModule.initRuntime({ ...options, sourceMap: this.workflowBundle.sourceMap, getTimeOfDay: core_bridge_1.native.getTimeOfDay, registeredActivityNames: this.registeredActivityNames, stackTracesEnabled: vm_shared_1.globalHandlers.promiseHookInstalled, }); const activator = context.__TEMPORAL_ACTIVATOR__; const newVM = new ReusableVMWorkflow(options.info.runId, context, activator, workflowModule); ReusableVMWorkflowCreator.workflowByRunId.set(options.info.runId, newVM); return newVM; } /** * Create a new instance, pre-compile scripts from given code. * * This method is generic to support subclassing. */ static async create(workflowBundle, isolateExecutionTimeoutMs, registeredActivityNames) { vm_shared_1.globalHandlers.install(); // Call is idempotent await vm_shared_1.globalHandlers.addWorkflowBundle(workflowBundle); const script = new node_vm_1.default.Script(workflowBundle.code, { filename: workflowBundle.filename }); return new this(script, workflowBundle, isolateExecutionTimeoutMs, registeredActivityNames); } /** * Cleanup the pre-compiled script */ async destroy() { try { node_vm_1.default.runInContext(`__TEMPORAL__.api.destroy()`, this.context); } finally { vm_shared_1.globalHandlers.removeWorkflowBundle(this.workflowBundle); delete this._context; } } } exports.ReusableVMWorkflowCreator = ReusableVMWorkflowCreator; /** * A Workflow implementation using Node.js' built-in `vm` module */ class ReusableVMWorkflow extends vm_shared_1.BaseVMWorkflow { async dispose() { this.workflowModule.dispose(); // In Bun, microtasks scheduled inside the VM context may not be processed // automatically due to lack of proper microtaskMode: 'afterEvaluate' support. // Drain the microtask queue to prevent state leakage to the next workflow // that will reuse this VM context. if (bun_1.isBun) await new Promise(setImmediate); ReusableVMWorkflowCreator.workflowByRunId.delete(this.runId); } } exports.ReusableVMWorkflow = ReusableVMWorkflow; /** * Call `Object.freeze()` recursively on an object. * * Note that there are limits to this approach, as traversing using getOwnPropertyXxx doesn't allow * reaching variables defined in internal scopes. That notably means that Map and Set classes, * are not frozen. Similarly, private properties and variables defined in closures are unreachable * and will therefore not be frozen. It is simply impossible to cover all potential cases. * * We also do not attempt to visit the prototype chain, as this would make it much harder to load * polyfills, and it is extremely unlikely anyway that one would modify the prototype of a built-in * object in a way that would have undesirable consequences (i.e. a polyfill function may actually * leak to another workflow context, but it wouldn't carry anything Workflow specific). * * This implementation is based on https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze. * Some implementatino decisions (e.g. freezing functions, not freezing prototypes, not handling Maps * and Sets, etc) are specific to the Reusable VM Workflow Sandbox use case, and may not be appropriate * for other use cases. For that reason, it is preferable to keep this function private to this module, * rather than exposing it as a reusable utility in the common package. */ function deepFreeze(object, visited = new WeakSet()) { if (object == null || visited.has(object) || (typeof object !== 'object' && typeof object !== 'function')) return object; visited.add(object); if (Object.isFrozen(object)) return object; if (typeof object === 'object') { // Retrieve the property names defined on object const propNames = [...Object.getOwnPropertyNames(object), ...Object.getOwnPropertySymbols(object)]; // Freeze properties before freezing self for (const name of propNames) { const value = object[name]; if (value && (typeof value === 'object' || typeof value === 'function')) { try { deepFreeze(value, visited); } catch (_err) { // This is okay, for various reasons, some objects can't be frozen, e.g. Uint8Array. } } } } return Object.freeze(object); } //# sourceMappingURL=reusable-vm.js.map