@camunda8/orchestration-cluster-api 10.0.0-alpha.43 → 10.0.0-alpha.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +3 -18
- package/dist/{CamundaClient-Dxa-jdNc.d.ts → CamundaClient-3psgxgtn.d.ts} +365 -50
- package/dist/{CamundaClient-DwHGY8e9.d.cts → CamundaClient-BiHf6wkM.d.cts} +365 -50
- package/dist/{chunk-JKBNNUPT.js → chunk-B5HJCAHJ.js} +79 -17
- package/dist/chunk-B5HJCAHJ.js.map +1 -0
- package/dist/effect/index.cjs +190 -80
- package/dist/effect/index.cjs.map +1 -1
- package/dist/effect/index.d.cts +14 -1
- package/dist/effect/index.d.ts +14 -1
- package/dist/effect/index.js +19 -3
- package/dist/effect/index.js.map +1 -1
- package/dist/index.cjs +222 -79
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.js +48 -2
- package/dist/index.js.map +1 -1
- package/dist/threadWorkerEntry.cjs +77 -48
- package/dist/threadWorkerEntry.cjs.map +1 -1
- package/dist/threadWorkerEntry.js +77 -48
- package/dist/threadWorkerEntry.js.map +1 -1
- package/dist/{zod.gen-2PU225MP.js → zod.gen-A3RZUVR4.js} +97 -63
- package/dist/zod.gen-A3RZUVR4.js.map +1 -0
- package/package.json +3 -3
- package/dist/chunk-JKBNNUPT.js.map +0 -1
- package/dist/zod.gen-2PU225MP.js.map +0 -1
|
@@ -43,8 +43,84 @@ function toModuleSpecifier(handlerModule) {
|
|
|
43
43
|
return isPath ? (0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(handlerModule)).href : handlerModule;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// src/runtime/
|
|
46
|
+
// src/runtime/threadJobProxy.ts
|
|
47
47
|
var JobActionReceipt = "JOB_ACTION_RECEIPT";
|
|
48
|
+
function createJobProxy(jobData, client) {
|
|
49
|
+
const acknowledged = { value: false };
|
|
50
|
+
const ack = () => {
|
|
51
|
+
acknowledged.value = true;
|
|
52
|
+
job.acknowledged = true;
|
|
53
|
+
};
|
|
54
|
+
const job = { ...jobData };
|
|
55
|
+
job.complete = async (variables = {}, result) => {
|
|
56
|
+
ack();
|
|
57
|
+
job._completionAction = {
|
|
58
|
+
method: "completeJob",
|
|
59
|
+
args: [
|
|
60
|
+
{
|
|
61
|
+
variables,
|
|
62
|
+
jobKey: jobData.jobKey,
|
|
63
|
+
...result !== void 0 && { result },
|
|
64
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
return JobActionReceipt;
|
|
69
|
+
};
|
|
70
|
+
job.fail = async (reason) => {
|
|
71
|
+
ack();
|
|
72
|
+
job._completionAction = {
|
|
73
|
+
method: "failJob",
|
|
74
|
+
args: [
|
|
75
|
+
{
|
|
76
|
+
...reason,
|
|
77
|
+
jobKey: jobData.jobKey,
|
|
78
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
};
|
|
82
|
+
return JobActionReceipt;
|
|
83
|
+
};
|
|
84
|
+
job.error = async (error) => {
|
|
85
|
+
ack();
|
|
86
|
+
job._completionAction = {
|
|
87
|
+
method: "throwJobError",
|
|
88
|
+
args: [
|
|
89
|
+
{
|
|
90
|
+
...error,
|
|
91
|
+
jobKey: jobData.jobKey,
|
|
92
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
93
|
+
}
|
|
94
|
+
]
|
|
95
|
+
};
|
|
96
|
+
return JobActionReceipt;
|
|
97
|
+
};
|
|
98
|
+
job.cancelWorkflow = async () => {
|
|
99
|
+
ack();
|
|
100
|
+
job._completionAction = {
|
|
101
|
+
method: "cancelProcessInstance",
|
|
102
|
+
args: [{ processInstanceKey: jobData.processInstanceKey }]
|
|
103
|
+
};
|
|
104
|
+
return JobActionReceipt;
|
|
105
|
+
};
|
|
106
|
+
job.ignore = async () => {
|
|
107
|
+
ack();
|
|
108
|
+
return JobActionReceipt;
|
|
109
|
+
};
|
|
110
|
+
job.modifyJobTimeout = ({ newTimeoutMs }) => client.updateJob({
|
|
111
|
+
changeset: { timeout: newTimeoutMs },
|
|
112
|
+
jobKey: jobData.jobKey,
|
|
113
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
114
|
+
});
|
|
115
|
+
job.modifyRetries = ({ retries }) => client.updateJob({
|
|
116
|
+
changeset: { retries },
|
|
117
|
+
jobKey: jobData.jobKey,
|
|
118
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
119
|
+
});
|
|
120
|
+
return job;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/runtime/threadWorkerEntry.ts
|
|
48
124
|
if (!import_node_worker_threads.parentPort) {
|
|
49
125
|
throw new Error("threadWorkerEntry must run inside a worker_threads Worker");
|
|
50
126
|
}
|
|
@@ -101,52 +177,5 @@ import_node_worker_threads.parentPort.on("message", async (msg) => {
|
|
|
101
177
|
}
|
|
102
178
|
}
|
|
103
179
|
});
|
|
104
|
-
function createJobProxy(jobData, client) {
|
|
105
|
-
const acknowledged = { value: false };
|
|
106
|
-
const ack = () => {
|
|
107
|
-
acknowledged.value = true;
|
|
108
|
-
job.acknowledged = true;
|
|
109
|
-
};
|
|
110
|
-
const job = { ...jobData };
|
|
111
|
-
job.complete = async (variables = {}, result) => {
|
|
112
|
-
ack();
|
|
113
|
-
job._completionAction = {
|
|
114
|
-
method: "completeJob",
|
|
115
|
-
args: [{ variables, jobKey: jobData.jobKey, ...result !== void 0 && { result } }]
|
|
116
|
-
};
|
|
117
|
-
return JobActionReceipt;
|
|
118
|
-
};
|
|
119
|
-
job.fail = async (reason) => {
|
|
120
|
-
ack();
|
|
121
|
-
job._completionAction = {
|
|
122
|
-
method: "failJob",
|
|
123
|
-
args: [{ ...reason, jobKey: jobData.jobKey }]
|
|
124
|
-
};
|
|
125
|
-
return JobActionReceipt;
|
|
126
|
-
};
|
|
127
|
-
job.error = async (error) => {
|
|
128
|
-
ack();
|
|
129
|
-
job._completionAction = {
|
|
130
|
-
method: "throwJobError",
|
|
131
|
-
args: [{ ...error, jobKey: jobData.jobKey }]
|
|
132
|
-
};
|
|
133
|
-
return JobActionReceipt;
|
|
134
|
-
};
|
|
135
|
-
job.cancelWorkflow = async () => {
|
|
136
|
-
ack();
|
|
137
|
-
job._completionAction = {
|
|
138
|
-
method: "cancelProcessInstance",
|
|
139
|
-
args: [{ processInstanceKey: jobData.processInstanceKey }]
|
|
140
|
-
};
|
|
141
|
-
return JobActionReceipt;
|
|
142
|
-
};
|
|
143
|
-
job.ignore = async () => {
|
|
144
|
-
ack();
|
|
145
|
-
return JobActionReceipt;
|
|
146
|
-
};
|
|
147
|
-
job.modifyJobTimeout = ({ newTimeoutMs }) => client.updateJob({ changeset: { timeout: newTimeoutMs }, jobKey: jobData.jobKey });
|
|
148
|
-
job.modifyRetries = ({ retries }) => client.updateJob({ changeset: { retries }, jobKey: jobData.jobKey });
|
|
149
|
-
return job;
|
|
150
|
-
}
|
|
151
180
|
import_node_worker_threads.parentPort.postMessage({ type: "ready" });
|
|
152
181
|
//# sourceMappingURL=threadWorkerEntry.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/threadWorkerEntry.ts","../src/runtime/clientProxy.ts","../src/runtime/moduleSpecifier.ts"],"sourcesContent":["/**\n * Worker thread entry point for the shared ThreadPool.\n *\n * Each thread in the pool runs this script. It:\n * 1. Waits for job messages on parentPort\n * 2. Dynamically imports and caches the handler module per handlerModule path\n * 3. Creates a client proxy + job proxy per job\n * 4. Calls the handler, sends back the result\n *\n * Threads are generic — the handler module is specified per-job, not per-thread.\n * Handlers are cached by module path so each import happens only once per thread.\n */\nimport { parentPort } from 'node:worker_threads';\nimport type { JobResult as ApiJobResult } from '../gen/types.gen';\nimport { createClientProxy } from './clientProxy.ts';\nimport { toModuleSpecifier } from './moduleSpecifier.ts';\n\n// Inline the JobActionReceipt constant to avoid importing the full SDK dependency chain\n// (jobWorker.ts → ../gen/CamundaClient → entire SDK) in the worker thread.\nconst JobActionReceipt = 'JOB_ACTION_RECEIPT' as const;\n\nif (!parentPort) {\n throw new Error('threadWorkerEntry must run inside a worker_threads Worker');\n}\n\ninterface JobMessage {\n type: 'job';\n /** Unique ID for this job dispatch (correlates response) */\n taskId: string;\n /** Serialized job data (plain object, no methods) */\n jobData: Record<string, unknown>;\n /** Handler module path — resolved per job, cached per thread */\n handlerModule: string;\n}\n\ninterface JobResult {\n type: 'job-result';\n taskId: string;\n ok: boolean;\n error?: string;\n /** When the handler acknowledged the job via complete/fail/error,\n * the action is captured here for the main thread to execute. */\n completionAction?: {\n method: string;\n args: unknown[];\n };\n}\n\n/** Cache of loaded handlers keyed by module path. Each thread loads a handler once per unique path. */\nconst handlerCache = new Map<string, (job: any, client: any) => Promise<any>>();\n\nasync function loadHandler(\n handlerModule: string\n): Promise<(job: any, client: any) => Promise<any>> {\n const cached = handlerCache.get(handlerModule);\n if (cached) return cached;\n\n const mod = await import(toModuleSpecifier(handlerModule));\n const handler = mod.default ?? mod.handler;\n if (typeof handler !== 'function') {\n throw new Error(\n `Threaded handler module '${handlerModule}' must export a default function or named 'handler' export`\n );\n }\n handlerCache.set(handlerModule, handler);\n return handler;\n}\n\nparentPort.on('message', async (msg: JobMessage & { clientPort?: any }) => {\n if (msg.type !== 'job') return;\n\n const { taskId, jobData, handlerModule, clientPort } = msg as JobMessage & {\n clientPort: import('node:worker_threads').MessagePort;\n };\n\n try {\n const handlerFn = await loadHandler(handlerModule);\n\n // Create a client proxy for this job's MessagePort\n const clientProxy = createClientProxy(clientPort);\n\n // Build a job-like object with proxied action methods\n const job = createJobProxy(jobData, clientProxy);\n\n await handlerFn(job, clientProxy);\n\n // If the handler never acknowledged the job (no complete/fail/error/ignore),\n // treat it as an error so the broker can retry rather than silently dropping it.\n if (!job.acknowledged) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error:\n `Handler for '${handlerModule}' returned without acknowledging the job. ` +\n 'Call job.complete(), job.fail(), job.error(), or job.ignore().',\n };\n parentPort!.postMessage(result);\n return;\n }\n\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: true,\n completionAction: job._completionAction || undefined,\n };\n parentPort!.postMessage(result);\n } catch (err: any) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error: err?.message || String(err),\n };\n parentPort!.postMessage(result);\n } finally {\n // Close this job's client port\n if (clientPort && typeof clientPort.close === 'function') {\n clientPort.close();\n }\n }\n});\n\n/**\n * Build a job object with action methods that proxy through the client.\n * The job data is plain serialized data from the main thread.\n * Action methods (complete, fail, error, etc.) call back to the main thread client.\n */\nfunction createJobProxy(jobData: Record<string, unknown>, client: any): any {\n const acknowledged = { value: false };\n const ack = () => {\n acknowledged.value = true;\n job.acknowledged = true;\n };\n\n const job: any = { ...jobData };\n\n /**\n * Completion actions (complete/fail/error/cancelWorkflow) are stored as intent\n * rather than proxied through the MessagePort. The thread returns immediately,\n * and the main thread executes the API call asynchronously. This keeps threads\n * free for CPU work instead of blocking on I/O round-trips.\n */\n\n job.complete = async (variables: Record<string, unknown> = {}, result?: ApiJobResult) => {\n ack();\n job._completionAction = {\n method: 'completeJob',\n args: [{ variables, jobKey: jobData.jobKey, ...(result !== undefined && { result }) }],\n };\n return JobActionReceipt;\n };\n\n job.fail = async (reason: any) => {\n ack();\n job._completionAction = {\n method: 'failJob',\n args: [{ ...reason, jobKey: jobData.jobKey }],\n };\n return JobActionReceipt;\n };\n\n job.error = async (error: any) => {\n ack();\n job._completionAction = {\n method: 'throwJobError',\n args: [{ ...error, jobKey: jobData.jobKey }],\n };\n return JobActionReceipt;\n };\n\n job.cancelWorkflow = async () => {\n ack();\n job._completionAction = {\n method: 'cancelProcessInstance',\n args: [{ processInstanceKey: jobData.processInstanceKey }],\n };\n return JobActionReceipt;\n };\n\n job.ignore = async () => {\n ack();\n return JobActionReceipt;\n };\n\n // Non-completion actions still proxy through the client (rare, need response)\n job.modifyJobTimeout = ({ newTimeoutMs }: { newTimeoutMs: number }) =>\n client.updateJob({ changeset: { timeout: newTimeoutMs }, jobKey: jobData.jobKey });\n\n job.modifyRetries = ({ retries }: { retries: number }) =>\n client.updateJob({ changeset: { retries }, jobKey: jobData.jobKey });\n\n return job;\n}\n\n// Signal ready\nparentPort.postMessage({ type: 'ready' });\n","/**\n * Client proxy for worker_threads: allows threaded job handlers to call\n * CamundaClient methods transparently via MessagePort.\n *\n * Main thread side: installClientCallHandler(port, client)\n * Worker thread side: createClientProxy(port) → CamundaClient-shaped proxy\n */\n\nimport type { MessagePort } from 'node:worker_threads';\nimport type { CamundaClient } from '../gen/CamundaClient';\n\n// Wire protocol\nexport interface ClientCallMessage {\n type: 'client-call';\n callId: string;\n method: string;\n args: unknown[];\n}\n\nexport interface ClientCallResult {\n type: 'client-call-result';\n callId: string;\n result?: unknown;\n error?: string;\n}\n\n/** Fallback UUID generator for runtimes where globalThis.crypto.randomUUID is unavailable. */\nlet _counter = 0;\nfunction fallbackUUID(): string {\n // biome-ignore lint/plugin: correlation-id entropy, not cadence\n return `${Date.now().toString(36)}-${(++_counter).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a Proxy that looks like CamundaClient but forwards every method call\n * over a MessagePort to the main thread. Runs inside a worker thread.\n */\nexport function createClientProxy(port: MessagePort): CamundaClient {\n return new Proxy({} as CamundaClient, {\n get(_, method: string) {\n // Ignore symbol properties and common non-method accesses\n if (typeof method === 'symbol') return undefined;\n if (method === 'then') return undefined; // Prevent Proxy from being treated as thenable\n if (method === 'toJSON') return undefined;\n\n return (...args: unknown[]) => {\n const callId = globalThis.crypto?.randomUUID\n ? globalThis.crypto.randomUUID()\n : fallbackUUID();\n const msg: ClientCallMessage = { type: 'client-call', callId, method, args };\n port.postMessage(msg);\n return new Promise<unknown>((resolve, reject) => {\n const handler = (reply: ClientCallResult) => {\n if (reply.type !== 'client-call-result' || reply.callId !== callId) return;\n port.off('message', handler);\n if (reply.error !== undefined) {\n reject(new Error(reply.error));\n } else {\n resolve(reply.result);\n }\n };\n port.on('message', handler);\n });\n };\n },\n });\n}\n\n/**\n * Install a message handler on a MessagePort (main thread side) that\n * receives client-call messages from a worker thread and executes them\n * on the real CamundaClient instance.\n */\nexport function installClientCallHandler(port: MessagePort, client: CamundaClient): void {\n port.on('message', async (msg: ClientCallMessage) => {\n if (msg.type !== 'client-call') return;\n try {\n const fn = (client as any)[msg.method];\n if (typeof fn !== 'function') {\n throw new Error(`CamundaClient has no method '${msg.method}'`);\n }\n const result = await fn.apply(client, msg.args);\n const reply: ClientCallResult = { type: 'client-call-result', callId: msg.callId, result };\n port.postMessage(reply);\n } catch (err: any) {\n const reply: ClientCallResult = {\n type: 'client-call-result',\n callId: msg.callId,\n error: err?.message || String(err),\n };\n port.postMessage(reply);\n }\n });\n}\n","import { resolve as resolvePath } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\n/**\n * Turn a handler module reference into a specifier `import()` accepts.\n *\n * Bare specifiers (`@scope/pkg`, `my-handlers`) are passed through so normal package\n * resolution applies. Anything path-shaped is resolved against the working directory and\n * converted to a `file:` URL.\n *\n * The conversion must go through `pathToFileURL`. Hand-building the URL breaks on Windows\n * in three separate ways: `file://C:/...` puts the drive letter in the *host* rather than\n * the path, backslashes are not URL-legal and need escaping, and a bare `C:\\...` handed to\n * `new URL()` parses `C:` as the scheme — which is what produces \"Only URLs with a scheme\n * in: file, data, and node are supported by the default ESM loader\".\n */\nexport function toModuleSpecifier(handlerModule: string): string {\n const isPath =\n handlerModule.startsWith('.') ||\n handlerModule.startsWith('/') ||\n /^[a-zA-Z]:[\\\\/]/.test(handlerModule);\n\n return isPath ? pathToFileURL(resolvePath(handlerModule)).href : handlerModule;\n}\n"],"mappings":"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","names":["resolvePath","result"]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime/threadWorkerEntry.ts","../src/runtime/clientProxy.ts","../src/runtime/moduleSpecifier.ts","../src/runtime/threadJobProxy.ts"],"sourcesContent":["/**\n * Worker thread entry point for the shared ThreadPool.\n *\n * Each thread in the pool runs this script. It:\n * 1. Waits for job messages on parentPort\n * 2. Dynamically imports and caches the handler module per handlerModule path\n * 3. Creates a client proxy + job proxy per job\n * 4. Calls the handler, sends back the result\n *\n * Threads are generic — the handler module is specified per-job, not per-thread.\n * Handlers are cached by module path so each import happens only once per thread.\n */\nimport { parentPort } from 'node:worker_threads';\nimport { createClientProxy } from './clientProxy.ts';\nimport { toModuleSpecifier } from './moduleSpecifier.ts';\nimport { createJobProxy } from './threadJobProxy.ts';\n\nif (!parentPort) {\n throw new Error('threadWorkerEntry must run inside a worker_threads Worker');\n}\n\ninterface JobMessage {\n type: 'job';\n /** Unique ID for this job dispatch (correlates response) */\n taskId: string;\n /** Serialized job data (plain object, no methods) */\n jobData: Record<string, unknown>;\n /** Handler module path — resolved per job, cached per thread */\n handlerModule: string;\n}\n\ninterface JobResult {\n type: 'job-result';\n taskId: string;\n ok: boolean;\n error?: string;\n /** When the handler acknowledged the job via complete/fail/error,\n * the action is captured here for the main thread to execute. */\n completionAction?: {\n method: string;\n args: unknown[];\n };\n}\n\n/** Cache of loaded handlers keyed by module path. Each thread loads a handler once per unique path. */\nconst handlerCache = new Map<string, (job: any, client: any) => Promise<any>>();\n\nasync function loadHandler(\n handlerModule: string\n): Promise<(job: any, client: any) => Promise<any>> {\n const cached = handlerCache.get(handlerModule);\n if (cached) return cached;\n\n const mod = await import(toModuleSpecifier(handlerModule));\n const handler = mod.default ?? mod.handler;\n if (typeof handler !== 'function') {\n throw new Error(\n `Threaded handler module '${handlerModule}' must export a default function or named 'handler' export`\n );\n }\n handlerCache.set(handlerModule, handler);\n return handler;\n}\n\nparentPort.on('message', async (msg: JobMessage & { clientPort?: any }) => {\n if (msg.type !== 'job') return;\n\n const { taskId, jobData, handlerModule, clientPort } = msg as JobMessage & {\n clientPort: import('node:worker_threads').MessagePort;\n };\n\n try {\n const handlerFn = await loadHandler(handlerModule);\n\n // Create a client proxy for this job's MessagePort\n const clientProxy = createClientProxy(clientPort);\n\n // Build a job-like object with proxied action methods\n const job = createJobProxy(jobData, clientProxy);\n\n await handlerFn(job, clientProxy);\n\n // If the handler never acknowledged the job (no complete/fail/error/ignore),\n // treat it as an error so the broker can retry rather than silently dropping it.\n if (!job.acknowledged) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error:\n `Handler for '${handlerModule}' returned without acknowledging the job. ` +\n 'Call job.complete(), job.fail(), job.error(), or job.ignore().',\n };\n parentPort!.postMessage(result);\n return;\n }\n\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: true,\n completionAction: job._completionAction || undefined,\n };\n parentPort!.postMessage(result);\n } catch (err: any) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error: err?.message || String(err),\n };\n parentPort!.postMessage(result);\n } finally {\n // Close this job's client port\n if (clientPort && typeof clientPort.close === 'function') {\n clientPort.close();\n }\n }\n});\n\n// Signal ready\nparentPort.postMessage({ type: 'ready' });\n","/**\n * Client proxy for worker_threads: allows threaded job handlers to call\n * CamundaClient methods transparently via MessagePort.\n *\n * Main thread side: installClientCallHandler(port, client)\n * Worker thread side: createClientProxy(port) → CamundaClient-shaped proxy\n */\n\nimport type { MessagePort } from 'node:worker_threads';\nimport type { CamundaClient } from '../gen/CamundaClient';\n\n// Wire protocol\nexport interface ClientCallMessage {\n type: 'client-call';\n callId: string;\n method: string;\n args: unknown[];\n}\n\nexport interface ClientCallResult {\n type: 'client-call-result';\n callId: string;\n result?: unknown;\n error?: string;\n}\n\n/** Fallback UUID generator for runtimes where globalThis.crypto.randomUUID is unavailable. */\nlet _counter = 0;\nfunction fallbackUUID(): string {\n // biome-ignore lint/plugin: correlation-id entropy, not cadence\n return `${Date.now().toString(36)}-${(++_counter).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a Proxy that looks like CamundaClient but forwards every method call\n * over a MessagePort to the main thread. Runs inside a worker thread.\n */\nexport function createClientProxy(port: MessagePort): CamundaClient {\n return new Proxy({} as CamundaClient, {\n get(_, method: string) {\n // Ignore symbol properties and common non-method accesses\n if (typeof method === 'symbol') return undefined;\n if (method === 'then') return undefined; // Prevent Proxy from being treated as thenable\n if (method === 'toJSON') return undefined;\n\n return (...args: unknown[]) => {\n const callId = globalThis.crypto?.randomUUID\n ? globalThis.crypto.randomUUID()\n : fallbackUUID();\n const msg: ClientCallMessage = { type: 'client-call', callId, method, args };\n port.postMessage(msg);\n return new Promise<unknown>((resolve, reject) => {\n const handler = (reply: ClientCallResult) => {\n if (reply.type !== 'client-call-result' || reply.callId !== callId) return;\n port.off('message', handler);\n if (reply.error !== undefined) {\n reject(new Error(reply.error));\n } else {\n resolve(reply.result);\n }\n };\n port.on('message', handler);\n });\n };\n },\n });\n}\n\n/**\n * Install a message handler on a MessagePort (main thread side) that\n * receives client-call messages from a worker thread and executes them\n * on the real CamundaClient instance.\n */\nexport function installClientCallHandler(port: MessagePort, client: CamundaClient): void {\n port.on('message', async (msg: ClientCallMessage) => {\n if (msg.type !== 'client-call') return;\n try {\n const fn = (client as any)[msg.method];\n if (typeof fn !== 'function') {\n throw new Error(`CamundaClient has no method '${msg.method}'`);\n }\n const result = await fn.apply(client, msg.args);\n const reply: ClientCallResult = { type: 'client-call-result', callId: msg.callId, result };\n port.postMessage(reply);\n } catch (err: any) {\n const reply: ClientCallResult = {\n type: 'client-call-result',\n callId: msg.callId,\n error: err?.message || String(err),\n };\n port.postMessage(reply);\n }\n });\n}\n","import { resolve as resolvePath } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\n/**\n * Turn a handler module reference into a specifier `import()` accepts.\n *\n * Bare specifiers (`@scope/pkg`, `my-handlers`) are passed through so normal package\n * resolution applies. Anything path-shaped is resolved against the working directory and\n * converted to a `file:` URL.\n *\n * The conversion must go through `pathToFileURL`. Hand-building the URL breaks on Windows\n * in three separate ways: `file://C:/...` puts the drive letter in the *host* rather than\n * the path, backslashes are not URL-legal and need escaping, and a bare `C:\\...` handed to\n * `new URL()` parses `C:` as the scheme — which is what produces \"Only URLs with a scheme\n * in: file, data, and node are supported by the default ESM loader\".\n */\nexport function toModuleSpecifier(handlerModule: string): string {\n const isPath =\n handlerModule.startsWith('.') ||\n handlerModule.startsWith('/') ||\n /^[a-zA-Z]:[\\\\/]/.test(handlerModule);\n\n return isPath ? pathToFileURL(resolvePath(handlerModule)).href : handlerModule;\n}\n","/**\n * Build a job object with action methods that proxy through the client, used by\n * the threaded worker entry point (`threadWorkerEntry.ts`).\n *\n * Kept in its own module — separate from the worker-thread bootstrap, which\n * throws on import outside a `worker_threads` Worker — so the proxy's behaviour\n * (in particular that EVERY job action forwards the activation `leaseToken` back\n * to the main-thread client) is unit-testable directly, without spinning up a\n * real worker thread.\n *\n * The job data is plain serialized data from the main thread. Completion actions\n * (complete/fail/error/cancelWorkflow) are captured as intent on\n * `job._completionAction` and executed by the main thread; the non-completion\n * actions (modifyJobTimeout/modifyRetries) proxy straight through the client.\n */\nimport type { JobResult as ApiJobResult } from '../gen/types.gen';\n\n// Inline the JobActionReceipt constant to avoid importing the full SDK dependency\n// chain (jobWorker.ts → ../gen/CamundaClient → entire SDK) into the worker thread.\nconst JobActionReceipt = 'JOB_ACTION_RECEIPT' as const;\n\nexport function createJobProxy(jobData: Record<string, unknown>, client: any): any {\n const acknowledged = { value: false };\n const ack = () => {\n acknowledged.value = true;\n job.acknowledged = true;\n };\n\n const job: any = { ...jobData };\n\n /**\n * Completion actions (complete/fail/error/cancelWorkflow) are stored as intent\n * rather than proxied through the MessagePort. The thread returns immediately,\n * and the main thread executes the API call asynchronously. This keeps threads\n * free for CPU work instead of blocking on I/O round-trips.\n */\n\n job.complete = async (variables: Record<string, unknown> = {}, result?: ApiJobResult) => {\n ack();\n job._completionAction = {\n method: 'completeJob',\n args: [\n {\n variables,\n jobKey: jobData.jobKey,\n ...(result !== undefined && { result }),\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n },\n ],\n };\n return JobActionReceipt;\n };\n\n job.fail = async (reason: any) => {\n ack();\n job._completionAction = {\n method: 'failJob',\n args: [\n {\n ...reason,\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n },\n ],\n };\n return JobActionReceipt;\n };\n\n job.error = async (error: any) => {\n ack();\n job._completionAction = {\n method: 'throwJobError',\n args: [\n {\n ...error,\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n },\n ],\n };\n return JobActionReceipt;\n };\n\n job.cancelWorkflow = async () => {\n ack();\n job._completionAction = {\n method: 'cancelProcessInstance',\n args: [{ processInstanceKey: jobData.processInstanceKey }],\n };\n return JobActionReceipt;\n };\n\n job.ignore = async () => {\n ack();\n return JobActionReceipt;\n };\n\n // Non-completion actions still proxy through the client (rare, need response)\n job.modifyJobTimeout = ({ newTimeoutMs }: { newTimeoutMs: number }) =>\n client.updateJob({\n changeset: { timeout: newTimeoutMs },\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n });\n\n job.modifyRetries = ({ retries }: { retries: number }) =>\n client.updateJob({\n changeset: { retries },\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n });\n\n return job;\n}\n"],"mappings":"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","names":["resolvePath","result"]}
|
|
@@ -41,8 +41,84 @@ function toModuleSpecifier(handlerModule) {
|
|
|
41
41
|
return isPath ? pathToFileURL(resolvePath(handlerModule)).href : handlerModule;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
// src/runtime/
|
|
44
|
+
// src/runtime/threadJobProxy.ts
|
|
45
45
|
var JobActionReceipt = "JOB_ACTION_RECEIPT";
|
|
46
|
+
function createJobProxy(jobData, client) {
|
|
47
|
+
const acknowledged = { value: false };
|
|
48
|
+
const ack = () => {
|
|
49
|
+
acknowledged.value = true;
|
|
50
|
+
job.acknowledged = true;
|
|
51
|
+
};
|
|
52
|
+
const job = { ...jobData };
|
|
53
|
+
job.complete = async (variables = {}, result) => {
|
|
54
|
+
ack();
|
|
55
|
+
job._completionAction = {
|
|
56
|
+
method: "completeJob",
|
|
57
|
+
args: [
|
|
58
|
+
{
|
|
59
|
+
variables,
|
|
60
|
+
jobKey: jobData.jobKey,
|
|
61
|
+
...result !== void 0 && { result },
|
|
62
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
return JobActionReceipt;
|
|
67
|
+
};
|
|
68
|
+
job.fail = async (reason) => {
|
|
69
|
+
ack();
|
|
70
|
+
job._completionAction = {
|
|
71
|
+
method: "failJob",
|
|
72
|
+
args: [
|
|
73
|
+
{
|
|
74
|
+
...reason,
|
|
75
|
+
jobKey: jobData.jobKey,
|
|
76
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
};
|
|
80
|
+
return JobActionReceipt;
|
|
81
|
+
};
|
|
82
|
+
job.error = async (error) => {
|
|
83
|
+
ack();
|
|
84
|
+
job._completionAction = {
|
|
85
|
+
method: "throwJobError",
|
|
86
|
+
args: [
|
|
87
|
+
{
|
|
88
|
+
...error,
|
|
89
|
+
jobKey: jobData.jobKey,
|
|
90
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
};
|
|
94
|
+
return JobActionReceipt;
|
|
95
|
+
};
|
|
96
|
+
job.cancelWorkflow = async () => {
|
|
97
|
+
ack();
|
|
98
|
+
job._completionAction = {
|
|
99
|
+
method: "cancelProcessInstance",
|
|
100
|
+
args: [{ processInstanceKey: jobData.processInstanceKey }]
|
|
101
|
+
};
|
|
102
|
+
return JobActionReceipt;
|
|
103
|
+
};
|
|
104
|
+
job.ignore = async () => {
|
|
105
|
+
ack();
|
|
106
|
+
return JobActionReceipt;
|
|
107
|
+
};
|
|
108
|
+
job.modifyJobTimeout = ({ newTimeoutMs }) => client.updateJob({
|
|
109
|
+
changeset: { timeout: newTimeoutMs },
|
|
110
|
+
jobKey: jobData.jobKey,
|
|
111
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
112
|
+
});
|
|
113
|
+
job.modifyRetries = ({ retries }) => client.updateJob({
|
|
114
|
+
changeset: { retries },
|
|
115
|
+
jobKey: jobData.jobKey,
|
|
116
|
+
...jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}
|
|
117
|
+
});
|
|
118
|
+
return job;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/runtime/threadWorkerEntry.ts
|
|
46
122
|
if (!parentPort) {
|
|
47
123
|
throw new Error("threadWorkerEntry must run inside a worker_threads Worker");
|
|
48
124
|
}
|
|
@@ -99,52 +175,5 @@ parentPort.on("message", async (msg) => {
|
|
|
99
175
|
}
|
|
100
176
|
}
|
|
101
177
|
});
|
|
102
|
-
function createJobProxy(jobData, client) {
|
|
103
|
-
const acknowledged = { value: false };
|
|
104
|
-
const ack = () => {
|
|
105
|
-
acknowledged.value = true;
|
|
106
|
-
job.acknowledged = true;
|
|
107
|
-
};
|
|
108
|
-
const job = { ...jobData };
|
|
109
|
-
job.complete = async (variables = {}, result) => {
|
|
110
|
-
ack();
|
|
111
|
-
job._completionAction = {
|
|
112
|
-
method: "completeJob",
|
|
113
|
-
args: [{ variables, jobKey: jobData.jobKey, ...result !== void 0 && { result } }]
|
|
114
|
-
};
|
|
115
|
-
return JobActionReceipt;
|
|
116
|
-
};
|
|
117
|
-
job.fail = async (reason) => {
|
|
118
|
-
ack();
|
|
119
|
-
job._completionAction = {
|
|
120
|
-
method: "failJob",
|
|
121
|
-
args: [{ ...reason, jobKey: jobData.jobKey }]
|
|
122
|
-
};
|
|
123
|
-
return JobActionReceipt;
|
|
124
|
-
};
|
|
125
|
-
job.error = async (error) => {
|
|
126
|
-
ack();
|
|
127
|
-
job._completionAction = {
|
|
128
|
-
method: "throwJobError",
|
|
129
|
-
args: [{ ...error, jobKey: jobData.jobKey }]
|
|
130
|
-
};
|
|
131
|
-
return JobActionReceipt;
|
|
132
|
-
};
|
|
133
|
-
job.cancelWorkflow = async () => {
|
|
134
|
-
ack();
|
|
135
|
-
job._completionAction = {
|
|
136
|
-
method: "cancelProcessInstance",
|
|
137
|
-
args: [{ processInstanceKey: jobData.processInstanceKey }]
|
|
138
|
-
};
|
|
139
|
-
return JobActionReceipt;
|
|
140
|
-
};
|
|
141
|
-
job.ignore = async () => {
|
|
142
|
-
ack();
|
|
143
|
-
return JobActionReceipt;
|
|
144
|
-
};
|
|
145
|
-
job.modifyJobTimeout = ({ newTimeoutMs }) => client.updateJob({ changeset: { timeout: newTimeoutMs }, jobKey: jobData.jobKey });
|
|
146
|
-
job.modifyRetries = ({ retries }) => client.updateJob({ changeset: { retries }, jobKey: jobData.jobKey });
|
|
147
|
-
return job;
|
|
148
|
-
}
|
|
149
178
|
parentPort.postMessage({ type: "ready" });
|
|
150
179
|
//# sourceMappingURL=threadWorkerEntry.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/runtime/threadWorkerEntry.ts","../src/runtime/clientProxy.ts","../src/runtime/moduleSpecifier.ts"],"sourcesContent":["/**\n * Worker thread entry point for the shared ThreadPool.\n *\n * Each thread in the pool runs this script. It:\n * 1. Waits for job messages on parentPort\n * 2. Dynamically imports and caches the handler module per handlerModule path\n * 3. Creates a client proxy + job proxy per job\n * 4. Calls the handler, sends back the result\n *\n * Threads are generic — the handler module is specified per-job, not per-thread.\n * Handlers are cached by module path so each import happens only once per thread.\n */\nimport { parentPort } from 'node:worker_threads';\nimport type { JobResult as ApiJobResult } from '../gen/types.gen';\nimport { createClientProxy } from './clientProxy.ts';\nimport { toModuleSpecifier } from './moduleSpecifier.ts';\n\n// Inline the JobActionReceipt constant to avoid importing the full SDK dependency chain\n// (jobWorker.ts → ../gen/CamundaClient → entire SDK) in the worker thread.\nconst JobActionReceipt = 'JOB_ACTION_RECEIPT' as const;\n\nif (!parentPort) {\n throw new Error('threadWorkerEntry must run inside a worker_threads Worker');\n}\n\ninterface JobMessage {\n type: 'job';\n /** Unique ID for this job dispatch (correlates response) */\n taskId: string;\n /** Serialized job data (plain object, no methods) */\n jobData: Record<string, unknown>;\n /** Handler module path — resolved per job, cached per thread */\n handlerModule: string;\n}\n\ninterface JobResult {\n type: 'job-result';\n taskId: string;\n ok: boolean;\n error?: string;\n /** When the handler acknowledged the job via complete/fail/error,\n * the action is captured here for the main thread to execute. */\n completionAction?: {\n method: string;\n args: unknown[];\n };\n}\n\n/** Cache of loaded handlers keyed by module path. Each thread loads a handler once per unique path. */\nconst handlerCache = new Map<string, (job: any, client: any) => Promise<any>>();\n\nasync function loadHandler(\n handlerModule: string\n): Promise<(job: any, client: any) => Promise<any>> {\n const cached = handlerCache.get(handlerModule);\n if (cached) return cached;\n\n const mod = await import(toModuleSpecifier(handlerModule));\n const handler = mod.default ?? mod.handler;\n if (typeof handler !== 'function') {\n throw new Error(\n `Threaded handler module '${handlerModule}' must export a default function or named 'handler' export`\n );\n }\n handlerCache.set(handlerModule, handler);\n return handler;\n}\n\nparentPort.on('message', async (msg: JobMessage & { clientPort?: any }) => {\n if (msg.type !== 'job') return;\n\n const { taskId, jobData, handlerModule, clientPort } = msg as JobMessage & {\n clientPort: import('node:worker_threads').MessagePort;\n };\n\n try {\n const handlerFn = await loadHandler(handlerModule);\n\n // Create a client proxy for this job's MessagePort\n const clientProxy = createClientProxy(clientPort);\n\n // Build a job-like object with proxied action methods\n const job = createJobProxy(jobData, clientProxy);\n\n await handlerFn(job, clientProxy);\n\n // If the handler never acknowledged the job (no complete/fail/error/ignore),\n // treat it as an error so the broker can retry rather than silently dropping it.\n if (!job.acknowledged) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error:\n `Handler for '${handlerModule}' returned without acknowledging the job. ` +\n 'Call job.complete(), job.fail(), job.error(), or job.ignore().',\n };\n parentPort!.postMessage(result);\n return;\n }\n\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: true,\n completionAction: job._completionAction || undefined,\n };\n parentPort!.postMessage(result);\n } catch (err: any) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error: err?.message || String(err),\n };\n parentPort!.postMessage(result);\n } finally {\n // Close this job's client port\n if (clientPort && typeof clientPort.close === 'function') {\n clientPort.close();\n }\n }\n});\n\n/**\n * Build a job object with action methods that proxy through the client.\n * The job data is plain serialized data from the main thread.\n * Action methods (complete, fail, error, etc.) call back to the main thread client.\n */\nfunction createJobProxy(jobData: Record<string, unknown>, client: any): any {\n const acknowledged = { value: false };\n const ack = () => {\n acknowledged.value = true;\n job.acknowledged = true;\n };\n\n const job: any = { ...jobData };\n\n /**\n * Completion actions (complete/fail/error/cancelWorkflow) are stored as intent\n * rather than proxied through the MessagePort. The thread returns immediately,\n * and the main thread executes the API call asynchronously. This keeps threads\n * free for CPU work instead of blocking on I/O round-trips.\n */\n\n job.complete = async (variables: Record<string, unknown> = {}, result?: ApiJobResult) => {\n ack();\n job._completionAction = {\n method: 'completeJob',\n args: [{ variables, jobKey: jobData.jobKey, ...(result !== undefined && { result }) }],\n };\n return JobActionReceipt;\n };\n\n job.fail = async (reason: any) => {\n ack();\n job._completionAction = {\n method: 'failJob',\n args: [{ ...reason, jobKey: jobData.jobKey }],\n };\n return JobActionReceipt;\n };\n\n job.error = async (error: any) => {\n ack();\n job._completionAction = {\n method: 'throwJobError',\n args: [{ ...error, jobKey: jobData.jobKey }],\n };\n return JobActionReceipt;\n };\n\n job.cancelWorkflow = async () => {\n ack();\n job._completionAction = {\n method: 'cancelProcessInstance',\n args: [{ processInstanceKey: jobData.processInstanceKey }],\n };\n return JobActionReceipt;\n };\n\n job.ignore = async () => {\n ack();\n return JobActionReceipt;\n };\n\n // Non-completion actions still proxy through the client (rare, need response)\n job.modifyJobTimeout = ({ newTimeoutMs }: { newTimeoutMs: number }) =>\n client.updateJob({ changeset: { timeout: newTimeoutMs }, jobKey: jobData.jobKey });\n\n job.modifyRetries = ({ retries }: { retries: number }) =>\n client.updateJob({ changeset: { retries }, jobKey: jobData.jobKey });\n\n return job;\n}\n\n// Signal ready\nparentPort.postMessage({ type: 'ready' });\n","/**\n * Client proxy for worker_threads: allows threaded job handlers to call\n * CamundaClient methods transparently via MessagePort.\n *\n * Main thread side: installClientCallHandler(port, client)\n * Worker thread side: createClientProxy(port) → CamundaClient-shaped proxy\n */\n\nimport type { MessagePort } from 'node:worker_threads';\nimport type { CamundaClient } from '../gen/CamundaClient';\n\n// Wire protocol\nexport interface ClientCallMessage {\n type: 'client-call';\n callId: string;\n method: string;\n args: unknown[];\n}\n\nexport interface ClientCallResult {\n type: 'client-call-result';\n callId: string;\n result?: unknown;\n error?: string;\n}\n\n/** Fallback UUID generator for runtimes where globalThis.crypto.randomUUID is unavailable. */\nlet _counter = 0;\nfunction fallbackUUID(): string {\n // biome-ignore lint/plugin: correlation-id entropy, not cadence\n return `${Date.now().toString(36)}-${(++_counter).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a Proxy that looks like CamundaClient but forwards every method call\n * over a MessagePort to the main thread. Runs inside a worker thread.\n */\nexport function createClientProxy(port: MessagePort): CamundaClient {\n return new Proxy({} as CamundaClient, {\n get(_, method: string) {\n // Ignore symbol properties and common non-method accesses\n if (typeof method === 'symbol') return undefined;\n if (method === 'then') return undefined; // Prevent Proxy from being treated as thenable\n if (method === 'toJSON') return undefined;\n\n return (...args: unknown[]) => {\n const callId = globalThis.crypto?.randomUUID\n ? globalThis.crypto.randomUUID()\n : fallbackUUID();\n const msg: ClientCallMessage = { type: 'client-call', callId, method, args };\n port.postMessage(msg);\n return new Promise<unknown>((resolve, reject) => {\n const handler = (reply: ClientCallResult) => {\n if (reply.type !== 'client-call-result' || reply.callId !== callId) return;\n port.off('message', handler);\n if (reply.error !== undefined) {\n reject(new Error(reply.error));\n } else {\n resolve(reply.result);\n }\n };\n port.on('message', handler);\n });\n };\n },\n });\n}\n\n/**\n * Install a message handler on a MessagePort (main thread side) that\n * receives client-call messages from a worker thread and executes them\n * on the real CamundaClient instance.\n */\nexport function installClientCallHandler(port: MessagePort, client: CamundaClient): void {\n port.on('message', async (msg: ClientCallMessage) => {\n if (msg.type !== 'client-call') return;\n try {\n const fn = (client as any)[msg.method];\n if (typeof fn !== 'function') {\n throw new Error(`CamundaClient has no method '${msg.method}'`);\n }\n const result = await fn.apply(client, msg.args);\n const reply: ClientCallResult = { type: 'client-call-result', callId: msg.callId, result };\n port.postMessage(reply);\n } catch (err: any) {\n const reply: ClientCallResult = {\n type: 'client-call-result',\n callId: msg.callId,\n error: err?.message || String(err),\n };\n port.postMessage(reply);\n }\n });\n}\n","import { resolve as resolvePath } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\n/**\n * Turn a handler module reference into a specifier `import()` accepts.\n *\n * Bare specifiers (`@scope/pkg`, `my-handlers`) are passed through so normal package\n * resolution applies. Anything path-shaped is resolved against the working directory and\n * converted to a `file:` URL.\n *\n * The conversion must go through `pathToFileURL`. Hand-building the URL breaks on Windows\n * in three separate ways: `file://C:/...` puts the drive letter in the *host* rather than\n * the path, backslashes are not URL-legal and need escaping, and a bare `C:\\...` handed to\n * `new URL()` parses `C:` as the scheme — which is what produces \"Only URLs with a scheme\n * in: file, data, and node are supported by the default ESM loader\".\n */\nexport function toModuleSpecifier(handlerModule: string): string {\n const isPath =\n handlerModule.startsWith('.') ||\n handlerModule.startsWith('/') ||\n /^[a-zA-Z]:[\\\\/]/.test(handlerModule);\n\n return isPath ? pathToFileURL(resolvePath(handlerModule)).href : handlerModule;\n}\n"],"mappings":"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","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../src/runtime/threadWorkerEntry.ts","../src/runtime/clientProxy.ts","../src/runtime/moduleSpecifier.ts","../src/runtime/threadJobProxy.ts"],"sourcesContent":["/**\n * Worker thread entry point for the shared ThreadPool.\n *\n * Each thread in the pool runs this script. It:\n * 1. Waits for job messages on parentPort\n * 2. Dynamically imports and caches the handler module per handlerModule path\n * 3. Creates a client proxy + job proxy per job\n * 4. Calls the handler, sends back the result\n *\n * Threads are generic — the handler module is specified per-job, not per-thread.\n * Handlers are cached by module path so each import happens only once per thread.\n */\nimport { parentPort } from 'node:worker_threads';\nimport { createClientProxy } from './clientProxy.ts';\nimport { toModuleSpecifier } from './moduleSpecifier.ts';\nimport { createJobProxy } from './threadJobProxy.ts';\n\nif (!parentPort) {\n throw new Error('threadWorkerEntry must run inside a worker_threads Worker');\n}\n\ninterface JobMessage {\n type: 'job';\n /** Unique ID for this job dispatch (correlates response) */\n taskId: string;\n /** Serialized job data (plain object, no methods) */\n jobData: Record<string, unknown>;\n /** Handler module path — resolved per job, cached per thread */\n handlerModule: string;\n}\n\ninterface JobResult {\n type: 'job-result';\n taskId: string;\n ok: boolean;\n error?: string;\n /** When the handler acknowledged the job via complete/fail/error,\n * the action is captured here for the main thread to execute. */\n completionAction?: {\n method: string;\n args: unknown[];\n };\n}\n\n/** Cache of loaded handlers keyed by module path. Each thread loads a handler once per unique path. */\nconst handlerCache = new Map<string, (job: any, client: any) => Promise<any>>();\n\nasync function loadHandler(\n handlerModule: string\n): Promise<(job: any, client: any) => Promise<any>> {\n const cached = handlerCache.get(handlerModule);\n if (cached) return cached;\n\n const mod = await import(toModuleSpecifier(handlerModule));\n const handler = mod.default ?? mod.handler;\n if (typeof handler !== 'function') {\n throw new Error(\n `Threaded handler module '${handlerModule}' must export a default function or named 'handler' export`\n );\n }\n handlerCache.set(handlerModule, handler);\n return handler;\n}\n\nparentPort.on('message', async (msg: JobMessage & { clientPort?: any }) => {\n if (msg.type !== 'job') return;\n\n const { taskId, jobData, handlerModule, clientPort } = msg as JobMessage & {\n clientPort: import('node:worker_threads').MessagePort;\n };\n\n try {\n const handlerFn = await loadHandler(handlerModule);\n\n // Create a client proxy for this job's MessagePort\n const clientProxy = createClientProxy(clientPort);\n\n // Build a job-like object with proxied action methods\n const job = createJobProxy(jobData, clientProxy);\n\n await handlerFn(job, clientProxy);\n\n // If the handler never acknowledged the job (no complete/fail/error/ignore),\n // treat it as an error so the broker can retry rather than silently dropping it.\n if (!job.acknowledged) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error:\n `Handler for '${handlerModule}' returned without acknowledging the job. ` +\n 'Call job.complete(), job.fail(), job.error(), or job.ignore().',\n };\n parentPort!.postMessage(result);\n return;\n }\n\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: true,\n completionAction: job._completionAction || undefined,\n };\n parentPort!.postMessage(result);\n } catch (err: any) {\n const result: JobResult = {\n type: 'job-result',\n taskId,\n ok: false,\n error: err?.message || String(err),\n };\n parentPort!.postMessage(result);\n } finally {\n // Close this job's client port\n if (clientPort && typeof clientPort.close === 'function') {\n clientPort.close();\n }\n }\n});\n\n// Signal ready\nparentPort.postMessage({ type: 'ready' });\n","/**\n * Client proxy for worker_threads: allows threaded job handlers to call\n * CamundaClient methods transparently via MessagePort.\n *\n * Main thread side: installClientCallHandler(port, client)\n * Worker thread side: createClientProxy(port) → CamundaClient-shaped proxy\n */\n\nimport type { MessagePort } from 'node:worker_threads';\nimport type { CamundaClient } from '../gen/CamundaClient';\n\n// Wire protocol\nexport interface ClientCallMessage {\n type: 'client-call';\n callId: string;\n method: string;\n args: unknown[];\n}\n\nexport interface ClientCallResult {\n type: 'client-call-result';\n callId: string;\n result?: unknown;\n error?: string;\n}\n\n/** Fallback UUID generator for runtimes where globalThis.crypto.randomUUID is unavailable. */\nlet _counter = 0;\nfunction fallbackUUID(): string {\n // biome-ignore lint/plugin: correlation-id entropy, not cadence\n return `${Date.now().toString(36)}-${(++_counter).toString(36)}-${Math.random().toString(36).slice(2, 10)}`;\n}\n\n/**\n * Create a Proxy that looks like CamundaClient but forwards every method call\n * over a MessagePort to the main thread. Runs inside a worker thread.\n */\nexport function createClientProxy(port: MessagePort): CamundaClient {\n return new Proxy({} as CamundaClient, {\n get(_, method: string) {\n // Ignore symbol properties and common non-method accesses\n if (typeof method === 'symbol') return undefined;\n if (method === 'then') return undefined; // Prevent Proxy from being treated as thenable\n if (method === 'toJSON') return undefined;\n\n return (...args: unknown[]) => {\n const callId = globalThis.crypto?.randomUUID\n ? globalThis.crypto.randomUUID()\n : fallbackUUID();\n const msg: ClientCallMessage = { type: 'client-call', callId, method, args };\n port.postMessage(msg);\n return new Promise<unknown>((resolve, reject) => {\n const handler = (reply: ClientCallResult) => {\n if (reply.type !== 'client-call-result' || reply.callId !== callId) return;\n port.off('message', handler);\n if (reply.error !== undefined) {\n reject(new Error(reply.error));\n } else {\n resolve(reply.result);\n }\n };\n port.on('message', handler);\n });\n };\n },\n });\n}\n\n/**\n * Install a message handler on a MessagePort (main thread side) that\n * receives client-call messages from a worker thread and executes them\n * on the real CamundaClient instance.\n */\nexport function installClientCallHandler(port: MessagePort, client: CamundaClient): void {\n port.on('message', async (msg: ClientCallMessage) => {\n if (msg.type !== 'client-call') return;\n try {\n const fn = (client as any)[msg.method];\n if (typeof fn !== 'function') {\n throw new Error(`CamundaClient has no method '${msg.method}'`);\n }\n const result = await fn.apply(client, msg.args);\n const reply: ClientCallResult = { type: 'client-call-result', callId: msg.callId, result };\n port.postMessage(reply);\n } catch (err: any) {\n const reply: ClientCallResult = {\n type: 'client-call-result',\n callId: msg.callId,\n error: err?.message || String(err),\n };\n port.postMessage(reply);\n }\n });\n}\n","import { resolve as resolvePath } from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\n/**\n * Turn a handler module reference into a specifier `import()` accepts.\n *\n * Bare specifiers (`@scope/pkg`, `my-handlers`) are passed through so normal package\n * resolution applies. Anything path-shaped is resolved against the working directory and\n * converted to a `file:` URL.\n *\n * The conversion must go through `pathToFileURL`. Hand-building the URL breaks on Windows\n * in three separate ways: `file://C:/...` puts the drive letter in the *host* rather than\n * the path, backslashes are not URL-legal and need escaping, and a bare `C:\\...` handed to\n * `new URL()` parses `C:` as the scheme — which is what produces \"Only URLs with a scheme\n * in: file, data, and node are supported by the default ESM loader\".\n */\nexport function toModuleSpecifier(handlerModule: string): string {\n const isPath =\n handlerModule.startsWith('.') ||\n handlerModule.startsWith('/') ||\n /^[a-zA-Z]:[\\\\/]/.test(handlerModule);\n\n return isPath ? pathToFileURL(resolvePath(handlerModule)).href : handlerModule;\n}\n","/**\n * Build a job object with action methods that proxy through the client, used by\n * the threaded worker entry point (`threadWorkerEntry.ts`).\n *\n * Kept in its own module — separate from the worker-thread bootstrap, which\n * throws on import outside a `worker_threads` Worker — so the proxy's behaviour\n * (in particular that EVERY job action forwards the activation `leaseToken` back\n * to the main-thread client) is unit-testable directly, without spinning up a\n * real worker thread.\n *\n * The job data is plain serialized data from the main thread. Completion actions\n * (complete/fail/error/cancelWorkflow) are captured as intent on\n * `job._completionAction` and executed by the main thread; the non-completion\n * actions (modifyJobTimeout/modifyRetries) proxy straight through the client.\n */\nimport type { JobResult as ApiJobResult } from '../gen/types.gen';\n\n// Inline the JobActionReceipt constant to avoid importing the full SDK dependency\n// chain (jobWorker.ts → ../gen/CamundaClient → entire SDK) into the worker thread.\nconst JobActionReceipt = 'JOB_ACTION_RECEIPT' as const;\n\nexport function createJobProxy(jobData: Record<string, unknown>, client: any): any {\n const acknowledged = { value: false };\n const ack = () => {\n acknowledged.value = true;\n job.acknowledged = true;\n };\n\n const job: any = { ...jobData };\n\n /**\n * Completion actions (complete/fail/error/cancelWorkflow) are stored as intent\n * rather than proxied through the MessagePort. The thread returns immediately,\n * and the main thread executes the API call asynchronously. This keeps threads\n * free for CPU work instead of blocking on I/O round-trips.\n */\n\n job.complete = async (variables: Record<string, unknown> = {}, result?: ApiJobResult) => {\n ack();\n job._completionAction = {\n method: 'completeJob',\n args: [\n {\n variables,\n jobKey: jobData.jobKey,\n ...(result !== undefined && { result }),\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n },\n ],\n };\n return JobActionReceipt;\n };\n\n job.fail = async (reason: any) => {\n ack();\n job._completionAction = {\n method: 'failJob',\n args: [\n {\n ...reason,\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n },\n ],\n };\n return JobActionReceipt;\n };\n\n job.error = async (error: any) => {\n ack();\n job._completionAction = {\n method: 'throwJobError',\n args: [\n {\n ...error,\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n },\n ],\n };\n return JobActionReceipt;\n };\n\n job.cancelWorkflow = async () => {\n ack();\n job._completionAction = {\n method: 'cancelProcessInstance',\n args: [{ processInstanceKey: jobData.processInstanceKey }],\n };\n return JobActionReceipt;\n };\n\n job.ignore = async () => {\n ack();\n return JobActionReceipt;\n };\n\n // Non-completion actions still proxy through the client (rare, need response)\n job.modifyJobTimeout = ({ newTimeoutMs }: { newTimeoutMs: number }) =>\n client.updateJob({\n changeset: { timeout: newTimeoutMs },\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n });\n\n job.modifyRetries = ({ retries }: { retries: number }) =>\n client.updateJob({\n changeset: { retries },\n jobKey: jobData.jobKey,\n ...(jobData.leaseToken != null ? { leaseToken: jobData.leaseToken } : {}),\n });\n\n return job;\n}\n"],"mappings":";AAYA,SAAS,kBAAkB;;;ACe3B,IAAI,WAAW;AACf,SAAS,eAAuB;AAE9B,SAAO,GAAG,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,UAAU,SAAS,EAAE,CAAC,IAAI,KAAK,OAAO,EAAE,SAAS,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC;AAC3G;AAMO,SAAS,kBAAkB,MAAkC;AAClE,SAAO,IAAI,MAAM,CAAC,GAAoB;AAAA,IACpC,IAAI,GAAG,QAAgB;AAErB,UAAI,OAAO,WAAW,SAAU,QAAO;AACvC,UAAI,WAAW,OAAQ,QAAO;AAC9B,UAAI,WAAW,SAAU,QAAO;AAEhC,aAAO,IAAI,SAAoB;AAC7B,cAAM,SAAS,WAAW,QAAQ,aAC9B,WAAW,OAAO,WAAW,IAC7B,aAAa;AACjB,cAAM,MAAyB,EAAE,MAAM,eAAe,QAAQ,QAAQ,KAAK;AAC3E,aAAK,YAAY,GAAG;AACpB,eAAO,IAAI,QAAiB,CAAC,SAAS,WAAW;AAC/C,gBAAM,UAAU,CAAC,UAA4B;AAC3C,gBAAI,MAAM,SAAS,wBAAwB,MAAM,WAAW,OAAQ;AACpE,iBAAK,IAAI,WAAW,OAAO;AAC3B,gBAAI,MAAM,UAAU,QAAW;AAC7B,qBAAO,IAAI,MAAM,MAAM,KAAK,CAAC;AAAA,YAC/B,OAAO;AACL,sBAAQ,MAAM,MAAM;AAAA,YACtB;AAAA,UACF;AACA,eAAK,GAAG,WAAW,OAAO;AAAA,QAC5B,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF,CAAC;AACH;;;AClEA,SAAS,WAAW,mBAAmB;AACvC,SAAS,qBAAqB;AAevB,SAAS,kBAAkB,eAA+B;AAC/D,QAAM,SACJ,cAAc,WAAW,GAAG,KAC5B,cAAc,WAAW,GAAG,KAC5B,kBAAkB,KAAK,aAAa;AAEtC,SAAO,SAAS,cAAc,YAAY,aAAa,CAAC,EAAE,OAAO;AACnE;;;ACJA,IAAM,mBAAmB;AAElB,SAAS,eAAe,SAAkC,QAAkB;AACjF,QAAM,eAAe,EAAE,OAAO,MAAM;AACpC,QAAM,MAAM,MAAM;AAChB,iBAAa,QAAQ;AACrB,QAAI,eAAe;AAAA,EACrB;AAEA,QAAM,MAAW,EAAE,GAAG,QAAQ;AAS9B,MAAI,WAAW,OAAO,YAAqC,CAAC,GAAG,WAA0B;AACvF,QAAI;AACJ,QAAI,oBAAoB;AAAA,MACtB,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ;AAAA,UACE;AAAA,UACA,QAAQ,QAAQ;AAAA,UAChB,GAAI,WAAW,UAAa,EAAE,OAAO;AAAA,UACrC,GAAI,QAAQ,cAAc,OAAO,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,QACzE;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,OAAO,OAAO,WAAgB;AAChC,QAAI;AACJ,QAAI,oBAAoB;AAAA,MACtB,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ;AAAA,UACE,GAAG;AAAA,UACH,QAAQ,QAAQ;AAAA,UAChB,GAAI,QAAQ,cAAc,OAAO,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,QACzE;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,QAAQ,OAAO,UAAe;AAChC,QAAI;AACJ,QAAI,oBAAoB;AAAA,MACtB,QAAQ;AAAA,MACR,MAAM;AAAA,QACJ;AAAA,UACE,GAAG;AAAA,UACH,QAAQ,QAAQ;AAAA,UAChB,GAAI,QAAQ,cAAc,OAAO,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,QACzE;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,YAAY;AAC/B,QAAI;AACJ,QAAI,oBAAoB;AAAA,MACtB,QAAQ;AAAA,MACR,MAAM,CAAC,EAAE,oBAAoB,QAAQ,mBAAmB,CAAC;AAAA,IAC3D;AACA,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,YAAY;AACvB,QAAI;AACJ,WAAO;AAAA,EACT;AAGA,MAAI,mBAAmB,CAAC,EAAE,aAAa,MACrC,OAAO,UAAU;AAAA,IACf,WAAW,EAAE,SAAS,aAAa;AAAA,IACnC,QAAQ,QAAQ;AAAA,IAChB,GAAI,QAAQ,cAAc,OAAO,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,EACzE,CAAC;AAEH,MAAI,gBAAgB,CAAC,EAAE,QAAQ,MAC7B,OAAO,UAAU;AAAA,IACf,WAAW,EAAE,QAAQ;AAAA,IACrB,QAAQ,QAAQ;AAAA,IAChB,GAAI,QAAQ,cAAc,OAAO,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,EACzE,CAAC;AAEH,SAAO;AACT;;;AHhGA,IAAI,CAAC,YAAY;AACf,QAAM,IAAI,MAAM,2DAA2D;AAC7E;AA0BA,IAAM,eAAe,oBAAI,IAAqD;AAE9E,eAAe,YACb,eACkD;AAClD,QAAM,SAAS,aAAa,IAAI,aAAa;AAC7C,MAAI,OAAQ,QAAO;AAEnB,QAAM,MAAM,MAAM,OAAO,kBAAkB,aAAa;AACxD,QAAM,UAAU,IAAI,WAAW,IAAI;AACnC,MAAI,OAAO,YAAY,YAAY;AACjC,UAAM,IAAI;AAAA,MACR,4BAA4B,aAAa;AAAA,IAC3C;AAAA,EACF;AACA,eAAa,IAAI,eAAe,OAAO;AACvC,SAAO;AACT;AAEA,WAAW,GAAG,WAAW,OAAO,QAA2C;AACzE,MAAI,IAAI,SAAS,MAAO;AAExB,QAAM,EAAE,QAAQ,SAAS,eAAe,WAAW,IAAI;AAIvD,MAAI;AACF,UAAM,YAAY,MAAM,YAAY,aAAa;AAGjD,UAAM,cAAc,kBAAkB,UAAU;AAGhD,UAAM,MAAM,eAAe,SAAS,WAAW;AAE/C,UAAM,UAAU,KAAK,WAAW;AAIhC,QAAI,CAAC,IAAI,cAAc;AACrB,YAAMA,UAAoB;AAAA,QACxB,MAAM;AAAA,QACN;AAAA,QACA,IAAI;AAAA,QACJ,OACE,gBAAgB,aAAa;AAAA,MAEjC;AACA,iBAAY,YAAYA,OAAM;AAC9B;AAAA,IACF;AAEA,UAAM,SAAoB;AAAA,MACxB,MAAM;AAAA,MACN;AAAA,MACA,IAAI;AAAA,MACJ,kBAAkB,IAAI,qBAAqB;AAAA,IAC7C;AACA,eAAY,YAAY,MAAM;AAAA,EAChC,SAAS,KAAU;AACjB,UAAM,SAAoB;AAAA,MACxB,MAAM;AAAA,MACN;AAAA,MACA,IAAI;AAAA,MACJ,OAAO,KAAK,WAAW,OAAO,GAAG;AAAA,IACnC;AACA,eAAY,YAAY,MAAM;AAAA,EAChC,UAAE;AAEA,QAAI,cAAc,OAAO,WAAW,UAAU,YAAY;AACxD,iBAAW,MAAM;AAAA,IACnB;AAAA,EACF;AACF,CAAC;AAGD,WAAW,YAAY,EAAE,MAAM,QAAQ,CAAC;","names":["result"]}
|