@open-mercato/core 0.10.1-develop.7736.1.88e07906bf → 0.10.1-develop.7748.1.d745ba61aa
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/api_key/index.js +2 -0
- package/dist/generated/entities/api_key/index.js.map +2 -2
- package/dist/generated/entities/attachment_partition/index.js +2 -0
- package/dist/generated/entities/attachment_partition/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/helpers/integration/attachmentAccessFixtures.js +144 -0
- package/dist/helpers/integration/attachmentAccessFixtures.js.map +7 -0
- package/dist/modules/api_keys/data/entities.js +7 -0
- package/dist/modules/api_keys/data/entities.js.map +2 -2
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js +30 -0
- package/dist/modules/api_keys/migrations/Migration20261012202110_api_keys.js.map +7 -0
- package/dist/modules/api_keys/services/apiKeyService.js +31 -9
- package/dist/modules/api_keys/services/apiKeyService.js.map +2 -2
- package/dist/modules/attachments/api/file/[id]/route.js +16 -10
- package/dist/modules/attachments/api/file/[id]/route.js.map +2 -2
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js +15 -9
- package/dist/modules/attachments/api/image/[id]/[[...slug]]/route.js.map +2 -2
- package/dist/modules/attachments/api/library/[id]/route.js +100 -56
- package/dist/modules/attachments/api/library/[id]/route.js.map +3 -3
- package/dist/modules/attachments/api/library/route.js +56 -17
- package/dist/modules/attachments/api/library/route.js.map +2 -2
- package/dist/modules/attachments/api/partitions/route.js +2 -0
- package/dist/modules/attachments/api/partitions/route.js.map +2 -2
- package/dist/modules/attachments/api/route.js +74 -34
- package/dist/modules/attachments/api/route.js.map +3 -3
- package/dist/modules/attachments/api/transfer/route.js +63 -37
- package/dist/modules/attachments/api/transfer/route.js.map +2 -2
- package/dist/modules/attachments/data/entities.js +3 -0
- package/dist/modules/attachments/data/entities.js.map +2 -2
- package/dist/modules/attachments/data/validators.js +41 -0
- package/dist/modules/attachments/data/validators.js.map +7 -0
- package/dist/modules/attachments/di.js +5 -2
- package/dist/modules/attachments/di.js.map +2 -2
- package/dist/modules/attachments/generators.js +29 -0
- package/dist/modules/attachments/generators.js.map +7 -0
- package/dist/modules/attachments/index.js +8 -1
- package/dist/modules/attachments/index.js.map +2 -2
- package/dist/modules/attachments/lib/access-errors.js +35 -0
- package/dist/modules/attachments/lib/access-errors.js.map +7 -0
- package/dist/modules/attachments/lib/access-mutation.js +23 -0
- package/dist/modules/attachments/lib/access-mutation.js.map +7 -0
- package/dist/modules/attachments/lib/access-protection.js +73 -0
- package/dist/modules/attachments/lib/access-protection.js.map +7 -0
- package/dist/modules/attachments/lib/access-query.js +74 -0
- package/dist/modules/attachments/lib/access-query.js.map +7 -0
- package/dist/modules/attachments/lib/access-registry.js +87 -0
- package/dist/modules/attachments/lib/access-registry.js.map +7 -0
- package/dist/modules/attachments/lib/access-runner.js +161 -0
- package/dist/modules/attachments/lib/access-runner.js.map +7 -0
- package/dist/modules/attachments/lib/access-types.js +1 -0
- package/dist/modules/attachments/lib/access-types.js.map +7 -0
- package/dist/modules/attachments/lib/attachment-service.js +14 -6
- package/dist/modules/attachments/lib/attachment-service.js.map +2 -2
- package/dist/modules/attachments/lib/createFromBuffer.js +2 -0
- package/dist/modules/attachments/lib/createFromBuffer.js.map +2 -2
- package/dist/modules/attachments/lib/partitions.js +3 -2
- package/dist/modules/attachments/lib/partitions.js.map +2 -2
- package/dist/modules/attachments/lib/scoped-upload-service.js +2 -0
- package/dist/modules/attachments/lib/scoped-upload-service.js.map +2 -2
- package/dist/modules/attachments/lib/target-access-service.js +12 -3
- package/dist/modules/attachments/lib/target-access-service.js.map +2 -2
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js +38 -0
- package/dist/modules/attachments/migrations/Migration20261001120000_attachments.js.map +7 -0
- package/dist/modules/attachments/setup.js +10 -0
- package/dist/modules/attachments/setup.js.map +2 -2
- package/dist/modules/notifications/api/[id]/action/route.js +8 -0
- package/dist/modules/notifications/api/[id]/action/route.js.map +2 -2
- package/dist/modules/notifications/api/route.js +17 -0
- package/dist/modules/notifications/api/route.js.map +2 -2
- package/dist/modules/notifications/data/validators.js +1 -1
- package/dist/modules/notifications/data/validators.js.map +2 -2
- package/dist/modules/notifications/lib/notificationService.js +45 -3
- package/dist/modules/notifications/lib/notificationService.js.map +2 -2
- package/dist/modules/notifications/lib/routeHelpers.js +27 -0
- package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
- package/dist/modules/payment_gateways/lib/gateway-service.js +50 -34
- package/dist/modules/payment_gateways/lib/gateway-service.js.map +2 -2
- package/dist/modules/payment_gateways/lib/status-machine.js +10 -1
- package/dist/modules/payment_gateways/lib/status-machine.js.map +2 -2
- package/dist/modules/payment_gateways/lib/webhook-processor.js +1 -1
- package/dist/modules/payment_gateways/lib/webhook-processor.js.map +2 -2
- package/dist/modules/staff/notifications.js +4 -2
- package/dist/modules/staff/notifications.js.map +2 -2
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js +1 -1
- package/dist/modules/workflows/api/portal/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js +7 -1
- package/dist/modules/workflows/api/tasks/[id]/complete/route.js.map +2 -2
- package/dist/modules/workflows/backend/tasks/[id]/page.js +2 -1
- package/dist/modules/workflows/backend/tasks/[id]/page.js.map +2 -2
- package/dist/modules/workflows/data/entities.js.map +2 -2
- package/dist/modules/workflows/lib/activity-executor.js +18 -4
- package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
- package/dist/modules/workflows/lib/activity-types.js +2 -2
- package/dist/modules/workflows/lib/activity-types.js.map +2 -2
- package/dist/modules/workflows/lib/condition-handler.js +7 -1
- package/dist/modules/workflows/lib/condition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/duration.js +13 -0
- package/dist/modules/workflows/lib/duration.js.map +2 -2
- package/dist/modules/workflows/lib/instance-status.js +16 -0
- package/dist/modules/workflows/lib/instance-status.js.map +7 -0
- package/dist/modules/workflows/lib/step-handler.js +9 -2
- package/dist/modules/workflows/lib/step-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-handler.js +16 -8
- package/dist/modules/workflows/lib/task-handler.js.map +2 -2
- package/dist/modules/workflows/lib/task-quick-action.js +13 -10
- package/dist/modules/workflows/lib/task-quick-action.js.map +2 -2
- package/dist/modules/workflows/lib/task-sla.js +10 -0
- package/dist/modules/workflows/lib/task-sla.js.map +2 -2
- package/dist/modules/workflows/lib/transition-handler.js +2 -1
- package/dist/modules/workflows/lib/transition-handler.js.map +2 -2
- package/dist/modules/workflows/lib/workflow-executor.js +29 -1
- package/dist/modules/workflows/lib/workflow-executor.js.map +2 -2
- package/dist/modules/workflows/notifications.js +4 -0
- package/dist/modules/workflows/notifications.js.map +2 -2
- package/generated/entities/api_key/index.ts +1 -0
- package/generated/entities/attachment_partition/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/helpers/integration/attachmentAccessFixtures.ts +141 -0
- package/src/modules/api_keys/data/entities.ts +25 -1
- package/src/modules/api_keys/migrations/.snapshot-open-mercato.json +234 -68
- package/src/modules/api_keys/migrations/Migration20261012202110_api_keys.ts +47 -0
- package/src/modules/api_keys/services/apiKeyService.ts +55 -9
- package/src/modules/attachments/api/file/[id]/route.ts +12 -9
- package/src/modules/attachments/api/image/[id]/[[...slug]]/route.ts +10 -8
- package/src/modules/attachments/api/library/[id]/route.ts +87 -58
- package/src/modules/attachments/api/library/route.ts +40 -11
- package/src/modules/attachments/api/partitions/route.ts +2 -0
- package/src/modules/attachments/api/route.ts +68 -35
- package/src/modules/attachments/api/transfer/route.ts +65 -42
- package/src/modules/attachments/data/entities.ts +4 -0
- package/src/modules/attachments/data/validators.ts +36 -0
- package/src/modules/attachments/di.ts +4 -1
- package/src/modules/attachments/generators.ts +27 -0
- package/src/modules/attachments/i18n/de.json +4 -0
- package/src/modules/attachments/i18n/en.json +4 -0
- package/src/modules/attachments/i18n/es.json +4 -0
- package/src/modules/attachments/i18n/ko.json +4 -0
- package/src/modules/attachments/i18n/pl.json +4 -0
- package/src/modules/attachments/index.ts +18 -0
- package/src/modules/attachments/lib/access-errors.ts +32 -0
- package/src/modules/attachments/lib/access-mutation.ts +26 -0
- package/src/modules/attachments/lib/access-protection.ts +83 -0
- package/src/modules/attachments/lib/access-query.ts +86 -0
- package/src/modules/attachments/lib/access-registry.ts +117 -0
- package/src/modules/attachments/lib/access-runner.ts +183 -0
- package/src/modules/attachments/lib/access-types.ts +78 -0
- package/src/modules/attachments/lib/attachment-service.ts +8 -4
- package/src/modules/attachments/lib/createFromBuffer.ts +3 -0
- package/src/modules/attachments/lib/partitions.ts +3 -2
- package/src/modules/attachments/lib/scoped-upload-service.ts +2 -0
- package/src/modules/attachments/lib/target-access-service.ts +10 -3
- package/src/modules/attachments/migrations/.snapshot-open-mercato.json +17 -0
- package/src/modules/attachments/migrations/Migration20261001120000_attachments.ts +35 -0
- package/src/modules/attachments/setup.ts +10 -0
- package/src/modules/auth/i18n/de.json +3 -0
- package/src/modules/auth/i18n/en.json +3 -0
- package/src/modules/auth/i18n/es.json +3 -0
- package/src/modules/auth/i18n/ko.json +3 -0
- package/src/modules/auth/i18n/pl.json +3 -0
- package/src/modules/notifications/api/[id]/action/route.ts +8 -0
- package/src/modules/notifications/api/route.ts +17 -0
- package/src/modules/notifications/data/validators.ts +1 -1
- package/src/modules/notifications/i18n/de.json +2 -0
- package/src/modules/notifications/i18n/en.json +2 -0
- package/src/modules/notifications/i18n/es.json +2 -0
- package/src/modules/notifications/i18n/ko.json +2 -0
- package/src/modules/notifications/i18n/pl.json +2 -0
- package/src/modules/notifications/lib/notificationService.ts +67 -1
- package/src/modules/notifications/lib/routeHelpers.ts +38 -0
- package/src/modules/payment_gateways/lib/gateway-service.ts +50 -33
- package/src/modules/payment_gateways/lib/status-machine.ts +19 -0
- package/src/modules/payment_gateways/lib/webhook-processor.ts +1 -1
- package/src/modules/staff/notifications.ts +2 -0
- package/src/modules/workflows/AGENTS.md +2 -2
- package/src/modules/workflows/api/portal/tasks/[id]/complete/route.ts +5 -1
- package/src/modules/workflows/api/tasks/[id]/complete/route.ts +7 -1
- package/src/modules/workflows/backend/tasks/[id]/page.tsx +6 -2
- package/src/modules/workflows/data/entities.ts +6 -0
- package/src/modules/workflows/i18n/de.json +2 -0
- package/src/modules/workflows/i18n/en.json +2 -0
- package/src/modules/workflows/i18n/es.json +2 -0
- package/src/modules/workflows/i18n/ko.json +2 -0
- package/src/modules/workflows/i18n/pl.json +2 -0
- package/src/modules/workflows/lib/activity-executor.ts +36 -4
- package/src/modules/workflows/lib/activity-types.ts +2 -2
- package/src/modules/workflows/lib/condition-handler.ts +16 -1
- package/src/modules/workflows/lib/duration.ts +25 -0
- package/src/modules/workflows/lib/instance-status.ts +29 -0
- package/src/modules/workflows/lib/step-handler.ts +16 -1
- package/src/modules/workflows/lib/task-handler.ts +18 -10
- package/src/modules/workflows/lib/task-quick-action.ts +19 -10
- package/src/modules/workflows/lib/task-sla.ts +13 -0
- package/src/modules/workflows/lib/transition-handler.ts +2 -1
- package/src/modules/workflows/lib/workflow-executor.ts +44 -1
- package/src/modules/workflows/notifications.ts +4 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/activity-types.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Workflows Module - Built-in Activity Types\n *\n * Registers the built-in activity types with the Activity Registry (spec\n * 2026-07-26-workflows-ux-redesign.md section 3.2). Each entry delegates to\n * the existing STABLE executeX handlers in activity-executor.ts \u2014 the\n * handlers keep their exported signatures; the registry only adapts them to\n * the uniform (config, ctx, deps) shape.\n *\n * The executor is reached through a runtime binding seam instead of any\n * import: this module sits on the registry bootstrap chain that validators.ts\n * and the visual editor import, and even a dynamic import('./activity-executor')\n * gets chunked into the client bundle by Turbopack, dragging queue/bullmq and\n * node builtins into contexts that forbid them. activity-executor.ts binds its\n * own handlers via bindActivityExecutor() when it loads on the server, so the\n * closures below resolve them without the bundler ever seeing an edge.\n * Registration itself needs only the schemas, form specs, and metadata. WAIT\n * is the exception: its pure delay executes inline so the timer is scheduled\n * synchronously (matching executeWait's observable behavior under fake\n * timers).\n */\n\nimport { z, type ZodTypeAny } from 'zod'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport { getActivityType, registerActivityType } from './activity-registry'\nimport { calculateWaitDelayMs } from './duration'\nimport {\n callApiConfigSchema,\n callWebhookConfigSchema,\n emitEventConfigSchema,\n executeFunctionConfigSchema,\n invokeAgentConfigSchema,\n sendEmailConfigSchema,\n setVariableConfigSchema,\n updateEntityConfigSchema,\n waitConfigSchema,\n type SetVariableConfig,\n type WaitConfig,\n} from '../data/activity-config-schemas'\n\nconst BUILTIN_ACTIVITY_TYPE_IDS = [\n 'SEND_EMAIL',\n 'EMIT_EVENT',\n 'UPDATE_ENTITY',\n 'CALL_WEBHOOK',\n 'EXECUTE_FUNCTION',\n 'WAIT',\n 'CALL_API',\n 'SET_VARIABLE',\n 'INVOKE_AGENT',\n] as const\n\nconst i18nKeyFor = (id: string): string => `workflows.activities.types.${id}`\n\nexport type ActivityExecutorBinding = Pick<\n typeof import('./activity-executor'),\n | 'executeSendEmail'\n | 'executeEmitEvent'\n | 'executeUpdateEntity'\n | 'executeCallWebhook'\n | 'executeFunction'\n | 'executeCallApi'\n | 'executeSetVariable'\n | 'executeInvokeAgent'\n>\n\nlet boundExecutor: ActivityExecutorBinding | null = null\n\nexport function bindActivityExecutor(executor: ActivityExecutorBinding): void {\n boundExecutor = executor\n}\n\nconst loadExecutor = async (): Promise<ActivityExecutorBinding> => {\n if (!boundExecutor) {\n throw new Error('[internal] Activity executor is not bound in this runtime')\n }\n return boundExecutor\n}\n\n/**\n * UPDATE_ENTITY does NOT put the command's return value into context \u2014 it puts\n * `executeUpdateEntity`'s envelope there (`{ executed, commandId, result,\n * logEntryId }`, activity-executor.ts), and the command's own output sits under\n * `result`. Both merge paths carry that envelope verbatim: the sync transition\n * route namespaces it under `activityName || activityType`, the async route\n * under `${activityId}_result`. So the contract has to be the envelope, exactly\n * as INVOKE_AGENT's is \u2014 a contract naming the command's bare keys advertises\n * `<activity>.dealId` for a context that only ever holds\n * `<activity>.result.dealId`.\n */\nconst updateEntityEnvelopeShape = {\n executed: z.boolean(),\n commandId: z.string(),\n logEntryId: z.string(),\n}\n\n/**\n * Resolves UPDATE_ENTITY's output contract from the command registry (import\n * is UI-safe: `commands/registry` pulls in only the logger, no ORM). The\n * lookup is sync over already-registered handlers; a missing or not-yet-loaded\n * handler \u2014 or one without an `outputSchema` \u2014 degrades honestly to 'unknown',\n * which prefix-resolves every ref under the activity key and so stays exactly\n * as permissive as it was before the envelope was modelled.\n */\nconst resolveCommandOutputContract = (config: unknown): ZodTypeAny | 'unknown' => {\n if (typeof config !== 'object' || config === null) return 'unknown'\n const commandId = (config as Record<string, unknown>).commandId\n if (typeof commandId !== 'string' || commandId.length === 0) return 'unknown'\n const outputSchema = commandRegistry.outputSchemaOf(commandId)\n if (!outputSchema) return 'unknown'\n return z.object({ ...updateEntityEnvelopeShape, result: outputSchema })\n}\n\n/**\n * CALL_API's output contract resolves the picked endpoint's declared response\n * schema from the in-process endpoint catalog (lib/endpoint-catalog.ts).\n * That helper is server-only, so \u2014 like the executor \u2014 it is reached through\n * a runtime binding seam: server-output-contract.ts binds the resolver when\n * it loads. Unbound runtimes (the browser), unmatched or free-text endpoints,\n * and templated methods all degrade honestly to 'unknown'.\n */\nexport type CallApiResponseSchemaResolver = (endpoint: string, method: string) => ZodTypeAny | 'unknown'\n\nlet boundCallApiResponseSchemaResolver: CallApiResponseSchemaResolver | null = null\n\nexport function bindCallApiResponseSchemaResolver(resolver: CallApiResponseSchemaResolver): void {\n boundCallApiResponseSchemaResolver = resolver\n}\n\nconst resolveCallApiOutputContract = (config: unknown): ZodTypeAny | 'unknown' => {\n if (!boundCallApiResponseSchemaResolver) return 'unknown'\n if (typeof config !== 'object' || config === null) return 'unknown'\n const record = config as Record<string, unknown>\n const endpoint = record.endpoint\n if (typeof endpoint !== 'string' || endpoint.length === 0) return 'unknown'\n const rawMethod = record.method\n if (rawMethod !== undefined && typeof rawMethod !== 'string') return 'unknown'\n const method = typeof rawMethod === 'string' && rawMethod.length > 0 ? rawMethod : 'GET'\n if (method.includes('{{')) return 'unknown'\n return boundCallApiResponseSchemaResolver(endpoint, method)\n}\n\n/**\n * INVOKE_AGENT's output contract describes the normalized agent-result envelope\n * that `mapAgentResultToContext` reads its `outputMapping` source paths from.\n * The envelope keys are the platform's own contract; the OUTCOME shape under\n * `data` (researcher agents) or `proposalPayload` (proposal agents) belongs\n * to the selected agent and lives in the OPTIONAL agent_orchestrator peer, so it\n * arrives through a runtime binding seam \u2014 core never imports enterprise.\n * server-output-contract.ts binds a resolver backed by the peer's DI bridge;\n * unbound runtimes (the browser), a missing peer, a templated or unknown agent\n * id, and agents without a declared OUTCOME all degrade honestly to 'unknown'.\n */\nexport type AgentOutcomeContract = {\n resultKind: 'research' | 'proposal'\n schema: ZodTypeAny\n}\n\nexport type AgentOutcomeSchemaResolver = (agentId: string) => AgentOutcomeContract | 'unknown'\n\nlet boundAgentOutcomeSchemaResolver: AgentOutcomeSchemaResolver | null = null\n\nexport function bindAgentOutcomeSchemaResolver(resolver: AgentOutcomeSchemaResolver): void {\n boundAgentOutcomeSchemaResolver = resolver\n}\n\nconst agentEnvelopeShape = {\n kind: z.string(),\n disposition: z.string(),\n agentId: z.string(),\n proposalId: z.string(),\n}\n\nconst resolveInvokeAgentOutputContract = (config: unknown): ZodTypeAny | 'unknown' => {\n if (!boundAgentOutcomeSchemaResolver) return 'unknown'\n if (typeof config !== 'object' || config === null) return 'unknown'\n const agentId = (config as Record<string, unknown>).agentId\n if (typeof agentId !== 'string' || agentId.length === 0 || agentId.includes('{{')) return 'unknown'\n const outcome = boundAgentOutcomeSchemaResolver(agentId)\n if (outcome === 'unknown') return 'unknown'\n const outcomeKey = outcome.resultKind === 'research' ? 'data' : 'proposalPayload'\n return z.object({ ...agentEnvelopeShape, [outcomeKey]: outcome.schema })\n}\n\n/**\n * Would-do mocks receive raw (possibly still-templated) config, so they read\n * keys defensively instead of trusting a schema parse: callers like the\n * test-step route interpolate before invoking, but nothing guarantees it.\n */\nconst asConfigRecord = (config: unknown): Record<string, unknown> =>\n typeof config === 'object' && config !== null ? (config as Record<string, unknown>) : {}\n\n/**\n * INVOKE_AGENT's would-do payload (spec section 8.2).\n *\n * It names the agent and the disposition the step would REQUEST \u2014 never a\n * fabricated outcome. A real run's disposition is decided by the model's\n * confidence against `onResult.autoApproveThreshold`, and a simulation has no\n * confidence, so the honest answer is the one `dispositionService` already\n * gives for a missing confidence: fail closed to human review. The envelope\n * deliberately does NOT reuse the runtime `kind` vocabulary\n * (`auto_approved` / `researcher` / `user_task`) so nothing downstream can\n * mistake a simulation for a disposition that actually happened.\n */\nconst buildInvokeAgentWouldDo = (config: unknown): Record<string, unknown> => {\n const record = asConfigRecord(config)\n const onResult = asConfigRecord(record.onResult)\n const threshold = typeof onResult.autoApproveThreshold === 'number'\n ? onResult.autoApproveThreshold\n : null\n return {\n simulated: true,\n invoked: false,\n kind: 'would_invoke',\n wouldInvokeAgent: typeof record.agentId === 'string' ? record.agentId : null,\n wouldRequestDisposition: 'human_review',\n reason: onResult.alwaysAsk === true ? 'alwaysAsk' : 'noConfidenceInSimulation',\n autoApproveThreshold: threshold,\n }\n}\n\nexport function registerBuiltinActivityTypes(): void {\n if (BUILTIN_ACTIVITY_TYPE_IDS.every((id) => getActivityType(id) != null)) return\n\n registerActivityType({\n id: 'SEND_EMAIL',\n icon: 'Mail',\n i18nKey: i18nKeyFor('SEND_EMAIL'),\n configSchema: sendEmailConfigSchema,\n form: [\n { id: 'to', component: 'text', required: true },\n { id: 'subject', component: 'text', required: true },\n { id: 'template', component: 'text' },\n { id: 'body', component: 'textarea' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeSendEmail(config, ctx, deps.container, deps.signal),\n async: { capable: true },\n mock: (config) => {\n const record = asConfigRecord(config)\n return { sent: false, simulated: true, wouldSendTo: record.to, subject: record.subject }\n },\n })\n\n registerActivityType({\n id: 'EMIT_EVENT',\n icon: 'Radio',\n i18nKey: i18nKeyFor('EMIT_EVENT'),\n configSchema: emitEventConfigSchema,\n form: [\n { id: 'eventName', component: 'eventName', required: true },\n { id: 'payload', component: 'json' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeEmitEvent(config, ctx, deps.container, deps.signal),\n async: { capable: true },\n mock: (config) => {\n const record = asConfigRecord(config)\n return { emitted: false, simulated: true, eventName: record.eventName }\n },\n })\n\n registerActivityType({\n id: 'UPDATE_ENTITY',\n icon: 'Database',\n i18nKey: i18nKeyFor('UPDATE_ENTITY'),\n configSchema: updateEntityConfigSchema,\n form: [\n {\n id: 'commandId',\n component: 'commandId',\n required: true,\n descriptionKey: 'workflows.activityConfig.UPDATE_ENTITY.commandIdHint',\n },\n { id: 'input', component: 'json', required: true },\n { id: 'statusDictionary', component: 'text' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeUpdateEntity(deps.em, config, ctx, deps.container, deps.signal),\n async: { capable: true },\n outputContract: resolveCommandOutputContract,\n mock: (config) => {\n const record = asConfigRecord(config)\n return { executed: false, simulated: true, commandId: record.commandId }\n },\n })\n\n registerActivityType({\n id: 'CALL_WEBHOOK',\n icon: 'Webhook',\n i18nKey: i18nKeyFor('CALL_WEBHOOK'),\n configSchema: callWebhookConfigSchema,\n form: [\n {\n id: 'url',\n component: 'text',\n required: true,\n descriptionKey: 'workflows.activityConfig.CALL_WEBHOOK.urlHint',\n },\n { id: 'method', component: 'select' },\n { id: 'headers', component: 'keyValue' },\n { id: 'body', component: 'json' },\n ],\n execute: async (config, ctx, deps) => (await loadExecutor()).executeCallWebhook(config, ctx, {\n signal: deps.signal,\n timeoutMs: deps.timeoutMs,\n }),\n async: { capable: true },\n mock: (config) => {\n const record = asConfigRecord(config)\n return { simulated: true, wouldCall: { url: record.url, method: record.method ?? 'POST' } }\n },\n })\n\n registerActivityType({\n id: 'EXECUTE_FUNCTION',\n icon: 'FunctionSquare',\n i18nKey: i18nKeyFor('EXECUTE_FUNCTION'),\n configSchema: executeFunctionConfigSchema,\n form: [\n {\n id: 'functionName',\n component: 'functionName',\n required: true,\n descriptionKey: 'workflows.activityConfig.EXECUTE_FUNCTION.functionNameHint',\n },\n { id: 'args', component: 'json' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeFunction(config, ctx, deps.container, deps.signal),\n async: { capable: true },\n mock: 'refuse',\n })\n\n registerActivityType<WaitConfig>({\n id: 'WAIT',\n icon: 'Clock',\n i18nKey: i18nKeyFor('WAIT'),\n configSchema: waitConfigSchema,\n form: [\n { id: 'duration', component: 'duration' },\n { id: 'until', component: 'datetime' },\n ],\n execute: (config) => {\n const durationMs = calculateWaitDelayMs(config)\n return new Promise((resolve) => {\n setTimeout(() => resolve({ waited: true, durationMs }), durationMs)\n })\n },\n executeAsync: async () => ({ waited: true, async: true }),\n async: { capable: true },\n enqueueDelayMs: (config) =>\n config.duration || config.until ? calculateWaitDelayMs(config) : null,\n mock: () => ({ waited: true, simulated: true }),\n })\n\n registerActivityType({\n id: 'CALL_API',\n icon: 'Globe',\n i18nKey: i18nKeyFor('CALL_API'),\n configSchema: callApiConfigSchema,\n form: [\n {\n id: 'endpoint',\n component: 'endpoint',\n required: true,\n descriptionKey: 'workflows.activityConfig.CALL_API.endpointHint',\n },\n { id: 'method', component: 'select' },\n { id: 'headers', component: 'keyValue' },\n { id: 'body', component: 'json' },\n { id: 'validateTenantMatch', component: 'checkbox' },\n ],\n execute: async (config, ctx, deps) => (await loadExecutor()).executeCallApi(deps.em, config, ctx, deps.container, deps.signal),\n async: { capable: false, reason: 'mintsPerRequestKey' },\n outputContract: resolveCallApiOutputContract,\n mock: 'refuse',\n })\n\n registerActivityType<SetVariableConfig>({\n id: 'SET_VARIABLE',\n icon: 'Variable',\n i18nKey: i18nKeyFor('SET_VARIABLE'),\n configSchema: setVariableConfigSchema,\n form: [\n {\n id: 'assignments',\n component: 'assignments',\n required: true,\n descriptionKey: 'workflows.activityConfig.SET_VARIABLE.assignmentsHint',\n },\n ],\n execute: async (config, ctx) => (await loadExecutor()).executeSetVariable(config, ctx),\n async: { capable: false, reason: 'asyncResumeMergeDoesNotApplyAssignments' },\n mock: (config) => ({ simulated: true, assignments: config.assignments }),\n })\n\n registerActivityType({\n id: 'INVOKE_AGENT',\n icon: 'Bot',\n i18nKey: i18nKeyFor('INVOKE_AGENT'),\n configSchema: invokeAgentConfigSchema,\n form: [\n { id: 'agentId', component: 'text', required: true },\n { id: 'input', component: 'json' },\n { id: 'onResult', component: 'json', required: true },\n { id: 'outputMapping', component: 'json' },\n ],\n // SYNC-dispatched by design: execute() enqueues an 'invoke_agent' job onto\n // the dedicated workflow-invoke-agent queue and returns a `__park` marker so\n // the step handler PAUSES the instance on INVOKE_AGENT_SIGNAL_NAME. The job\n // rides its own queue and resumes via sendSignal \u2014 not the generic\n // resumeWorkflowAfterActivities path \u2014 so the activity is not async-capable\n // in the registry's sense.\n execute: async (config, ctx, deps) => (await loadExecutor()).executeInvokeAgent(config, ctx, deps.container),\n async: { capable: false, reason: 'parksOnDedicatedQueue' },\n mock: buildInvokeAgentWouldDo,\n outputContract: resolveInvokeAgentOutputContract,\n })\n}\n\nregisterBuiltinActivityTypes()\n"],
|
|
5
|
-
"mappings": "AAsBA,SAAS,SAA0B;AACnC,SAAS,uBAAuB;AAChC,SAAS,iBAAiB,4BAA4B;AACtD,SAAS,4BAA4B;
|
|
4
|
+
"sourcesContent": ["/**\n * Workflows Module - Built-in Activity Types\n *\n * Registers the built-in activity types with the Activity Registry (spec\n * 2026-07-26-workflows-ux-redesign.md section 3.2). Each entry delegates to\n * the existing STABLE executeX handlers in activity-executor.ts \u2014 the\n * handlers keep their exported signatures; the registry only adapts them to\n * the uniform (config, ctx, deps) shape.\n *\n * The executor is reached through a runtime binding seam instead of any\n * import: this module sits on the registry bootstrap chain that validators.ts\n * and the visual editor import, and even a dynamic import('./activity-executor')\n * gets chunked into the client bundle by Turbopack, dragging queue/bullmq and\n * node builtins into contexts that forbid them. activity-executor.ts binds its\n * own handlers via bindActivityExecutor() when it loads on the server, so the\n * closures below resolve them without the bundler ever seeing an edge.\n * Registration itself needs only the schemas, form specs, and metadata. WAIT\n * is the exception: its pure delay executes inline so the timer is scheduled\n * synchronously (matching executeWait's observable behavior under fake\n * timers).\n */\n\nimport { z, type ZodTypeAny } from 'zod'\nimport { commandRegistry } from '@open-mercato/shared/lib/commands/registry'\nimport { getActivityType, registerActivityType } from './activity-registry'\nimport { assertInProcessWaitDelay, calculateWaitDelayMs } from './duration'\nimport {\n callApiConfigSchema,\n callWebhookConfigSchema,\n emitEventConfigSchema,\n executeFunctionConfigSchema,\n invokeAgentConfigSchema,\n sendEmailConfigSchema,\n setVariableConfigSchema,\n updateEntityConfigSchema,\n waitConfigSchema,\n type SetVariableConfig,\n type WaitConfig,\n} from '../data/activity-config-schemas'\n\nconst BUILTIN_ACTIVITY_TYPE_IDS = [\n 'SEND_EMAIL',\n 'EMIT_EVENT',\n 'UPDATE_ENTITY',\n 'CALL_WEBHOOK',\n 'EXECUTE_FUNCTION',\n 'WAIT',\n 'CALL_API',\n 'SET_VARIABLE',\n 'INVOKE_AGENT',\n] as const\n\nconst i18nKeyFor = (id: string): string => `workflows.activities.types.${id}`\n\nexport type ActivityExecutorBinding = Pick<\n typeof import('./activity-executor'),\n | 'executeSendEmail'\n | 'executeEmitEvent'\n | 'executeUpdateEntity'\n | 'executeCallWebhook'\n | 'executeFunction'\n | 'executeCallApi'\n | 'executeSetVariable'\n | 'executeInvokeAgent'\n>\n\nlet boundExecutor: ActivityExecutorBinding | null = null\n\nexport function bindActivityExecutor(executor: ActivityExecutorBinding): void {\n boundExecutor = executor\n}\n\nconst loadExecutor = async (): Promise<ActivityExecutorBinding> => {\n if (!boundExecutor) {\n throw new Error('[internal] Activity executor is not bound in this runtime')\n }\n return boundExecutor\n}\n\n/**\n * UPDATE_ENTITY does NOT put the command's return value into context \u2014 it puts\n * `executeUpdateEntity`'s envelope there (`{ executed, commandId, result,\n * logEntryId }`, activity-executor.ts), and the command's own output sits under\n * `result`. Both merge paths carry that envelope verbatim: the sync transition\n * route namespaces it under `activityName || activityType`, the async route\n * under `${activityId}_result`. So the contract has to be the envelope, exactly\n * as INVOKE_AGENT's is \u2014 a contract naming the command's bare keys advertises\n * `<activity>.dealId` for a context that only ever holds\n * `<activity>.result.dealId`.\n */\nconst updateEntityEnvelopeShape = {\n executed: z.boolean(),\n commandId: z.string(),\n logEntryId: z.string(),\n}\n\n/**\n * Resolves UPDATE_ENTITY's output contract from the command registry (import\n * is UI-safe: `commands/registry` pulls in only the logger, no ORM). The\n * lookup is sync over already-registered handlers; a missing or not-yet-loaded\n * handler \u2014 or one without an `outputSchema` \u2014 degrades honestly to 'unknown',\n * which prefix-resolves every ref under the activity key and so stays exactly\n * as permissive as it was before the envelope was modelled.\n */\nconst resolveCommandOutputContract = (config: unknown): ZodTypeAny | 'unknown' => {\n if (typeof config !== 'object' || config === null) return 'unknown'\n const commandId = (config as Record<string, unknown>).commandId\n if (typeof commandId !== 'string' || commandId.length === 0) return 'unknown'\n const outputSchema = commandRegistry.outputSchemaOf(commandId)\n if (!outputSchema) return 'unknown'\n return z.object({ ...updateEntityEnvelopeShape, result: outputSchema })\n}\n\n/**\n * CALL_API's output contract resolves the picked endpoint's declared response\n * schema from the in-process endpoint catalog (lib/endpoint-catalog.ts).\n * That helper is server-only, so \u2014 like the executor \u2014 it is reached through\n * a runtime binding seam: server-output-contract.ts binds the resolver when\n * it loads. Unbound runtimes (the browser), unmatched or free-text endpoints,\n * and templated methods all degrade honestly to 'unknown'.\n */\nexport type CallApiResponseSchemaResolver = (endpoint: string, method: string) => ZodTypeAny | 'unknown'\n\nlet boundCallApiResponseSchemaResolver: CallApiResponseSchemaResolver | null = null\n\nexport function bindCallApiResponseSchemaResolver(resolver: CallApiResponseSchemaResolver): void {\n boundCallApiResponseSchemaResolver = resolver\n}\n\nconst resolveCallApiOutputContract = (config: unknown): ZodTypeAny | 'unknown' => {\n if (!boundCallApiResponseSchemaResolver) return 'unknown'\n if (typeof config !== 'object' || config === null) return 'unknown'\n const record = config as Record<string, unknown>\n const endpoint = record.endpoint\n if (typeof endpoint !== 'string' || endpoint.length === 0) return 'unknown'\n const rawMethod = record.method\n if (rawMethod !== undefined && typeof rawMethod !== 'string') return 'unknown'\n const method = typeof rawMethod === 'string' && rawMethod.length > 0 ? rawMethod : 'GET'\n if (method.includes('{{')) return 'unknown'\n return boundCallApiResponseSchemaResolver(endpoint, method)\n}\n\n/**\n * INVOKE_AGENT's output contract describes the normalized agent-result envelope\n * that `mapAgentResultToContext` reads its `outputMapping` source paths from.\n * The envelope keys are the platform's own contract; the OUTCOME shape under\n * `data` (researcher agents) or `proposalPayload` (proposal agents) belongs\n * to the selected agent and lives in the OPTIONAL agent_orchestrator peer, so it\n * arrives through a runtime binding seam \u2014 core never imports enterprise.\n * server-output-contract.ts binds a resolver backed by the peer's DI bridge;\n * unbound runtimes (the browser), a missing peer, a templated or unknown agent\n * id, and agents without a declared OUTCOME all degrade honestly to 'unknown'.\n */\nexport type AgentOutcomeContract = {\n resultKind: 'research' | 'proposal'\n schema: ZodTypeAny\n}\n\nexport type AgentOutcomeSchemaResolver = (agentId: string) => AgentOutcomeContract | 'unknown'\n\nlet boundAgentOutcomeSchemaResolver: AgentOutcomeSchemaResolver | null = null\n\nexport function bindAgentOutcomeSchemaResolver(resolver: AgentOutcomeSchemaResolver): void {\n boundAgentOutcomeSchemaResolver = resolver\n}\n\nconst agentEnvelopeShape = {\n kind: z.string(),\n disposition: z.string(),\n agentId: z.string(),\n proposalId: z.string(),\n}\n\nconst resolveInvokeAgentOutputContract = (config: unknown): ZodTypeAny | 'unknown' => {\n if (!boundAgentOutcomeSchemaResolver) return 'unknown'\n if (typeof config !== 'object' || config === null) return 'unknown'\n const agentId = (config as Record<string, unknown>).agentId\n if (typeof agentId !== 'string' || agentId.length === 0 || agentId.includes('{{')) return 'unknown'\n const outcome = boundAgentOutcomeSchemaResolver(agentId)\n if (outcome === 'unknown') return 'unknown'\n const outcomeKey = outcome.resultKind === 'research' ? 'data' : 'proposalPayload'\n return z.object({ ...agentEnvelopeShape, [outcomeKey]: outcome.schema })\n}\n\n/**\n * Would-do mocks receive raw (possibly still-templated) config, so they read\n * keys defensively instead of trusting a schema parse: callers like the\n * test-step route interpolate before invoking, but nothing guarantees it.\n */\nconst asConfigRecord = (config: unknown): Record<string, unknown> =>\n typeof config === 'object' && config !== null ? (config as Record<string, unknown>) : {}\n\n/**\n * INVOKE_AGENT's would-do payload (spec section 8.2).\n *\n * It names the agent and the disposition the step would REQUEST \u2014 never a\n * fabricated outcome. A real run's disposition is decided by the model's\n * confidence against `onResult.autoApproveThreshold`, and a simulation has no\n * confidence, so the honest answer is the one `dispositionService` already\n * gives for a missing confidence: fail closed to human review. The envelope\n * deliberately does NOT reuse the runtime `kind` vocabulary\n * (`auto_approved` / `researcher` / `user_task`) so nothing downstream can\n * mistake a simulation for a disposition that actually happened.\n */\nconst buildInvokeAgentWouldDo = (config: unknown): Record<string, unknown> => {\n const record = asConfigRecord(config)\n const onResult = asConfigRecord(record.onResult)\n const threshold = typeof onResult.autoApproveThreshold === 'number'\n ? onResult.autoApproveThreshold\n : null\n return {\n simulated: true,\n invoked: false,\n kind: 'would_invoke',\n wouldInvokeAgent: typeof record.agentId === 'string' ? record.agentId : null,\n wouldRequestDisposition: 'human_review',\n reason: onResult.alwaysAsk === true ? 'alwaysAsk' : 'noConfidenceInSimulation',\n autoApproveThreshold: threshold,\n }\n}\n\nexport function registerBuiltinActivityTypes(): void {\n if (BUILTIN_ACTIVITY_TYPE_IDS.every((id) => getActivityType(id) != null)) return\n\n registerActivityType({\n id: 'SEND_EMAIL',\n icon: 'Mail',\n i18nKey: i18nKeyFor('SEND_EMAIL'),\n configSchema: sendEmailConfigSchema,\n form: [\n { id: 'to', component: 'text', required: true },\n { id: 'subject', component: 'text', required: true },\n { id: 'template', component: 'text' },\n { id: 'body', component: 'textarea' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeSendEmail(config, ctx, deps.container, deps.signal),\n async: { capable: true },\n mock: (config) => {\n const record = asConfigRecord(config)\n return { sent: false, simulated: true, wouldSendTo: record.to, subject: record.subject }\n },\n })\n\n registerActivityType({\n id: 'EMIT_EVENT',\n icon: 'Radio',\n i18nKey: i18nKeyFor('EMIT_EVENT'),\n configSchema: emitEventConfigSchema,\n form: [\n { id: 'eventName', component: 'eventName', required: true },\n { id: 'payload', component: 'json' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeEmitEvent(config, ctx, deps.container, deps.signal),\n async: { capable: true },\n mock: (config) => {\n const record = asConfigRecord(config)\n return { emitted: false, simulated: true, eventName: record.eventName }\n },\n })\n\n registerActivityType({\n id: 'UPDATE_ENTITY',\n icon: 'Database',\n i18nKey: i18nKeyFor('UPDATE_ENTITY'),\n configSchema: updateEntityConfigSchema,\n form: [\n {\n id: 'commandId',\n component: 'commandId',\n required: true,\n descriptionKey: 'workflows.activityConfig.UPDATE_ENTITY.commandIdHint',\n },\n { id: 'input', component: 'json', required: true },\n { id: 'statusDictionary', component: 'text' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeUpdateEntity(deps.em, config, ctx, deps.container, deps.signal),\n async: { capable: true },\n outputContract: resolveCommandOutputContract,\n mock: (config) => {\n const record = asConfigRecord(config)\n return { executed: false, simulated: true, commandId: record.commandId }\n },\n })\n\n registerActivityType({\n id: 'CALL_WEBHOOK',\n icon: 'Webhook',\n i18nKey: i18nKeyFor('CALL_WEBHOOK'),\n configSchema: callWebhookConfigSchema,\n form: [\n {\n id: 'url',\n component: 'text',\n required: true,\n descriptionKey: 'workflows.activityConfig.CALL_WEBHOOK.urlHint',\n },\n { id: 'method', component: 'select' },\n { id: 'headers', component: 'keyValue' },\n { id: 'body', component: 'json' },\n ],\n execute: async (config, ctx, deps) => (await loadExecutor()).executeCallWebhook(config, ctx, {\n signal: deps.signal,\n timeoutMs: deps.timeoutMs,\n }),\n async: { capable: true },\n mock: (config) => {\n const record = asConfigRecord(config)\n return { simulated: true, wouldCall: { url: record.url, method: record.method ?? 'POST' } }\n },\n })\n\n registerActivityType({\n id: 'EXECUTE_FUNCTION',\n icon: 'FunctionSquare',\n i18nKey: i18nKeyFor('EXECUTE_FUNCTION'),\n configSchema: executeFunctionConfigSchema,\n form: [\n {\n id: 'functionName',\n component: 'functionName',\n required: true,\n descriptionKey: 'workflows.activityConfig.EXECUTE_FUNCTION.functionNameHint',\n },\n { id: 'args', component: 'json' },\n ],\n execute: async (config, ctx, deps) =>\n (await loadExecutor()).executeFunction(config, ctx, deps.container, deps.signal),\n async: { capable: true },\n mock: 'refuse',\n })\n\n registerActivityType<WaitConfig>({\n id: 'WAIT',\n icon: 'Clock',\n i18nKey: i18nKeyFor('WAIT'),\n configSchema: waitConfigSchema,\n form: [\n { id: 'duration', component: 'duration' },\n { id: 'until', component: 'datetime' },\n ],\n execute: (config) => {\n const durationMs = assertInProcessWaitDelay(calculateWaitDelayMs(config))\n return new Promise((resolve) => {\n setTimeout(() => resolve({ waited: true, durationMs }), durationMs)\n })\n },\n executeAsync: async () => ({ waited: true, async: true }),\n async: { capable: true },\n enqueueDelayMs: (config) =>\n config.duration || config.until ? calculateWaitDelayMs(config) : null,\n mock: () => ({ waited: true, simulated: true }),\n })\n\n registerActivityType({\n id: 'CALL_API',\n icon: 'Globe',\n i18nKey: i18nKeyFor('CALL_API'),\n configSchema: callApiConfigSchema,\n form: [\n {\n id: 'endpoint',\n component: 'endpoint',\n required: true,\n descriptionKey: 'workflows.activityConfig.CALL_API.endpointHint',\n },\n { id: 'method', component: 'select' },\n { id: 'headers', component: 'keyValue' },\n { id: 'body', component: 'json' },\n { id: 'validateTenantMatch', component: 'checkbox' },\n ],\n execute: async (config, ctx, deps) => (await loadExecutor()).executeCallApi(deps.em, config, ctx, deps.container, deps.signal),\n async: { capable: false, reason: 'mintsPerRequestKey' },\n outputContract: resolveCallApiOutputContract,\n mock: 'refuse',\n })\n\n registerActivityType<SetVariableConfig>({\n id: 'SET_VARIABLE',\n icon: 'Variable',\n i18nKey: i18nKeyFor('SET_VARIABLE'),\n configSchema: setVariableConfigSchema,\n form: [\n {\n id: 'assignments',\n component: 'assignments',\n required: true,\n descriptionKey: 'workflows.activityConfig.SET_VARIABLE.assignmentsHint',\n },\n ],\n execute: async (config, ctx) => (await loadExecutor()).executeSetVariable(config, ctx),\n async: { capable: false, reason: 'asyncResumeMergeDoesNotApplyAssignments' },\n mock: (config) => ({ simulated: true, assignments: config.assignments }),\n })\n\n registerActivityType({\n id: 'INVOKE_AGENT',\n icon: 'Bot',\n i18nKey: i18nKeyFor('INVOKE_AGENT'),\n configSchema: invokeAgentConfigSchema,\n form: [\n { id: 'agentId', component: 'text', required: true },\n { id: 'input', component: 'json' },\n { id: 'onResult', component: 'json', required: true },\n { id: 'outputMapping', component: 'json' },\n ],\n // SYNC-dispatched by design: execute() enqueues an 'invoke_agent' job onto\n // the dedicated workflow-invoke-agent queue and returns a `__park` marker so\n // the step handler PAUSES the instance on INVOKE_AGENT_SIGNAL_NAME. The job\n // rides its own queue and resumes via sendSignal \u2014 not the generic\n // resumeWorkflowAfterActivities path \u2014 so the activity is not async-capable\n // in the registry's sense.\n execute: async (config, ctx, deps) => (await loadExecutor()).executeInvokeAgent(config, ctx, deps.container),\n async: { capable: false, reason: 'parksOnDedicatedQueue' },\n mock: buildInvokeAgentWouldDo,\n outputContract: resolveInvokeAgentOutputContract,\n })\n}\n\nregisterBuiltinActivityTypes()\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -29,6 +29,11 @@ function resolveMaxConditionAttempts() {
|
|
|
29
29
|
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_CONDITION_ATTEMPTS;
|
|
30
30
|
return parsed;
|
|
31
31
|
}
|
|
32
|
+
function resolveConditionAttemptCap(config) {
|
|
33
|
+
const floor = resolveMaxConditionAttempts();
|
|
34
|
+
if (!(config.timeoutMs > 0) || !(config.pollIntervalMs > 0)) return floor;
|
|
35
|
+
return Math.max(floor, Math.ceil(config.timeoutMs / config.pollIntervalMs) + 1);
|
|
36
|
+
}
|
|
32
37
|
function readWaitForConditionConfig(stepDef) {
|
|
33
38
|
const step = stepDef ?? {};
|
|
34
39
|
const config = step.config ?? {};
|
|
@@ -295,7 +300,7 @@ async function evaluateWaitCondition(em, container, options) {
|
|
|
295
300
|
}
|
|
296
301
|
const deadlineMs = Date.parse(options.deadlineAt);
|
|
297
302
|
const deadlinePassed = Number.isFinite(deadlineMs) ? nowMs >= deadlineMs : true;
|
|
298
|
-
const attemptCapReached = attempts >=
|
|
303
|
+
const attemptCapReached = attempts >= resolveConditionAttemptCap(waiter.config);
|
|
299
304
|
if (deadlinePassed || attemptCapReached) {
|
|
300
305
|
const eventLogger2 = container.resolve("eventLogger");
|
|
301
306
|
await eventLogger2.logWorkflowEvent(em, {
|
|
@@ -437,6 +442,7 @@ export {
|
|
|
437
442
|
evaluateConditionExpression,
|
|
438
443
|
evaluateWaitCondition,
|
|
439
444
|
readWaitForConditionConfig,
|
|
445
|
+
resolveConditionAttemptCap,
|
|
440
446
|
resolveMaxConditionAttempts,
|
|
441
447
|
wakeConditionWaiters
|
|
442
448
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/condition-handler.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Condition Handler Service\n *\n * Drives WAIT_FOR_CONDITION steps: a step that pauses a workflow (or a single\n * parallel branch) until a boolean predicate over the run context holds.\n *\n * Two paths resume a waiter:\n * - `wakeConditionWaiters` \u2014 the event-driven fast path, called by the scoped\n * context-write endpoint right after a patch is merged.\n * - `evaluateWaitCondition` \u2014 the durability backstop, dispatched by the\n * activity worker from a delayed `kind: 'condition'` queue job. It also\n * owns timeout enforcement, so a wait can never hang forever.\n *\n * The predicate reuses the business_rules ConditionExpression language and\n * `ruleEvaluator.evaluateConditions` \u2014 the same evaluator that backs transition\n * conditions \u2014 so there is no second expression dialect.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport * as ruleEvaluator from '../../business_rules/lib/rule-evaluator'\nimport {\n WorkflowInstance,\n WorkflowDefinition,\n WorkflowBranchInstance,\n StepInstance,\n} from '../data/entities'\nimport { parseDuration } from './duration'\nimport { resolveCodeDefinitionForInstance } from './find-definition'\nimport { branchToken, rootToken, tokenReadContext } from './execution-token'\nimport type * as eventLoggerModule from './event-logger'\nimport type * as stepHandlerModule from './step-handler'\nimport type * as transitionHandlerModule from './transition-handler'\nimport type * as workflowExecutorModule from './workflow-executor'\n\nexport const CONDITION_STEP_TYPE = 'WAIT_FOR_CONDITION'\n\nexport const DEFAULT_CONDITION_POLL_INTERVAL_MS = 30_000\nexport const MIN_CONDITION_POLL_INTERVAL_MS = 5_000\nexport const MAX_CONDITION_POLL_INTERVAL_MS = 3_600_000\nexport const DEFAULT_MAX_CONDITION_ATTEMPTS = 1000\n\nexport class ConditionWaitError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: Record<string, unknown>\n ) {\n super(message)\n this.name = 'ConditionWaitError'\n }\n}\n\nexport type ConditionTimeoutBehaviour = 'FAIL' | 'CONTINUE'\n\nexport interface WaitForConditionConfig {\n condition: unknown\n timeout: string\n timeoutMs: number\n onTimeout: ConditionTimeoutBehaviour\n pollIntervalMs: number\n}\n\nexport type ConditionWokenBy = 'immediate' | 'poll' | 'context-write'\n\nexport interface EvaluateWaitConditionOptions {\n instanceId: string\n stepInstanceId: string\n branchInstanceId?: string | null\n deadlineAt: string\n attempt: number\n userId?: string\n tenantId: string\n organizationId: string\n}\n\nexport type WaitConditionOutcome = 'met' | 'pending' | 'timed_out' | 'noop'\n\nexport interface WaitConditionEvaluationResult {\n outcome: WaitConditionOutcome\n reason?: string\n}\n\nexport interface WakeConditionWaitersOptions {\n instanceId: string\n tenantId: string\n organizationId: string\n userId?: string\n}\n\nexport interface WakeConditionWaitersResult {\n woken: string[]\n}\n\n/**\n * Hard cap on poll re-enqueues, so a never-satisfiable predicate cannot\n * saturate the queue even if the deadline arithmetic is somehow skewed.\n */\nexport function resolveMaxConditionAttempts(): number {\n const raw = process.env.OM_WORKFLOWS_MAX_CONDITION_ATTEMPTS\n if (!raw) return DEFAULT_MAX_CONDITION_ATTEMPTS\n const parsed = Number.parseInt(raw, 10)\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_CONDITION_ATTEMPTS\n return parsed\n}\n\n/**\n * Read and normalise a WAIT_FOR_CONDITION step's config. Fails closed: the\n * predicate and the timeout are both mandatory, and the poll interval is\n * clamped into its supported range and never allowed past the deadline.\n */\nexport function readWaitForConditionConfig(stepDef: unknown): WaitForConditionConfig {\n const step = (stepDef ?? {}) as { stepId?: string; config?: Record<string, unknown> | null }\n const config = (step.config ?? {}) as Record<string, unknown>\n\n const condition = config.condition\n if (condition == null) {\n throw new ConditionWaitError(\n 'WAIT_FOR_CONDITION requires a \"condition\" expression',\n 'CONDITION_CONFIG_MISSING',\n { stepId: step.stepId }\n )\n }\n\n const timeout = config.timeout\n if (typeof timeout !== 'string' || timeout.length === 0) {\n throw new ConditionWaitError(\n 'WAIT_FOR_CONDITION requires a \"timeout\" ISO 8601 duration',\n 'CONDITION_TIMEOUT_MISSING',\n { stepId: step.stepId }\n )\n }\n\n let timeoutMs: number\n try {\n timeoutMs = parseDuration(timeout)\n } catch {\n throw new ConditionWaitError(\n `WAIT_FOR_CONDITION invalid \"timeout\" duration: ${timeout}`,\n 'CONDITION_TIMEOUT_INVALID',\n { stepId: step.stepId, timeout }\n )\n }\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new ConditionWaitError(\n `WAIT_FOR_CONDITION \"timeout\" must be a positive duration: ${timeout}`,\n 'CONDITION_TIMEOUT_INVALID',\n { stepId: step.stepId, timeout }\n )\n }\n\n const onTimeout: ConditionTimeoutBehaviour = config.onTimeout === 'CONTINUE' ? 'CONTINUE' : 'FAIL'\n\n const rawPollInterval =\n typeof config.pollIntervalMs === 'number' && Number.isFinite(config.pollIntervalMs)\n ? config.pollIntervalMs\n : DEFAULT_CONDITION_POLL_INTERVAL_MS\n const clampedPollInterval = Math.min(\n Math.max(rawPollInterval, MIN_CONDITION_POLL_INTERVAL_MS),\n MAX_CONDITION_POLL_INTERVAL_MS\n )\n\n return {\n condition,\n timeout,\n timeoutMs,\n onTimeout,\n pollIntervalMs: Math.min(clampedPollInterval, timeoutMs),\n }\n}\n\n/**\n * Evaluate a wait predicate against a read context. Shared by the step handler\n * (entry evaluation) and both resume paths, so all three agree on semantics.\n */\nexport async function evaluateConditionExpression(params: {\n condition: unknown\n data: Record<string, unknown>\n instanceId: string\n userId?: string\n}): Promise<boolean> {\n const evaluationContext: ruleEvaluator.RuleEvaluationContext = {\n entityType: 'workflow:wait_condition',\n entityId: params.instanceId,\n user: params.userId ? { id: params.userId } : undefined,\n }\n return await ruleEvaluator.evaluateConditions(params.condition, params.data, evaluationContext)\n}\n\n/** Read context a waiter's predicate sees \u2014 instance context, branch overlay included. */\nexport function conditionReadContext(\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance | null\n): Record<string, unknown> {\n const token = branch ? branchToken(instance, branch) : rootToken(instance)\n return tokenReadContext(token)\n}\n\nasync function loadDefinitionForInstance(\n em: EntityManager,\n instance: WorkflowInstance\n): Promise<WorkflowDefinition | null> {\n const stored = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowDefinition,\n {\n id: instance.definitionId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId }\n )\n return stored ?? resolveCodeDefinitionForInstance(instance)\n}\n\nfunction findStepDefinition(definition: WorkflowDefinition, stepId: string): Record<string, unknown> | null {\n const steps = (definition.definition?.steps ?? []) as Array<Record<string, unknown>>\n return steps.find((step) => step.stepId === stepId) ?? null\n}\n\nfunction waitedMsSince(stepInstance: StepInstance, now: number): number {\n const enteredAt = stepInstance.enteredAt ? stepInstance.enteredAt.getTime() : now\n return Math.max(0, now - enteredAt)\n}\n\ninterface ResolvedWaiter {\n stepInstance: StepInstance\n branch: WorkflowBranchInstance | null\n stepDef: Record<string, unknown>\n config: WaitForConditionConfig\n}\n\nasync function loadBranch(\n em: EntityManager,\n instance: WorkflowInstance,\n branchInstanceId: string\n): Promise<WorkflowBranchInstance | null> {\n return await em.findOne(WorkflowBranchInstance, {\n id: branchInstanceId,\n workflowInstanceId: instance.id,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Exit a satisfied (or CONTINUE-on-timeout) waiter and drive the workflow on.\n * Branch waiters take the `resumeBranch` path so sibling branches are untouched.\n */\nasync function completeConditionWait(\n em: EntityManager,\n container: AwilixContainer,\n params: {\n instance: WorkflowInstance\n definition: WorkflowDefinition\n stepInstance: StepInstance\n branch: WorkflowBranchInstance | null\n attempts: number\n waitedMs: number\n wokenBy: ConditionWokenBy\n timedOut: boolean\n userId?: string\n }\n): Promise<void> {\n const { instance, definition, stepInstance, branch, attempts, waitedMs, wokenBy, timedOut, userId } = params\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')\n const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n const outputData: Record<string, unknown> = {\n stepType: CONDITION_STEP_TYPE,\n conditionMet: !timedOut,\n attempts,\n waitedMs,\n wokenBy,\n }\n if (timedOut) outputData.timedOut = true\n\n if (!timedOut) {\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(branch ? { branchInstanceId: branch.id } : {}),\n eventType: 'CONDITION_MET',\n eventData: { stepId: stepInstance.stepId, attempts, waitedMs, wokenBy },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n\n if (branch) {\n const { resumeBranch } = await import('./parallel-handler')\n const resumed = await resumeBranch(em, {\n instanceId: instance.id,\n branchInstanceId: branch.id,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n exitStepInstanceId: stepInstance.id,\n exitOutput: outputData,\n })\n if (resumed) {\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })\n }\n return\n }\n\n await stepHandler.exitStep(em, stepInstance, outputData)\n\n const autoTransitions = ((definition.definition?.transitions ?? []) as Array<Record<string, unknown>>).filter(\n (transition) => transition.fromStepId === stepInstance.stepId && transition.trigger === 'auto'\n )\n\n if (autoTransitions.length === 0) {\n instance.status = 'RUNNING'\n instance.updatedAt = new Date()\n await em.flush()\n return\n }\n\n const transitionContext = { workflowContext: instance.context, userId }\n\n const validTransitions = await transitionHandler.findValidTransitions(\n em,\n instance,\n stepInstance.stepId,\n transitionContext\n )\n const firstValidTransition = validTransitions.find((candidate) => candidate.isValid)\n\n if (!firstValidTransition || !firstValidTransition.transition) {\n instance.status = 'RUNNING'\n instance.updatedAt = new Date()\n await em.flush()\n return\n }\n\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n stepInstance.stepId,\n firstValidTransition.transition.toStepId,\n { ...transitionContext, transitionId: firstValidTransition.transition.transitionId },\n )\n\n if (!transitionResult.success) {\n throw new ConditionWaitError(\n 'Transition failed after condition was met',\n 'TRANSITION_FAILED',\n { error: transitionResult.error }\n )\n }\n\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })\n}\n\n/** Timeout with `onTimeout: 'FAIL'` \u2014 fail the step and take the normal compensation path. */\nasync function failConditionWait(\n em: EntityManager,\n container: AwilixContainer,\n params: {\n instance: WorkflowInstance\n stepInstance: StepInstance\n branch: WorkflowBranchInstance | null\n attempts: number\n waitedMs: number\n userId?: string\n }\n): Promise<void> {\n const { instance, stepInstance, branch, attempts, waitedMs, userId } = params\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n const errorMessage = `WAIT_FOR_CONDITION timed out after ${waitedMs}ms without the condition becoming true`\n const now = new Date()\n\n stepInstance.status = 'FAILED'\n stepInstance.errorData = { error: errorMessage, attempts, waitedMs }\n stepInstance.exitedAt = now\n stepInstance.updatedAt = now\n if (branch) {\n branch.status = 'FAILED'\n branch.updatedAt = now\n }\n await em.flush()\n\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(branch ? { branchInstanceId: branch.id } : {}),\n eventType: 'STEP_FAILED',\n eventData: { stepId: stepInstance.stepId, error: errorMessage },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n await workflowExecutor.completeWorkflow(em, container, instance.id, 'FAILED', { error: errorMessage })\n}\n\nasync function resolveWaiter(\n em: EntityManager,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n stepInstance: StepInstance\n): Promise<ResolvedWaiter | null> {\n if (stepInstance.status !== 'ACTIVE') return null\n if (stepInstance.stepType !== CONDITION_STEP_TYPE) return null\n\n const branch = stepInstance.branchInstanceId\n ? await loadBranch(em, instance, stepInstance.branchInstanceId)\n : null\n\n if (stepInstance.branchInstanceId) {\n if (!branch || branch.status !== 'PAUSED') return null\n } else if (instance.status !== 'PAUSED') {\n return null\n }\n\n const stepDef = findStepDefinition(definition, stepInstance.stepId)\n if (!stepDef || stepDef.stepType !== CONDITION_STEP_TYPE) return null\n\n return { stepInstance, branch, stepDef, config: readWaitForConditionConfig(stepDef) }\n}\n\n/**\n * Backstop poll tick for one parked waiter. Mirrors `timerHandler.fireTimer`,\n * but is a no-op (never an error) when the event-driven path already resumed\n * the instance \u2014 a redelivered or superseded job must not fail the queue.\n */\nexport async function evaluateWaitCondition(\n em: EntityManager,\n container: AwilixContainer,\n options: EvaluateWaitConditionOptions\n): Promise<WaitConditionEvaluationResult> {\n const { instanceId, stepInstanceId, tenantId, organizationId, userId } = options\n\n const instance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n { id: instanceId, tenantId, organizationId },\n undefined,\n { tenantId, organizationId }\n )\n if (!instance) return { outcome: 'noop', reason: 'INSTANCE_NOT_FOUND' }\n\n const stepInstance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n StepInstance,\n { id: stepInstanceId, workflowInstanceId: instance.id, tenantId, organizationId },\n undefined,\n { tenantId, organizationId }\n )\n if (!stepInstance) return { outcome: 'noop', reason: 'STEP_INSTANCE_NOT_FOUND' }\n\n const definition = await loadDefinitionForInstance(em, instance)\n if (!definition) {\n throw new ConditionWaitError('Workflow definition not found', 'DEFINITION_NOT_FOUND', {\n definitionId: instance.definitionId,\n })\n }\n\n const waiter = await resolveWaiter(em, instance, definition, stepInstance)\n if (!waiter) return { outcome: 'noop', reason: 'NOT_WAITING_FOR_CONDITION' }\n\n const nowMs = Date.now()\n const waitedMs = waitedMsSince(stepInstance, nowMs)\n const attempts = Math.max(1, options.attempt)\n\n const met = await evaluateConditionExpression({\n condition: waiter.config.condition,\n data: conditionReadContext(instance, waiter.branch),\n instanceId: instance.id,\n userId,\n })\n\n if (met) {\n await completeConditionWait(em, container, {\n instance,\n definition,\n stepInstance,\n branch: waiter.branch,\n attempts,\n waitedMs,\n wokenBy: 'poll',\n timedOut: false,\n userId,\n })\n return { outcome: 'met' }\n }\n\n const deadlineMs = Date.parse(options.deadlineAt)\n const deadlinePassed = Number.isFinite(deadlineMs) ? nowMs >= deadlineMs : true\n const attemptCapReached = attempts >= resolveMaxConditionAttempts()\n\n if (deadlinePassed || attemptCapReached) {\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(waiter.branch ? { branchInstanceId: waiter.branch.id } : {}),\n eventType: 'CONDITION_TIMED_OUT',\n eventData: {\n stepId: stepInstance.stepId,\n attempts,\n waitedMs,\n onTimeout: waiter.config.onTimeout,\n attemptCapReached,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n if (waiter.config.onTimeout === 'CONTINUE') {\n await completeConditionWait(em, container, {\n instance,\n definition,\n stepInstance,\n branch: waiter.branch,\n attempts,\n waitedMs,\n wokenBy: 'poll',\n timedOut: true,\n userId,\n })\n } else {\n await failConditionWait(em, container, {\n instance,\n stepInstance,\n branch: waiter.branch,\n attempts,\n waitedMs,\n userId,\n })\n }\n return { outcome: 'timed_out' }\n }\n\n // Re-enqueue at min(pollInterval, remaining). `deadlineAt` is forwarded\n // verbatim, so a slow queue cannot extend the configured wait.\n const delayMs = Math.max(1, Math.min(waiter.config.pollIntervalMs, deadlineMs - nowMs))\n const { enqueueConditionCheckJob } = await import('./activity-executor')\n const jobId = await enqueueConditionCheckJob({\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n branchInstanceId: waiter.branch ? waiter.branch.id : undefined,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n userId,\n deadlineAt: options.deadlineAt,\n attempt: attempts + 1,\n delayMs,\n })\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(waiter.branch ? { branchInstanceId: waiter.branch.id } : {}),\n eventType: 'CONDITION_EVALUATED',\n eventData: {\n stepId: stepInstance.stepId,\n met: false,\n attempt: attempts,\n nextCheckAt: new Date(nowMs + delayMs).toISOString(),\n jobId,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return { outcome: 'pending' }\n}\n\n/**\n * Event-driven wake: re-evaluate every waiter parked in this instance and\n * resume the ones whose predicate now holds. A miss is a silent no-op \u2014 the\n * pending poll job still stands, and logging misses here would flood the\n * timeline with one row per unrelated context write.\n */\nexport async function wakeConditionWaiters(\n em: EntityManager,\n container: AwilixContainer,\n options: WakeConditionWaitersOptions\n): Promise<WakeConditionWaitersResult> {\n const { instanceId, tenantId, organizationId, userId } = options\n\n const instance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n { id: instanceId, tenantId, organizationId },\n undefined,\n { tenantId, organizationId }\n )\n if (!instance) return { woken: [] }\n if (instance.status !== 'RUNNING' && instance.status !== 'PAUSED' && instance.status !== 'FORKED') {\n return { woken: [] }\n }\n\n const stepInstances = await findWithDecryption(\n em as PostgreSqlEntityManager,\n StepInstance,\n {\n workflowInstanceId: instance.id,\n stepType: CONDITION_STEP_TYPE,\n status: 'ACTIVE',\n tenantId,\n organizationId,\n },\n undefined,\n { tenantId, organizationId }\n )\n if (stepInstances.length === 0) return { woken: [] }\n\n const definition = await loadDefinitionForInstance(em, instance)\n if (!definition) return { woken: [] }\n\n const woken: string[] = []\n\n for (const stepInstance of stepInstances) {\n const waiter = await resolveWaiter(em, instance, definition, stepInstance)\n if (!waiter) continue\n\n const met = await evaluateConditionExpression({\n condition: waiter.config.condition,\n data: conditionReadContext(instance, waiter.branch),\n instanceId: instance.id,\n userId,\n })\n if (!met) continue\n\n const nowMs = Date.now()\n await completeConditionWait(em, container, {\n instance,\n definition,\n stepInstance,\n branch: waiter.branch,\n attempts: 1,\n waitedMs: waitedMsSince(stepInstance, nowMs),\n wokenBy: 'context-write',\n timedOut: false,\n userId,\n })\n woken.push(stepInstance.stepId)\n }\n\n return { woken }\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["/**\n * Condition Handler Service\n *\n * Drives WAIT_FOR_CONDITION steps: a step that pauses a workflow (or a single\n * parallel branch) until a boolean predicate over the run context holds.\n *\n * Two paths resume a waiter:\n * - `wakeConditionWaiters` \u2014 the event-driven fast path, called by the scoped\n * context-write endpoint right after a patch is merged.\n * - `evaluateWaitCondition` \u2014 the durability backstop, dispatched by the\n * activity worker from a delayed `kind: 'condition'` queue job. It also\n * owns timeout enforcement, so a wait can never hang forever.\n *\n * The predicate reuses the business_rules ConditionExpression language and\n * `ruleEvaluator.evaluateConditions` \u2014 the same evaluator that backs transition\n * conditions \u2014 so there is no second expression dialect.\n */\n\nimport { EntityManager } from '@mikro-orm/core'\nimport type { EntityManager as PostgreSqlEntityManager } from '@mikro-orm/postgresql'\nimport type { AwilixContainer } from 'awilix'\nimport { findOneWithDecryption, findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport * as ruleEvaluator from '../../business_rules/lib/rule-evaluator'\nimport {\n WorkflowInstance,\n WorkflowDefinition,\n WorkflowBranchInstance,\n StepInstance,\n} from '../data/entities'\nimport { parseDuration } from './duration'\nimport { resolveCodeDefinitionForInstance } from './find-definition'\nimport { branchToken, rootToken, tokenReadContext } from './execution-token'\nimport type * as eventLoggerModule from './event-logger'\nimport type * as stepHandlerModule from './step-handler'\nimport type * as transitionHandlerModule from './transition-handler'\nimport type * as workflowExecutorModule from './workflow-executor'\n\nexport const CONDITION_STEP_TYPE = 'WAIT_FOR_CONDITION'\n\nexport const DEFAULT_CONDITION_POLL_INTERVAL_MS = 30_000\nexport const MIN_CONDITION_POLL_INTERVAL_MS = 5_000\nexport const MAX_CONDITION_POLL_INTERVAL_MS = 3_600_000\nexport const DEFAULT_MAX_CONDITION_ATTEMPTS = 1000\n\nexport class ConditionWaitError extends Error {\n constructor(\n message: string,\n public code: string,\n public details?: Record<string, unknown>\n ) {\n super(message)\n this.name = 'ConditionWaitError'\n }\n}\n\nexport type ConditionTimeoutBehaviour = 'FAIL' | 'CONTINUE'\n\nexport interface WaitForConditionConfig {\n condition: unknown\n timeout: string\n timeoutMs: number\n onTimeout: ConditionTimeoutBehaviour\n pollIntervalMs: number\n}\n\nexport type ConditionWokenBy = 'immediate' | 'poll' | 'context-write'\n\nexport interface EvaluateWaitConditionOptions {\n instanceId: string\n stepInstanceId: string\n branchInstanceId?: string | null\n deadlineAt: string\n attempt: number\n userId?: string\n tenantId: string\n organizationId: string\n}\n\nexport type WaitConditionOutcome = 'met' | 'pending' | 'timed_out' | 'noop'\n\nexport interface WaitConditionEvaluationResult {\n outcome: WaitConditionOutcome\n reason?: string\n}\n\nexport interface WakeConditionWaitersOptions {\n instanceId: string\n tenantId: string\n organizationId: string\n userId?: string\n}\n\nexport interface WakeConditionWaitersResult {\n woken: string[]\n}\n\n/**\n * Hard cap on poll re-enqueues, so a never-satisfiable predicate cannot\n * saturate the queue even if the deadline arithmetic is somehow skewed.\n */\nexport function resolveMaxConditionAttempts(): number {\n const raw = process.env.OM_WORKFLOWS_MAX_CONDITION_ATTEMPTS\n if (!raw) return DEFAULT_MAX_CONDITION_ATTEMPTS\n const parsed = Number.parseInt(raw, 10)\n if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_MAX_CONDITION_ATTEMPTS\n return parsed\n}\n\n/**\n * The attempt cap one waiter actually gets: the configured floor, raised to the\n * number of polls its own timeout needs (`timeoutMs / pollIntervalMs`, plus\n * the entry poll). A fixed 1000 would force a 90-day wait polled hourly to\n * time out after ~42 days. The deadline still ends the wait first; this only\n * bounds a loop whose deadline arithmetic went wrong.\n */\nexport function resolveConditionAttemptCap(\n config: Pick<WaitForConditionConfig, 'timeoutMs' | 'pollIntervalMs'>\n): number {\n const floor = resolveMaxConditionAttempts()\n if (!(config.timeoutMs > 0) || !(config.pollIntervalMs > 0)) return floor\n return Math.max(floor, Math.ceil(config.timeoutMs / config.pollIntervalMs) + 1)\n}\n\n/**\n * Read and normalise a WAIT_FOR_CONDITION step's config. Fails closed: the\n * predicate and the timeout are both mandatory, and the poll interval is\n * clamped into its supported range and never allowed past the deadline.\n */\nexport function readWaitForConditionConfig(stepDef: unknown): WaitForConditionConfig {\n const step = (stepDef ?? {}) as { stepId?: string; config?: Record<string, unknown> | null }\n const config = (step.config ?? {}) as Record<string, unknown>\n\n const condition = config.condition\n if (condition == null) {\n throw new ConditionWaitError(\n 'WAIT_FOR_CONDITION requires a \"condition\" expression',\n 'CONDITION_CONFIG_MISSING',\n { stepId: step.stepId }\n )\n }\n\n const timeout = config.timeout\n if (typeof timeout !== 'string' || timeout.length === 0) {\n throw new ConditionWaitError(\n 'WAIT_FOR_CONDITION requires a \"timeout\" ISO 8601 duration',\n 'CONDITION_TIMEOUT_MISSING',\n { stepId: step.stepId }\n )\n }\n\n let timeoutMs: number\n try {\n timeoutMs = parseDuration(timeout)\n } catch {\n throw new ConditionWaitError(\n `WAIT_FOR_CONDITION invalid \"timeout\" duration: ${timeout}`,\n 'CONDITION_TIMEOUT_INVALID',\n { stepId: step.stepId, timeout }\n )\n }\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {\n throw new ConditionWaitError(\n `WAIT_FOR_CONDITION \"timeout\" must be a positive duration: ${timeout}`,\n 'CONDITION_TIMEOUT_INVALID',\n { stepId: step.stepId, timeout }\n )\n }\n\n const onTimeout: ConditionTimeoutBehaviour = config.onTimeout === 'CONTINUE' ? 'CONTINUE' : 'FAIL'\n\n const rawPollInterval =\n typeof config.pollIntervalMs === 'number' && Number.isFinite(config.pollIntervalMs)\n ? config.pollIntervalMs\n : DEFAULT_CONDITION_POLL_INTERVAL_MS\n const clampedPollInterval = Math.min(\n Math.max(rawPollInterval, MIN_CONDITION_POLL_INTERVAL_MS),\n MAX_CONDITION_POLL_INTERVAL_MS\n )\n\n return {\n condition,\n timeout,\n timeoutMs,\n onTimeout,\n pollIntervalMs: Math.min(clampedPollInterval, timeoutMs),\n }\n}\n\n/**\n * Evaluate a wait predicate against a read context. Shared by the step handler\n * (entry evaluation) and both resume paths, so all three agree on semantics.\n */\nexport async function evaluateConditionExpression(params: {\n condition: unknown\n data: Record<string, unknown>\n instanceId: string\n userId?: string\n}): Promise<boolean> {\n const evaluationContext: ruleEvaluator.RuleEvaluationContext = {\n entityType: 'workflow:wait_condition',\n entityId: params.instanceId,\n user: params.userId ? { id: params.userId } : undefined,\n }\n return await ruleEvaluator.evaluateConditions(params.condition, params.data, evaluationContext)\n}\n\n/** Read context a waiter's predicate sees \u2014 instance context, branch overlay included. */\nexport function conditionReadContext(\n instance: WorkflowInstance,\n branch: WorkflowBranchInstance | null\n): Record<string, unknown> {\n const token = branch ? branchToken(instance, branch) : rootToken(instance)\n return tokenReadContext(token)\n}\n\nasync function loadDefinitionForInstance(\n em: EntityManager,\n instance: WorkflowInstance\n): Promise<WorkflowDefinition | null> {\n const stored = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowDefinition,\n {\n id: instance.definitionId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n deletedAt: null,\n },\n undefined,\n { tenantId: instance.tenantId, organizationId: instance.organizationId }\n )\n return stored ?? resolveCodeDefinitionForInstance(instance)\n}\n\nfunction findStepDefinition(definition: WorkflowDefinition, stepId: string): Record<string, unknown> | null {\n const steps = (definition.definition?.steps ?? []) as Array<Record<string, unknown>>\n return steps.find((step) => step.stepId === stepId) ?? null\n}\n\nfunction waitedMsSince(stepInstance: StepInstance, now: number): number {\n const enteredAt = stepInstance.enteredAt ? stepInstance.enteredAt.getTime() : now\n return Math.max(0, now - enteredAt)\n}\n\ninterface ResolvedWaiter {\n stepInstance: StepInstance\n branch: WorkflowBranchInstance | null\n stepDef: Record<string, unknown>\n config: WaitForConditionConfig\n}\n\nasync function loadBranch(\n em: EntityManager,\n instance: WorkflowInstance,\n branchInstanceId: string\n): Promise<WorkflowBranchInstance | null> {\n return await em.findOne(WorkflowBranchInstance, {\n id: branchInstanceId,\n workflowInstanceId: instance.id,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n}\n\n/**\n * Exit a satisfied (or CONTINUE-on-timeout) waiter and drive the workflow on.\n * Branch waiters take the `resumeBranch` path so sibling branches are untouched.\n */\nasync function completeConditionWait(\n em: EntityManager,\n container: AwilixContainer,\n params: {\n instance: WorkflowInstance\n definition: WorkflowDefinition\n stepInstance: StepInstance\n branch: WorkflowBranchInstance | null\n attempts: number\n waitedMs: number\n wokenBy: ConditionWokenBy\n timedOut: boolean\n userId?: string\n }\n): Promise<void> {\n const { instance, definition, stepInstance, branch, attempts, waitedMs, wokenBy, timedOut, userId } = params\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n const stepHandler = container.resolve<typeof stepHandlerModule>('stepHandler')\n const transitionHandler = container.resolve<typeof transitionHandlerModule>('transitionHandler')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n const outputData: Record<string, unknown> = {\n stepType: CONDITION_STEP_TYPE,\n conditionMet: !timedOut,\n attempts,\n waitedMs,\n wokenBy,\n }\n if (timedOut) outputData.timedOut = true\n\n if (!timedOut) {\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(branch ? { branchInstanceId: branch.id } : {}),\n eventType: 'CONDITION_MET',\n eventData: { stepId: stepInstance.stepId, attempts, waitedMs, wokenBy },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n }\n\n if (branch) {\n const { resumeBranch } = await import('./parallel-handler')\n const resumed = await resumeBranch(em, {\n instanceId: instance.id,\n branchInstanceId: branch.id,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n exitStepInstanceId: stepInstance.id,\n exitOutput: outputData,\n })\n if (resumed) {\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })\n }\n return\n }\n\n await stepHandler.exitStep(em, stepInstance, outputData)\n\n const autoTransitions = ((definition.definition?.transitions ?? []) as Array<Record<string, unknown>>).filter(\n (transition) => transition.fromStepId === stepInstance.stepId && transition.trigger === 'auto'\n )\n\n if (autoTransitions.length === 0) {\n instance.status = 'RUNNING'\n instance.updatedAt = new Date()\n await em.flush()\n return\n }\n\n const transitionContext = { workflowContext: instance.context, userId }\n\n const validTransitions = await transitionHandler.findValidTransitions(\n em,\n instance,\n stepInstance.stepId,\n transitionContext\n )\n const firstValidTransition = validTransitions.find((candidate) => candidate.isValid)\n\n if (!firstValidTransition || !firstValidTransition.transition) {\n instance.status = 'RUNNING'\n instance.updatedAt = new Date()\n await em.flush()\n return\n }\n\n const transitionResult = await transitionHandler.executeTransition(\n em,\n container,\n instance,\n stepInstance.stepId,\n firstValidTransition.transition.toStepId,\n { ...transitionContext, transitionId: firstValidTransition.transition.transitionId },\n )\n\n if (!transitionResult.success) {\n throw new ConditionWaitError(\n 'Transition failed after condition was met',\n 'TRANSITION_FAILED',\n { error: transitionResult.error }\n )\n }\n\n await workflowExecutor.executeWorkflow(em, container, instance.id, { userId })\n}\n\n/** Timeout with `onTimeout: 'FAIL'` \u2014 fail the step and take the normal compensation path. */\nasync function failConditionWait(\n em: EntityManager,\n container: AwilixContainer,\n params: {\n instance: WorkflowInstance\n stepInstance: StepInstance\n branch: WorkflowBranchInstance | null\n attempts: number\n waitedMs: number\n userId?: string\n }\n): Promise<void> {\n const { instance, stepInstance, branch, attempts, waitedMs, userId } = params\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n const workflowExecutor = container.resolve<typeof workflowExecutorModule>('workflowExecutor')\n\n const errorMessage = `WAIT_FOR_CONDITION timed out after ${waitedMs}ms without the condition becoming true`\n const now = new Date()\n\n stepInstance.status = 'FAILED'\n stepInstance.errorData = { error: errorMessage, attempts, waitedMs }\n stepInstance.exitedAt = now\n stepInstance.updatedAt = now\n if (branch) {\n branch.status = 'FAILED'\n branch.updatedAt = now\n }\n await em.flush()\n\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(branch ? { branchInstanceId: branch.id } : {}),\n eventType: 'STEP_FAILED',\n eventData: { stepId: stepInstance.stepId, error: errorMessage },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n await workflowExecutor.completeWorkflow(em, container, instance.id, 'FAILED', { error: errorMessage })\n}\n\nasync function resolveWaiter(\n em: EntityManager,\n instance: WorkflowInstance,\n definition: WorkflowDefinition,\n stepInstance: StepInstance\n): Promise<ResolvedWaiter | null> {\n if (stepInstance.status !== 'ACTIVE') return null\n if (stepInstance.stepType !== CONDITION_STEP_TYPE) return null\n\n const branch = stepInstance.branchInstanceId\n ? await loadBranch(em, instance, stepInstance.branchInstanceId)\n : null\n\n if (stepInstance.branchInstanceId) {\n if (!branch || branch.status !== 'PAUSED') return null\n } else if (instance.status !== 'PAUSED') {\n return null\n }\n\n const stepDef = findStepDefinition(definition, stepInstance.stepId)\n if (!stepDef || stepDef.stepType !== CONDITION_STEP_TYPE) return null\n\n return { stepInstance, branch, stepDef, config: readWaitForConditionConfig(stepDef) }\n}\n\n/**\n * Backstop poll tick for one parked waiter. Mirrors `timerHandler.fireTimer`,\n * but is a no-op (never an error) when the event-driven path already resumed\n * the instance \u2014 a redelivered or superseded job must not fail the queue.\n */\nexport async function evaluateWaitCondition(\n em: EntityManager,\n container: AwilixContainer,\n options: EvaluateWaitConditionOptions\n): Promise<WaitConditionEvaluationResult> {\n const { instanceId, stepInstanceId, tenantId, organizationId, userId } = options\n\n const instance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n { id: instanceId, tenantId, organizationId },\n undefined,\n { tenantId, organizationId }\n )\n if (!instance) return { outcome: 'noop', reason: 'INSTANCE_NOT_FOUND' }\n\n const stepInstance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n StepInstance,\n { id: stepInstanceId, workflowInstanceId: instance.id, tenantId, organizationId },\n undefined,\n { tenantId, organizationId }\n )\n if (!stepInstance) return { outcome: 'noop', reason: 'STEP_INSTANCE_NOT_FOUND' }\n\n const definition = await loadDefinitionForInstance(em, instance)\n if (!definition) {\n throw new ConditionWaitError('Workflow definition not found', 'DEFINITION_NOT_FOUND', {\n definitionId: instance.definitionId,\n })\n }\n\n const waiter = await resolveWaiter(em, instance, definition, stepInstance)\n if (!waiter) return { outcome: 'noop', reason: 'NOT_WAITING_FOR_CONDITION' }\n\n const nowMs = Date.now()\n const waitedMs = waitedMsSince(stepInstance, nowMs)\n const attempts = Math.max(1, options.attempt)\n\n const met = await evaluateConditionExpression({\n condition: waiter.config.condition,\n data: conditionReadContext(instance, waiter.branch),\n instanceId: instance.id,\n userId,\n })\n\n if (met) {\n await completeConditionWait(em, container, {\n instance,\n definition,\n stepInstance,\n branch: waiter.branch,\n attempts,\n waitedMs,\n wokenBy: 'poll',\n timedOut: false,\n userId,\n })\n return { outcome: 'met' }\n }\n\n const deadlineMs = Date.parse(options.deadlineAt)\n const deadlinePassed = Number.isFinite(deadlineMs) ? nowMs >= deadlineMs : true\n const attemptCapReached = attempts >= resolveConditionAttemptCap(waiter.config)\n\n if (deadlinePassed || attemptCapReached) {\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(waiter.branch ? { branchInstanceId: waiter.branch.id } : {}),\n eventType: 'CONDITION_TIMED_OUT',\n eventData: {\n stepId: stepInstance.stepId,\n attempts,\n waitedMs,\n onTimeout: waiter.config.onTimeout,\n attemptCapReached,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n if (waiter.config.onTimeout === 'CONTINUE') {\n await completeConditionWait(em, container, {\n instance,\n definition,\n stepInstance,\n branch: waiter.branch,\n attempts,\n waitedMs,\n wokenBy: 'poll',\n timedOut: true,\n userId,\n })\n } else {\n await failConditionWait(em, container, {\n instance,\n stepInstance,\n branch: waiter.branch,\n attempts,\n waitedMs,\n userId,\n })\n }\n return { outcome: 'timed_out' }\n }\n\n // Re-enqueue at min(pollInterval, remaining). `deadlineAt` is forwarded\n // verbatim, so a slow queue cannot extend the configured wait.\n const delayMs = Math.max(1, Math.min(waiter.config.pollIntervalMs, deadlineMs - nowMs))\n const { enqueueConditionCheckJob } = await import('./activity-executor')\n const jobId = await enqueueConditionCheckJob({\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n branchInstanceId: waiter.branch ? waiter.branch.id : undefined,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n userId,\n deadlineAt: options.deadlineAt,\n attempt: attempts + 1,\n delayMs,\n })\n\n const eventLogger = container.resolve<typeof eventLoggerModule>('eventLogger')\n await eventLogger.logWorkflowEvent(em, {\n workflowInstanceId: instance.id,\n stepInstanceId: stepInstance.id,\n ...(waiter.branch ? { branchInstanceId: waiter.branch.id } : {}),\n eventType: 'CONDITION_EVALUATED',\n eventData: {\n stepId: stepInstance.stepId,\n met: false,\n attempt: attempts,\n nextCheckAt: new Date(nowMs + delayMs).toISOString(),\n jobId,\n },\n userId,\n tenantId: instance.tenantId,\n organizationId: instance.organizationId,\n })\n\n return { outcome: 'pending' }\n}\n\n/**\n * Event-driven wake: re-evaluate every waiter parked in this instance and\n * resume the ones whose predicate now holds. A miss is a silent no-op \u2014 the\n * pending poll job still stands, and logging misses here would flood the\n * timeline with one row per unrelated context write.\n */\nexport async function wakeConditionWaiters(\n em: EntityManager,\n container: AwilixContainer,\n options: WakeConditionWaitersOptions\n): Promise<WakeConditionWaitersResult> {\n const { instanceId, tenantId, organizationId, userId } = options\n\n const instance = await findOneWithDecryption(\n em as PostgreSqlEntityManager,\n WorkflowInstance,\n { id: instanceId, tenantId, organizationId },\n undefined,\n { tenantId, organizationId }\n )\n if (!instance) return { woken: [] }\n if (instance.status !== 'RUNNING' && instance.status !== 'PAUSED' && instance.status !== 'FORKED') {\n return { woken: [] }\n }\n\n const stepInstances = await findWithDecryption(\n em as PostgreSqlEntityManager,\n StepInstance,\n {\n workflowInstanceId: instance.id,\n stepType: CONDITION_STEP_TYPE,\n status: 'ACTIVE',\n tenantId,\n organizationId,\n },\n undefined,\n { tenantId, organizationId }\n )\n if (stepInstances.length === 0) return { woken: [] }\n\n const definition = await loadDefinitionForInstance(em, instance)\n if (!definition) return { woken: [] }\n\n const woken: string[] = []\n\n for (const stepInstance of stepInstances) {\n const waiter = await resolveWaiter(em, instance, definition, stepInstance)\n if (!waiter) continue\n\n const met = await evaluateConditionExpression({\n condition: waiter.config.condition,\n data: conditionReadContext(instance, waiter.branch),\n instanceId: instance.id,\n userId,\n })\n if (!met) continue\n\n const nowMs = Date.now()\n await completeConditionWait(em, container, {\n instance,\n definition,\n stepInstance,\n branch: waiter.branch,\n attempts: 1,\n waitedMs: waitedMsSince(stepInstance, nowMs),\n wokenBy: 'context-write',\n timedOut: false,\n userId,\n })\n woken.push(stepInstance.stepId)\n }\n\n return { woken }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["eventLogger"]
|
|
7
7
|
}
|
|
@@ -57,6 +57,16 @@ function calculateDueDate(duration, from = /* @__PURE__ */ new Date()) {
|
|
|
57
57
|
if (parts) return addIso8601Duration(from, parts);
|
|
58
58
|
return new Date(from.getTime() + parseDuration(duration));
|
|
59
59
|
}
|
|
60
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
61
|
+
const MAX_INLINE_WAIT_MS = 6e4;
|
|
62
|
+
function assertInProcessWaitDelay(delayMs) {
|
|
63
|
+
if (delayMs > MAX_TIMER_DELAY_MS) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`WAIT activity: a ${delayMs}ms delay cannot be slept in-process (limit ${MAX_TIMER_DELAY_MS}ms); make it the last activity of its transition, mark it async, or use a WAIT_FOR_TIMER step`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return delayMs;
|
|
69
|
+
}
|
|
60
70
|
function calculateWaitDelayMs(config) {
|
|
61
71
|
if (config.until) {
|
|
62
72
|
const targetDate = new Date(config.until);
|
|
@@ -90,7 +100,10 @@ function toTimeoutMs(value) {
|
|
|
90
100
|
}
|
|
91
101
|
}
|
|
92
102
|
export {
|
|
103
|
+
MAX_INLINE_WAIT_MS,
|
|
104
|
+
MAX_TIMER_DELAY_MS,
|
|
93
105
|
addIso8601Duration,
|
|
106
|
+
assertInProcessWaitDelay,
|
|
94
107
|
calculateDueDate,
|
|
95
108
|
calculateWaitDelayMs,
|
|
96
109
|
parseDuration,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../src/modules/workflows/lib/duration.ts"],
|
|
4
|
-
"sourcesContent": ["export type Iso8601DurationParts = {\n years: number\n months: number\n weeks: number\n days: number\n hours: number\n minutes: number\n seconds: number\n}\n\n/**\n * Full ISO 8601 duration grammar: `P[nY][nM][nW][nD][T[nH][nM][nS]]`.\n *\n * The lookaheads reject the designator-only forms `P`, `PT` and `P1DT`, and the\n * anchors reject anything with trailing junk. `M` before `T` is months, `M`\n * after it is minutes \u2014 the distinction the hand-rolled parsers this replaces\n * could not make.\n */\nconst ISO_8601_DURATION_PATTERN =\n /^P(?=\\d|T\\d)(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)W)?(?:(\\d+)D)?(?:T(?=\\d)(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?$/\n\nconst SIMPLE_DURATION_PATTERN = /^(\\d+)(d|h|m|s)$/\n\nconst MS_PER_SECOND = 1000\nconst MS_PER_MINUTE = 60 * MS_PER_SECOND\nconst MS_PER_HOUR = 60 * MS_PER_MINUTE\nconst MS_PER_DAY = 24 * MS_PER_HOUR\n\n/**\n * Parse an ISO 8601 duration into its calendar components.\n *\n * Pure and total: returns `null` for anything that is not a valid ISO 8601\n * duration, so callers decide how to surface the problem.\n *\n * @param duration - Duration string (e.g. \"PT30M\", \"P1W\", \"P1DT12H\")\n * @returns Parsed components, or `null` when the string is not ISO 8601\n */\nexport function parseIso8601Duration(duration: string): Iso8601DurationParts | null {\n const match = duration.match(ISO_8601_DURATION_PATTERN)\n if (!match) return null\n\n return {\n years: parseInt(match[1] || '0', 10),\n months: parseInt(match[2] || '0', 10),\n weeks: parseInt(match[3] || '0', 10),\n days: parseInt(match[4] || '0', 10),\n hours: parseInt(match[5] || '0', 10),\n minutes: parseInt(match[6] || '0', 10),\n seconds: parseInt(match[7] || '0', 10),\n }\n}\n\n/**\n * Add an ISO 8601 duration to a date using calendar arithmetic.\n *\n * Years, months, weeks and days advance the calendar (so \"P1M\" lands on the\n * same day of the next month whatever its length), while hours, minutes and\n * seconds are added as elapsed time.\n *\n * @param from - Anchor date\n * @param parts - Parsed duration components\n * @returns A new date; `from` is never mutated\n */\nexport function addIso8601Duration(from: Date, parts: Iso8601DurationParts): Date {\n const result = new Date(from.getTime())\n\n if (parts.years) result.setFullYear(result.getFullYear() + parts.years)\n if (parts.months) result.setMonth(result.getMonth() + parts.months)\n\n const days = parts.weeks * 7 + parts.days\n if (days) result.setDate(result.getDate() + days)\n\n const elapsedMs =\n parts.hours * MS_PER_HOUR + parts.minutes * MS_PER_MINUTE + parts.seconds * MS_PER_SECOND\n\n return elapsedMs ? new Date(result.getTime() + elapsedMs) : result\n}\n\n/**\n * Parse a duration to milliseconds\n *\n * Supports:\n * - ISO 8601: PT30M (30 minutes), PT1H (1 hour), P1D (1 day), P1W (1 week), P1DT12H\n * - Simple formats: 5m, 1h, 3d, 30s\n *\n * Years and months are rejected here because they have no fixed millisecond\n * length \u2014 use `parseIso8601Duration` + `addIso8601Duration` when the duration\n * is anchored to a date.\n *\n * @param duration - Duration string\n * @returns Duration in milliseconds\n */\nexport function parseDuration(duration: string): number {\n const parts = parseIso8601Duration(duration)\n\n if (parts) {\n if (parts.years || parts.months) {\n throw new Error(\n `Duration ${duration} uses years or months, which have no fixed length in milliseconds`\n )\n }\n\n return (\n (parts.weeks * 7 + parts.days) * MS_PER_DAY +\n parts.hours * MS_PER_HOUR +\n parts.minutes * MS_PER_MINUTE +\n parts.seconds * MS_PER_SECOND\n )\n }\n\n const simpleMatch = duration.match(SIMPLE_DURATION_PATTERN)\n\n if (simpleMatch) {\n const value = parseInt(simpleMatch[1], 10)\n\n switch (simpleMatch[2]) {\n case 'd':\n return value * MS_PER_DAY\n case 'h':\n return value * MS_PER_HOUR\n case 'm':\n return value * MS_PER_MINUTE\n case 's':\n return value * MS_PER_SECOND\n }\n }\n\n throw new Error(`Invalid duration format: ${duration}`)\n}\n\n/**\n * Resolve a deadline from a duration anchored to a point in time.\n *\n * Accepts every format `parseDuration` accepts plus the calendar-only ISO 8601\n * components (years, months), which are applied as calendar arithmetic. Throws\n * on an unparseable duration \u2014 a deadline the author got wrong must surface,\n * never quietly become some default.\n *\n * @param duration - Duration string (e.g. \"PT30M\", \"P3D\", \"P1DT12H\")\n * @param from - Anchor date, defaults to now\n * @returns The resolved deadline\n */\nexport function calculateDueDate(duration: string, from: Date = new Date()): Date {\n const parts = parseIso8601Duration(duration)\n if (parts) return addIso8601Duration(from, parts)\n\n return new Date(from.getTime() + parseDuration(duration))\n}\n\nexport function calculateWaitDelayMs(config: { duration?: string; until?: string }): number {\n if (config.until) {\n const targetDate = new Date(config.until)\n if (isNaN(targetDate.getTime())) {\n throw new Error(`WAIT activity: invalid \"until\" datetime: ${config.until}`)\n }\n const delayMs = targetDate.getTime() - Date.now()\n return Math.max(0, delayMs)\n }\n\n if (config.duration) {\n return parseDuration(config.duration)\n }\n\n throw new Error('WAIT activity requires \"duration\" (e.g., \"PT5M\", \"1h\") or \"until\" (ISO 8601 datetime)')\n}\n\n/**\n * Normalize a timeout value to milliseconds\n *\n * Accepts every shape the activity editors and stored definitions carry:\n * - a millisecond number (30000)\n * - a millisecond string (\"30000\") \u2014 what the CrudForm activity editor writes,\n * and what its own placeholder (\"PT30S or 30000\") tells the user to type\n * - a duration string (\"PT30S\", \"5m\")\n *\n * Returns undefined for an absent or unusable value rather than throwing, so a\n * malformed timeout never fails an activity that would otherwise succeed.\n *\n * @param value - Raw timeout value\n * @returns Milliseconds, or undefined when the value is absent or unusable\n */\nexport function toTimeoutMs(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isFinite(value) && value > 0 ? value : undefined\n }\n if (typeof value !== 'string') return undefined\n\n const trimmed = value.trim()\n if (!trimmed) return undefined\n\n if (/^\\d+$/.test(trimmed)) {\n const milliseconds = Number(trimmed)\n return milliseconds > 0 ? milliseconds : undefined\n }\n\n try {\n const milliseconds = parseDuration(trimmed)\n return Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : undefined\n } catch {\n return undefined\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["export type Iso8601DurationParts = {\n years: number\n months: number\n weeks: number\n days: number\n hours: number\n minutes: number\n seconds: number\n}\n\n/**\n * Full ISO 8601 duration grammar: `P[nY][nM][nW][nD][T[nH][nM][nS]]`.\n *\n * The lookaheads reject the designator-only forms `P`, `PT` and `P1DT`, and the\n * anchors reject anything with trailing junk. `M` before `T` is months, `M`\n * after it is minutes \u2014 the distinction the hand-rolled parsers this replaces\n * could not make.\n */\nconst ISO_8601_DURATION_PATTERN =\n /^P(?=\\d|T\\d)(?:(\\d+)Y)?(?:(\\d+)M)?(?:(\\d+)W)?(?:(\\d+)D)?(?:T(?=\\d)(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?$/\n\nconst SIMPLE_DURATION_PATTERN = /^(\\d+)(d|h|m|s)$/\n\nconst MS_PER_SECOND = 1000\nconst MS_PER_MINUTE = 60 * MS_PER_SECOND\nconst MS_PER_HOUR = 60 * MS_PER_MINUTE\nconst MS_PER_DAY = 24 * MS_PER_HOUR\n\n/**\n * Parse an ISO 8601 duration into its calendar components.\n *\n * Pure and total: returns `null` for anything that is not a valid ISO 8601\n * duration, so callers decide how to surface the problem.\n *\n * @param duration - Duration string (e.g. \"PT30M\", \"P1W\", \"P1DT12H\")\n * @returns Parsed components, or `null` when the string is not ISO 8601\n */\nexport function parseIso8601Duration(duration: string): Iso8601DurationParts | null {\n const match = duration.match(ISO_8601_DURATION_PATTERN)\n if (!match) return null\n\n return {\n years: parseInt(match[1] || '0', 10),\n months: parseInt(match[2] || '0', 10),\n weeks: parseInt(match[3] || '0', 10),\n days: parseInt(match[4] || '0', 10),\n hours: parseInt(match[5] || '0', 10),\n minutes: parseInt(match[6] || '0', 10),\n seconds: parseInt(match[7] || '0', 10),\n }\n}\n\n/**\n * Add an ISO 8601 duration to a date using calendar arithmetic.\n *\n * Years, months, weeks and days advance the calendar (so \"P1M\" lands on the\n * same day of the next month whatever its length), while hours, minutes and\n * seconds are added as elapsed time.\n *\n * @param from - Anchor date\n * @param parts - Parsed duration components\n * @returns A new date; `from` is never mutated\n */\nexport function addIso8601Duration(from: Date, parts: Iso8601DurationParts): Date {\n const result = new Date(from.getTime())\n\n if (parts.years) result.setFullYear(result.getFullYear() + parts.years)\n if (parts.months) result.setMonth(result.getMonth() + parts.months)\n\n const days = parts.weeks * 7 + parts.days\n if (days) result.setDate(result.getDate() + days)\n\n const elapsedMs =\n parts.hours * MS_PER_HOUR + parts.minutes * MS_PER_MINUTE + parts.seconds * MS_PER_SECOND\n\n return elapsedMs ? new Date(result.getTime() + elapsedMs) : result\n}\n\n/**\n * Parse a duration to milliseconds\n *\n * Supports:\n * - ISO 8601: PT30M (30 minutes), PT1H (1 hour), P1D (1 day), P1W (1 week), P1DT12H\n * - Simple formats: 5m, 1h, 3d, 30s\n *\n * Years and months are rejected here because they have no fixed millisecond\n * length \u2014 use `parseIso8601Duration` + `addIso8601Duration` when the duration\n * is anchored to a date.\n *\n * @param duration - Duration string\n * @returns Duration in milliseconds\n */\nexport function parseDuration(duration: string): number {\n const parts = parseIso8601Duration(duration)\n\n if (parts) {\n if (parts.years || parts.months) {\n throw new Error(\n `Duration ${duration} uses years or months, which have no fixed length in milliseconds`\n )\n }\n\n return (\n (parts.weeks * 7 + parts.days) * MS_PER_DAY +\n parts.hours * MS_PER_HOUR +\n parts.minutes * MS_PER_MINUTE +\n parts.seconds * MS_PER_SECOND\n )\n }\n\n const simpleMatch = duration.match(SIMPLE_DURATION_PATTERN)\n\n if (simpleMatch) {\n const value = parseInt(simpleMatch[1], 10)\n\n switch (simpleMatch[2]) {\n case 'd':\n return value * MS_PER_DAY\n case 'h':\n return value * MS_PER_HOUR\n case 'm':\n return value * MS_PER_MINUTE\n case 's':\n return value * MS_PER_SECOND\n }\n }\n\n throw new Error(`Invalid duration format: ${duration}`)\n}\n\n/**\n * Resolve a deadline from a duration anchored to a point in time.\n *\n * Accepts every format `parseDuration` accepts plus the calendar-only ISO 8601\n * components (years, months), which are applied as calendar arithmetic. Throws\n * on an unparseable duration \u2014 a deadline the author got wrong must surface,\n * never quietly become some default.\n *\n * @param duration - Duration string (e.g. \"PT30M\", \"P3D\", \"P1DT12H\")\n * @param from - Anchor date, defaults to now\n * @returns The resolved deadline\n */\nexport function calculateDueDate(duration: string, from: Date = new Date()): Date {\n const parts = parseIso8601Duration(duration)\n if (parts) return addIso8601Duration(from, parts)\n\n return new Date(from.getTime() + parseDuration(duration))\n}\n\n/**\n * Largest delay Node's `setTimeout` honours (a signed 32-bit millisecond count,\n * ~24.86 days). A longer delay is not rejected \u2014 it fires after 1 ms with only a\n * `TimeoutOverflowWarning` \u2014 so an in-process wait must refuse it explicitly.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647\n\n/**\n * A WAIT longer than this is not slept inside the executing request: when it is\n * the last activity of its transition it is handed to the activity queue (the\n * path an `async: true` WAIT already takes). One minute keeps the short pauses\n * people use for pacing inline while anything that would hold a request or a\n * worker slot open for minutes becomes durable.\n */\nexport const MAX_INLINE_WAIT_MS = 60_000\n\nexport function assertInProcessWaitDelay(delayMs: number): number {\n if (delayMs > MAX_TIMER_DELAY_MS) {\n throw new Error(\n `WAIT activity: a ${delayMs}ms delay cannot be slept in-process (limit ${MAX_TIMER_DELAY_MS}ms); make it the last activity of its transition, mark it async, or use a WAIT_FOR_TIMER step`\n )\n }\n return delayMs\n}\n\nexport function calculateWaitDelayMs(config: { duration?: string; until?: string }): number {\n if (config.until) {\n const targetDate = new Date(config.until)\n if (isNaN(targetDate.getTime())) {\n throw new Error(`WAIT activity: invalid \"until\" datetime: ${config.until}`)\n }\n const delayMs = targetDate.getTime() - Date.now()\n return Math.max(0, delayMs)\n }\n\n if (config.duration) {\n return parseDuration(config.duration)\n }\n\n throw new Error('WAIT activity requires \"duration\" (e.g., \"PT5M\", \"1h\") or \"until\" (ISO 8601 datetime)')\n}\n\n/**\n * Normalize a timeout value to milliseconds\n *\n * Accepts every shape the activity editors and stored definitions carry:\n * - a millisecond number (30000)\n * - a millisecond string (\"30000\") \u2014 what the CrudForm activity editor writes,\n * and what its own placeholder (\"PT30S or 30000\") tells the user to type\n * - a duration string (\"PT30S\", \"5m\")\n *\n * Returns undefined for an absent or unusable value rather than throwing, so a\n * malformed timeout never fails an activity that would otherwise succeed.\n *\n * @param value - Raw timeout value\n * @returns Milliseconds, or undefined when the value is absent or unusable\n */\nexport function toTimeoutMs(value: unknown): number | undefined {\n if (typeof value === 'number') {\n return Number.isFinite(value) && value > 0 ? value : undefined\n }\n if (typeof value !== 'string') return undefined\n\n const trimmed = value.trim()\n if (!trimmed) return undefined\n\n if (/^\\d+$/.test(trimmed)) {\n const milliseconds = Number(trimmed)\n return milliseconds > 0 ? milliseconds : undefined\n }\n\n try {\n const milliseconds = parseDuration(trimmed)\n return Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : undefined\n } catch {\n return undefined\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const RUN_STATUSES_CLOSED_TO_USER_TASKS = [
|
|
2
|
+
"COMPLETED",
|
|
3
|
+
"FAILED",
|
|
4
|
+
"CANCELLED",
|
|
5
|
+
"COMPENSATING",
|
|
6
|
+
"COMPENSATED"
|
|
7
|
+
];
|
|
8
|
+
function isRunClosedToUserTasks(status) {
|
|
9
|
+
if (!status) return false;
|
|
10
|
+
return RUN_STATUSES_CLOSED_TO_USER_TASKS.includes(status);
|
|
11
|
+
}
|
|
12
|
+
export {
|
|
13
|
+
RUN_STATUSES_CLOSED_TO_USER_TASKS,
|
|
14
|
+
isRunClosedToUserTasks
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=instance-status.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/modules/workflows/lib/instance-status.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Workflow instance statuses a user-task completion or a user-task SLA job must\n * not touch.\n *\n * PURE: a type-only import, so the engine does not depend on the metrics\n * module's `WORKFLOW_TERMINAL_STATUSES` for a runtime decision.\n *\n * The name means \"a completion must not touch this run\", NOT \"its tasks are\n * closed\": a FAILED run keeps its open task so a retry can pick it up.\n * `COMPENSATING` is included although it is not terminal \u2014 `compensateWorkflow`\n * holds it while rollback activities run and then overwrites it with\n * `COMPENSATED` or `FAILED`, and a completion in that window would advance a\n * run that is being rolled back.\n */\n\nimport type { WorkflowInstanceStatus } from '../data/entities'\n\nexport const RUN_STATUSES_CLOSED_TO_USER_TASKS: readonly WorkflowInstanceStatus[] = [\n 'COMPLETED',\n 'FAILED',\n 'CANCELLED',\n 'COMPENSATING',\n 'COMPENSATED',\n]\n\nexport function isRunClosedToUserTasks(status: string | null | undefined): boolean {\n if (!status) return false\n return (RUN_STATUSES_CLOSED_TO_USER_TASKS as readonly string[]).includes(status)\n}\n"],
|
|
5
|
+
"mappings": "AAiBO,MAAM,oCAAuE;AAAA,EAClF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,uBAAuB,QAA4C;AACjF,MAAI,CAAC,OAAQ,QAAO;AACpB,SAAQ,kCAAwD,SAAS,MAAM;AACjF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -122,6 +122,11 @@ async function emitStepMilestone(em, stepInstance) {
|
|
|
122
122
|
} catch {
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
|
+
const MAX_PERSISTED_EXECUTION_TIME_MS = 2147483647;
|
|
126
|
+
function toPersistableExecutionTimeMs(executionTimeMs) {
|
|
127
|
+
if (executionTimeMs === null || !Number.isFinite(executionTimeMs)) return null;
|
|
128
|
+
return Math.abs(executionTimeMs) > MAX_PERSISTED_EXECUTION_TIME_MS ? null : executionTimeMs;
|
|
129
|
+
}
|
|
125
130
|
async function exitStep(em, stepInstance, outputData) {
|
|
126
131
|
const now = /* @__PURE__ */ new Date();
|
|
127
132
|
let executionTimeMs = null;
|
|
@@ -131,7 +136,7 @@ async function exitStep(em, stepInstance, outputData) {
|
|
|
131
136
|
stepInstance.status = "COMPLETED";
|
|
132
137
|
stepInstance.outputData = outputData || null;
|
|
133
138
|
stepInstance.exitedAt = now;
|
|
134
|
-
stepInstance.executionTimeMs = executionTimeMs;
|
|
139
|
+
stepInstance.executionTimeMs = toPersistableExecutionTimeMs(executionTimeMs);
|
|
135
140
|
stepInstance.updatedAt = now;
|
|
136
141
|
await em.flush();
|
|
137
142
|
await logStepEvent(em, {
|
|
@@ -1148,11 +1153,13 @@ function mapSubWorkflowOutput(childContext, outputMapping, ioContract) {
|
|
|
1148
1153
|
return { outputData: mapOutputData(childOutput, outputMapping || {}) };
|
|
1149
1154
|
}
|
|
1150
1155
|
export {
|
|
1156
|
+
MAX_PERSISTED_EXECUTION_TIME_MS,
|
|
1151
1157
|
StepExecutionError,
|
|
1152
1158
|
enterStep,
|
|
1153
1159
|
executeStep,
|
|
1154
1160
|
exitStep,
|
|
1155
1161
|
mapSubWorkflowOutput,
|
|
1156
|
-
markStepInstanceFailed
|
|
1162
|
+
markStepInstanceFailed,
|
|
1163
|
+
toPersistableExecutionTimeMs
|
|
1157
1164
|
};
|
|
1158
1165
|
//# sourceMappingURL=step-handler.js.map
|