@temporalio/worker
Version:
Temporal.io SDK Worker sub-package
297 lines (294 loc) • 14.1 kB
JavaScript
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WorkflowCodeBundler = exports.disallowedModules = exports.disallowedBuiltinModules = exports.allowedBuiltinModules = exports.defaultWorkflowInterceptorModules = void 0;
exports.moduleMatches = moduleMatches;
exports.bundleWorkflowCode = bundleWorkflowCode;
const realFS = __importStar(require("node:fs"));
const node_module_1 = require("node:module");
const node_path_1 = __importDefault(require("node:path"));
const node_util_1 = __importDefault(require("node:util"));
const unionfs = __importStar(require("unionfs"));
const memfs = __importStar(require("memfs"));
const webpack_1 = require("webpack");
const logger_1 = require("../logger");
const utils_1 = require("../utils");
exports.defaultWorkflowInterceptorModules = [require.resolve('../workflow-log-interceptor')];
exports.allowedBuiltinModules = ['assert', 'url', 'util'];
exports.disallowedBuiltinModules = node_module_1.builtinModules.filter((module) => !exports.allowedBuiltinModules.includes(module));
exports.disallowedModules = [
...exports.disallowedBuiltinModules,
'@temporalio/activity',
'@temporalio/client',
'@temporalio/worker',
'@temporalio/common/lib/internal-non-workflow',
'@temporalio/interceptors-opentelemetry/lib/client',
'@temporalio/interceptors-opentelemetry/lib/worker',
'@temporalio/testing',
'@temporalio/core-bridge',
];
function moduleMatches(userModule, modules) {
return modules.some((module) => userModule === module || userModule.startsWith(`${module}/`));
}
/**
* Builds a V8 Isolate by bundling provided Workflows using webpack.
*
* @param workflowsPath all Workflows found in path will be put in the bundle
* @param workflowInterceptorModules list of interceptor modules to register on Workflow creation
*/
class WorkflowCodeBundler {
foundProblematicModules = new Set();
logger;
workflowsPath;
workflowInterceptorModules;
payloadConverterPath;
failureConverterPath;
ignoreModules;
webpackConfigHook;
constructor({ logger, workflowsPath, payloadConverterPath, failureConverterPath, workflowInterceptorModules, ignoreModules, webpackConfigHook, }) {
this.logger = logger ?? new logger_1.DefaultLogger('INFO');
this.workflowsPath = workflowsPath;
this.payloadConverterPath = payloadConverterPath;
this.failureConverterPath = failureConverterPath;
this.workflowInterceptorModules = workflowInterceptorModules ?? [];
this.ignoreModules = ignoreModules ?? [];
this.webpackConfigHook = webpackConfigHook ?? ((config) => config);
}
/**
* @return a {@link WorkflowBundle} containing bundled code, including inlined source map
*/
async createBundle() {
const vol = new memfs.Volume();
const ufs = new unionfs.Union();
/**
* readdir and exclude sourcemaps and d.ts files
*/
function readdir(...args) {
// Help TS a bit because readdir has multiple signatures
const callback = args.pop();
const newArgs = [
...args,
(err, files) => {
if (err !== null) {
callback(err, []);
return;
}
callback(null, files.filter((f) => /\.[jt]s$/.test(node_path_1.default.extname(f)) && !f.endsWith('.d.ts')));
},
];
return realFS.readdir(...newArgs);
}
// Cast because the type definitions are inaccurate
const memoryFs = memfs.createFsFromVolume(vol);
ufs.use(memoryFs).use({ ...realFS, readdir: readdir });
const distDir = '/dist';
const entrypointPath = this.makeEntrypointPath(ufs, this.workflowsPath);
this.genEntrypoint(vol, entrypointPath);
const bundleFilePath = await this.bundle(ufs, memoryFs, entrypointPath, distDir);
let code = memoryFs.readFileSync(bundleFilePath, 'utf8');
// Replace webpack's module cache with an object injected by the runtime.
// This is the key to reusing a single v8 context.
code = code.replace('var __webpack_module_cache__ = {}', 'var __webpack_module_cache__ = globalThis.__webpack_module_cache__');
this.logger.info('Workflow bundle created', { size: `${(0, utils_1.toMB)(code.length)}MB` });
// Cast because the type definitions are inaccurate
return {
sourceMap: 'deprecated: this is no longer in use\n',
code,
};
}
makeEntrypointPath(fs, workflowsPath) {
const stat = fs.statSync(workflowsPath);
if (stat.isFile()) {
// workflowsPath is a file; make the entrypoint a sibling of that file
const { root, dir, name } = node_path_1.default.parse(workflowsPath);
return node_path_1.default.format({ root, dir, base: `${name}-autogenerated-entrypoint.cjs` });
}
else {
// workflowsPath is a directory; make the entrypoint a sibling of that directory
const { root, dir, base } = node_path_1.default.parse(workflowsPath);
return node_path_1.default.format({ root, dir, base: `${base}-autogenerated-entrypoint.cjs` });
}
}
/**
* Creates the main entrypoint for the generated webpack library.
*
* Exports all detected Workflow implementations and some workflow libraries to be used by the Worker.
*/
genEntrypoint(vol, target) {
const interceptorImports = [...new Set(this.workflowInterceptorModules)]
.map((v) => `require(/* webpackMode: "eager" */ ${JSON.stringify(v)})`)
.join(', \n');
const code = `
const api = require('@temporalio/workflow/lib/worker-interface.js');
exports.api = api;
const { overrideGlobals } = require('@temporalio/workflow/lib/global-overrides.js');
overrideGlobals();
exports.importWorkflows = function importWorkflows() {
return require(/* webpackMode: "eager" */ ${JSON.stringify(this.workflowsPath)});
}
exports.importInterceptors = function importInterceptors() {
return [
${interceptorImports}
];
}
`;
try {
vol.mkdirSync(node_path_1.default.dirname(target), { recursive: true });
}
catch (err) {
if (err.code !== 'EEXIST')
throw err;
}
vol.writeFileSync(target, code);
}
/**
* Run webpack
*/
async bundle(inputFilesystem, outputFilesystem, entry, distDir) {
const captureProblematicModules = async (data, _callback) => {
// Ignore the "node:" prefix if any.
const module = data.request?.startsWith('node:')
? data.request.slice('node:'.length)
: data.request ?? '';
if (moduleMatches(module, exports.disallowedModules) && !moduleMatches(module, this.ignoreModules)) {
this.foundProblematicModules.add(module);
}
return undefined;
};
const options = {
resolve: {
// https://webpack.js.org/configuration/resolve/#resolvemodules
modules: [node_path_1.default.resolve(__dirname, 'module-overrides'), 'node_modules'],
extensions: ['.ts', '.js'],
extensionAlias: { '.js': ['.ts', '.js'] },
alias: {
__temporal_custom_payload_converter$: this.payloadConverterPath ?? false,
__temporal_custom_failure_converter$: this.failureConverterPath ?? false,
...Object.fromEntries([...this.ignoreModules, ...exports.disallowedModules].map((m) => [m, false])),
},
},
externals: captureProblematicModules,
module: {
rules: [
{
test: /\.js$/,
enforce: 'pre',
use: [require.resolve('source-map-loader')],
},
{
test: /\.ts$/,
exclude: /node_modules/,
use: {
loader: require.resolve('swc-loader'),
options: {
sourceMap: true,
jsc: {
target: 'es2017',
parser: {
syntax: 'typescript',
decorators: true,
},
},
},
},
},
],
},
entry: [entry],
mode: 'development',
devtool: 'inline-source-map',
output: {
path: distDir,
filename: 'workflow-bundle-[fullhash].js',
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
library: '__TEMPORAL__',
},
ignoreWarnings: [/Failed to parse source map/],
};
const compiler = (0, webpack_1.webpack)(this.webpackConfigHook(options));
// Cast to any because the type declarations are inaccurate
compiler.inputFileSystem = inputFilesystem;
// Don't use ufs due to a strange bug on Windows:
// https://github.com/temporalio/sdk-typescript/pull/554
compiler.outputFileSystem = outputFilesystem;
try {
return await new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (stats !== undefined) {
const hasError = stats.hasErrors();
// To debug webpack build:
// const lines = stats.toString({ preset: 'verbose' }).split('\n');
const webpackOutput = stats.toString({
chunks: false,
colors: (0, logger_1.hasColorSupport)(this.logger),
errorDetails: true,
});
this.logger[hasError ? 'error' : 'info'](webpackOutput);
if (hasError) {
reject(new Error("Webpack finished with errors, if you're unsure what went wrong, visit our troubleshooting page at https://docs.temporal.io/develop/typescript/debugging#webpack-errors"));
}
if (this.foundProblematicModules.size) {
const err = new Error(`Your Workflow code (or a library used by your Workflow code) is importing the following disallowed modules:\n` +
Array.from(this.foundProblematicModules)
.map((module) => ` - '${module}'\n`)
.join('') +
`These modules can't be used in workflow context as they might break determinism.` +
`HINT: Consider the following options:\n` +
` • Make sure that activity code is not imported from workflow code. Use \`import type\` to import activity function signatures.\n` +
` • Move code that has non-deterministic behaviour to activities.\n` +
` • If you know for sure that a disallowed module will not be used at runtime, add its name to 'WorkerOptions.bundlerOptions.ignoreModules' in order to dismiss this warning.\n` +
`See also: https://typescript.temporal.io/api/namespaces/worker#workflowbundleoption and https://docs.temporal.io/typescript/determinism.`);
reject(err);
}
const outputFilename = Object.keys(stats.compilation.assets)[0];
if (!err) {
resolve(node_path_1.default.join(distDir, outputFilename));
}
}
reject(err);
});
});
}
finally {
await node_util_1.default.promisify(compiler.close).bind(compiler)();
}
}
}
exports.WorkflowCodeBundler = WorkflowCodeBundler;
/**
* Create a bundle to pass to {@link WorkerOptions.workflowBundle}. Helpful for reducing Worker startup time in
* production.
*
* When using with {@link Worker.runReplayHistory}, make sure to pass the same interceptors and payload converter used
* when the history was generated.
*/
async function bundleWorkflowCode(options) {
const bundler = new WorkflowCodeBundler(options);
return await bundler.createBundle();
}
//# sourceMappingURL=bundler.js.map
;