@open-mercato/core 0.6.8-develop.6908.1.4792c7717e → 0.6.8-develop.6910.1.560e304de2

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.
@@ -26,6 +26,7 @@ import { callWebhookConfigSchema } from '../data/validators'
26
26
  import { WorkflowActivityJob, WORKFLOW_ACTIVITIES_QUEUE_NAME } from './activity-queue-types'
27
27
  import { logWorkflowEvent } from './event-logger'
28
28
  import { parseDuration } from './duration'
29
+ import { resolveActivityTimeoutMs } from './activityTimeoutFields'
29
30
  import { getWorkflowSafeCommand } from './workflow-safe-commands'
30
31
 
31
32
  export { isPrivateUrl } from '@open-mercato/shared/lib/network'
@@ -114,33 +115,7 @@ export interface ActivityDefinition {
114
115
  compensate?: boolean // Flag to execute compensation on failure
115
116
  }
116
117
 
117
- /**
118
- * Effective timeout for an activity, in milliseconds.
119
- *
120
- * The editor and this executor both speak `timeoutMs`, but the definition
121
- * schema historically accepted only an ISO 8601 `timeout` string — so stored
122
- * definitions can carry either. Prefer `timeoutMs`; fall back to parsing
123
- * `timeout`, ignoring a malformed value rather than throwing mid-execution
124
- * (an unparseable timeout must not fail an activity that would otherwise
125
- * succeed). Returns undefined when no usable timeout is configured (#4424).
126
- */
127
- export function resolveActivityTimeoutMs(activity: {
128
- timeoutMs?: number
129
- timeout?: string
130
- }): number | undefined {
131
- if (typeof activity.timeoutMs === 'number' && activity.timeoutMs > 0) {
132
- return activity.timeoutMs
133
- }
134
- if (typeof activity.timeout === 'string' && activity.timeout.trim().length > 0) {
135
- try {
136
- const parsed = parseDuration(activity.timeout.trim())
137
- if (Number.isFinite(parsed) && parsed > 0) return parsed
138
- } catch {
139
- return undefined
140
- }
141
- }
142
- return undefined
143
- }
118
+ export { resolveActivityTimeoutMs }
144
119
 
145
120
  export interface RetryPolicy {
146
121
  maxAttempts: number
@@ -1494,6 +1469,11 @@ function sleep(ms: number): Promise<void> {
1494
1469
 
1495
1470
  /**
1496
1471
  * Execute a promise with timeout
1472
+ *
1473
+ * Only CALL_API and CALL_WEBHOOK honour the abort signal today — they forward
1474
+ * it to `fetch`. SEND_EMAIL, EMIT_EVENT, UPDATE_ENTITY and EXECUTE_FUNCTION
1475
+ * still run to completion after the timeout has been recorded. Tracked in
1476
+ * #5148.
1497
1477
  */
1498
1478
  async function executeWithTimeout<T>(
1499
1479
  executor: (signal: AbortSignal) => Promise<T>,
@@ -0,0 +1,75 @@
1
+ import { toTimeoutMs } from './duration'
2
+
3
+ /**
4
+ * The two timeout fields an activity definition can carry.
5
+ *
6
+ * `timeoutMs` is canonical and is what `resolveActivityTimeoutMs` prefers;
7
+ * `timeout` is the deprecated alias that stored definitions and the CrudForm
8
+ * activity editor still write. A timeout input must own both fields — read
9
+ * them merged and write both on every edit — otherwise the box shows the
10
+ * executor's effective timeout while changing only half of the pair, and the
11
+ * user's edit is silently discarded.
12
+ */
13
+ export type ActivityTimeoutFields = {
14
+ timeout?: string
15
+ timeoutMs?: number
16
+ }
17
+
18
+ /**
19
+ * Effective timeout for an activity, in milliseconds.
20
+ *
21
+ * The editors and the executor both speak `timeoutMs`, but the definition
22
+ * schema historically accepted only a `timeout` string — so stored definitions
23
+ * can carry either. Prefer `timeoutMs`; fall back to `toTimeoutMs`, which
24
+ * reads both a duration string ("PT30S", "5m") and a plain millisecond string
25
+ * ("30000") — the CrudForm activity editor writes the latter, and its own
26
+ * placeholder tells the user to. A malformed value is ignored rather than
27
+ * thrown mid-execution (an unparseable timeout must not fail an activity that
28
+ * would otherwise succeed). Returns undefined when no usable timeout is
29
+ * configured (#4424).
30
+ *
31
+ * `activity-executor` re-exports this so its import path stays stable; it
32
+ * lives here so the editors can assert the round-trip without pulling in the
33
+ * server-only executor.
34
+ */
35
+ export function resolveActivityTimeoutMs(activity: ActivityTimeoutFields): number | undefined {
36
+ if (typeof activity.timeoutMs === 'number' && activity.timeoutMs > 0) {
37
+ return activity.timeoutMs
38
+ }
39
+ return toTimeoutMs(activity.timeout)
40
+ }
41
+
42
+ /**
43
+ * Text shown by a timeout input that accepts duration strings as well as
44
+ * milliseconds (the CrudForm activity editor, whose placeholder reads
45
+ * "PT30S or 30000").
46
+ */
47
+ export function durationTimeoutInputValue(activity: ActivityTimeoutFields): string {
48
+ if (activity.timeout) return activity.timeout
49
+ return activity.timeoutMs != null ? String(activity.timeoutMs) : ''
50
+ }
51
+
52
+ /** Value shown by a millisecond-only timeout input (the two visual editors). */
53
+ export function millisecondTimeoutInputValue(activity: ActivityTimeoutFields): number | '' {
54
+ return activity.timeoutMs ?? toTimeoutMs(activity.timeout) ?? ''
55
+ }
56
+
57
+ /**
58
+ * Fields to merge into an activity after a duration-accepting input changed.
59
+ *
60
+ * The raw text stays in the deprecated alias so a partially typed duration
61
+ * survives the re-render, and `timeoutMs` carries whatever the executor can
62
+ * actually use — undefined while the text is incomplete or unusable.
63
+ */
64
+ export function durationTimeoutPatch(raw: string): ActivityTimeoutFields {
65
+ return { timeout: raw || undefined, timeoutMs: toTimeoutMs(raw) }
66
+ }
67
+
68
+ /**
69
+ * Fields to merge into an activity after a millisecond-only input changed.
70
+ * The deprecated alias is dropped, so clearing the box clears the timeout and
71
+ * stored definitions migrate off `timeout` as they are edited.
72
+ */
73
+ export function millisecondTimeoutPatch(raw: string): ActivityTimeoutFields {
74
+ return { timeout: undefined, timeoutMs: toTimeoutMs(raw) }
75
+ }
@@ -49,3 +49,40 @@ export function parseDuration(duration: string): number {
49
49
 
50
50
  throw new Error(`Invalid duration format: ${duration}`)
51
51
  }
52
+
53
+ /**
54
+ * Normalize a timeout value to milliseconds
55
+ *
56
+ * Accepts every shape the activity editors and stored definitions carry:
57
+ * - a millisecond number (30000)
58
+ * - a millisecond string ("30000") — what the CrudForm activity editor writes,
59
+ * and what its own placeholder ("PT30S or 30000") tells the user to type
60
+ * - a duration string ("PT30S", "5m")
61
+ *
62
+ * Returns undefined for an absent or unusable value rather than throwing, so a
63
+ * malformed timeout never fails an activity that would otherwise succeed.
64
+ *
65
+ * @param value - Raw timeout value
66
+ * @returns Milliseconds, or undefined when the value is absent or unusable
67
+ */
68
+ export function toTimeoutMs(value: unknown): number | undefined {
69
+ if (typeof value === 'number') {
70
+ return Number.isFinite(value) && value > 0 ? value : undefined
71
+ }
72
+ if (typeof value !== 'string') return undefined
73
+
74
+ const trimmed = value.trim()
75
+ if (!trimmed) return undefined
76
+
77
+ if (/^\d+$/.test(trimmed)) {
78
+ const milliseconds = Number(trimmed)
79
+ return milliseconds > 0 ? milliseconds : undefined
80
+ }
81
+
82
+ try {
83
+ const milliseconds = parseDuration(trimmed)
84
+ return Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : undefined
85
+ } catch {
86
+ return undefined
87
+ }
88
+ }