@gesslar/actioneer 2.1.0 → 2.3.1

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.
@@ -4,16 +4,14 @@ import {Data, Sass, Promised, Time, Util, Valid} from "@gesslar/toolkit"
4
4
  * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
5
5
  */
6
6
 
7
- /**
8
- * @typedef {object} ActionHooksConfig
9
- * @property {string} actionKind Action identifier shared between runner and hooks.
10
- * @property {unknown} hooks Already-instantiated hooks implementation.
11
- * @property {number} [hookTimeout] Timeout applied to hook execution in milliseconds.
12
- * @property {DebugFn} debug Logger to emit diagnostics.
13
- */
14
-
15
7
  /**
16
8
  * @typedef {Record<string, (context: unknown) => Promise<unknown>|unknown>} HookModule
9
+ *
10
+ * @typedef {object} ActionHooksConfig
11
+ * @property {string} actionKind - Action identifier shared between runner and hooks.
12
+ * @property {unknown} hooks - Already-instantiated hooks implementation.
13
+ * @property {number} [hookTimeout] - Timeout applied to hook execution in milliseconds.
14
+ * @property {DebugFn} debug - Logger to emit diagnostics.
17
15
  */
18
16
 
19
17
  /**
@@ -24,19 +22,19 @@ import {Data, Sass, Promised, Time, Util, Valid} from "@gesslar/toolkit"
24
22
  * Browser version: Requires pre-instantiated hooks. File-based loading is not supported.
25
23
  */
26
24
  export default class ActionHooks {
27
- /** @type {HookModule|null} */
25
+ /** @type {HookModule?} */
28
26
  #hooks = null
29
- /** @type {string|null} */
27
+ /** @type {string?} */
30
28
  #actionKind = null
31
29
  /** @type {number} */
32
30
  #timeout = 1_000 // Default 1 second timeout
33
- /** @type {DebugFn|null} */
31
+ /** @type {DebugFn?} */
34
32
  #debug = null
35
33
 
36
34
  /**
37
35
  * Creates a new ActionHook instance.
38
36
  *
39
- * @param {ActionHooksConfig} config Configuration values describing how to load the hooks.
37
+ * @param {ActionHooksConfig} config - Configuration values describing how to load the hooks.
40
38
  */
41
39
  constructor({actionKind, hooks, hookTimeout = 1_000, debug}) {
42
40
  this.#actionKind = actionKind
@@ -57,7 +55,7 @@ export default class ActionHooks {
57
55
  /**
58
56
  * Gets the loaded hooks object.
59
57
  *
60
- * @returns {object|null} Hooks object or null if not loaded
58
+ * @returns {object?} Hooks object or null if not loaded
61
59
  */
62
60
  get hooks() {
63
61
  return this.#hooks
@@ -75,7 +73,7 @@ export default class ActionHooks {
75
73
  /**
76
74
  * Gets the setup hook function if available.
77
75
  *
78
- * @returns {(args: object) => unknown|null} Setup hook function or null
76
+ * @returns {(args: object) => unknown} Setup hook function or null
79
77
  */
80
78
  get setup() {
81
79
  return this.hooks?.setup || null
@@ -84,7 +82,7 @@ export default class ActionHooks {
84
82
  /**
85
83
  * Gets the cleanup hook function if available.
86
84
  *
87
- * @returns {(args: object) => unknown|null} Cleanup hook function or null
85
+ * @returns {(args: object) => unknown} Cleanup hook function or null
88
86
  */
89
87
  get cleanup() {
90
88
  return this.hooks?.cleanup || null
@@ -94,9 +92,9 @@ export default class ActionHooks {
94
92
  * Static factory method to create and initialize a hook manager.
95
93
  * Browser version: Only works with pre-instantiated hooks passed via config.hooks.
96
94
  *
97
- * @param {ActionHooksConfig} config Configuration object with hooks property
98
- * @param {DebugFn} debug The debug function.
99
- * @returns {Promise<ActionHooks|null>} Initialized hook manager or null if no hooks provided
95
+ * @param {ActionHooksConfig} config - Configuration object with hooks property
96
+ * @param {DebugFn} debug - The debug function.
97
+ * @returns {Promise<ActionHooks?>} Initialized hook manager or null if no hooks provided
100
98
  */
101
99
  static async new(config, debug) {
102
100
  debug("Creating new HookManager instance with args: %o", 2, config)
@@ -117,9 +115,9 @@ export default class ActionHooks {
117
115
  /**
118
116
  * Invoke a dynamically-named hook such as `before$foo`.
119
117
  *
120
- * @param {'before'|'after'|'setup'|'cleanup'|string} kind Hook namespace.
121
- * @param {string|symbol} activityName Activity identifier.
122
- * @param {unknown} context Pipeline context supplied to the hook.
118
+ * @param {string} kind - Hook namespace.
119
+ * @param {string|symbol} activityName - Activity identifier.
120
+ * @param {unknown} context - Pipeline context supplied to the hook.
123
121
  * @returns {Promise<void>}
124
122
  */
125
123
  async callHook(kind, activityName, context) {
@@ -1,17 +1,17 @@
1
- import {Promised, Data, Sass, Valid} from "@gesslar/toolkit"
1
+ import {Promised, Data, Sass, Tantrum, Valid} from "@gesslar/toolkit"
2
2
 
3
3
  import {ACTIVITY} from "./Activity.js"
4
4
  import Piper from "./Piper.js"
5
5
 
6
6
  /**
7
- * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
7
+ * Types
8
+ *
9
+ * @import {default as ActionBuilder} from "./ActionBuilder.js"
10
+ * @import {default as ActionWrapper} from "./ActionWrapper.js"
8
11
  */
9
-
10
- /**
11
- * @typedef {import("./ActionBuilder.js").default} ActionBuilder
12
- */
13
-
14
12
  /**
13
+ * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
14
+ *
15
15
  * @typedef {object} ActionRunnerOptions
16
16
  * @property {DebugFn} [debug] Logger function.
17
17
  */
@@ -24,9 +24,9 @@ import Piper from "./Piper.js"
24
24
  * context object under `result.value` that can be replaced or enriched.
25
25
  */
26
26
  export default class ActionRunner extends Piper {
27
- /** @type {import("./ActionBuilder.js").default|null} */
27
+ /** @type {ActionBuilder?} */
28
28
  #actionBuilder = null
29
- /** @type {import("./ActionWrapper.js").default|null} */
29
+ /** @type {ActionWrapper?} */
30
30
  #actionWrapper = null
31
31
 
32
32
  /**
@@ -63,24 +63,32 @@ export default class ActionRunner extends Piper {
63
63
  /**
64
64
  * Executes the configured action pipeline.
65
65
  * Builds the ActionWrapper on first run and caches it for subsequent calls.
66
- * Supports WHILE, UNTIL, and SPLIT activity kinds.
66
+ * Supports WHILE, UNTIL, IF, SPLIT, BREAK, and CONTINUE activity kinds.
67
67
  *
68
68
  * @param {unknown} context - Seed value passed to the first activity.
69
+ * @param {import("./ActionWrapper.js").default|null} [parentWrapper] - Parent wrapper for BREAK/CONTINUE signaling.
69
70
  * @returns {Promise<unknown>} Final value produced by the pipeline.
70
71
  * @throws {Sass} When no activities are registered, conflicting activity kinds are used, or execution fails.
72
+ * @throws {Tantrum} When both an activity and the done callback fail.
71
73
  */
72
- async run(context) {
74
+ async run(context, parentWrapper=null) {
73
75
  if(!this.#actionWrapper)
74
- this.#actionWrapper = await this.#actionBuilder.build()
76
+ this.#actionWrapper = await this.#actionBuilder.build(this)
75
77
 
76
78
  const actionWrapper = this.#actionWrapper
77
- const activities = actionWrapper.activities
79
+ const activities = Array.from(actionWrapper.activities)
80
+
81
+ let caughtError = null
78
82
 
79
83
  try {
80
- for(const activity of activities) {
81
- try {
82
- // await timeout(500)
84
+ for(
85
+ let cursor = 0, max = activities.length;
86
+ cursor < max && cursor !== -1;
87
+ cursor++
88
+ ) {
89
+ const activity = activities[cursor]
83
90
 
91
+ try {
84
92
  const kind = activity.kind
85
93
 
86
94
  // If we have no kind, then it's just a once.
@@ -88,33 +96,50 @@ export default class ActionRunner extends Piper {
88
96
  if(!kind) {
89
97
  context = await this.#execute(activity, context)
90
98
  } else {
91
- // Validate that only one activity kind bit is set
92
- // (kind & (kind - 1)) !== 0 means multiple bits are set
93
- const multipleBitsSet = (kind & (kind - 1)) !== 0
94
- if(multipleBitsSet)
95
- throw Sass.new(
96
- "For Kathy Griffin's sake! You can't combine activity kinds. " +
97
- "Pick one: WHILE, UNTIL, or SPLIT!"
98
- )
99
-
100
- const {WHILE,UNTIL,SPLIT} = ACTIVITY
101
- const kindWhile = kind & WHILE
102
- const kindUntil = kind & UNTIL
103
- const kindSplit = kind & SPLIT
104
-
105
- if(kindWhile || kindUntil) {
106
- const predicate = activity.pred
107
-
99
+ const {UNTIL, WHILE, IF, SPLIT, BREAK, CONTINUE} = ACTIVITY
100
+ const kindUntil = kind === UNTIL
101
+ const kindWhile = kind === WHILE
102
+ const kindIf = kind === IF
103
+ const kindSplit = kind === SPLIT
104
+ const kindBreak = kind === BREAK
105
+ const kindContinue = kind === CONTINUE
106
+
107
+ if(kindBreak || kindContinue) {
108
+ if(!parentWrapper)
109
+ throw Sass.new(`Invalid use of control flow outside of context.`)
110
+
111
+ if(await this.#evalPredicate(activity, context)) {
112
+ if(kindBreak) {
113
+ this.emit("loop.break", parentWrapper)
114
+ break
115
+ }
116
+
117
+ if(kindContinue)
118
+ cursor = max
119
+ }
120
+ } else if(kindIf) {
121
+ if(await this.#evalPredicate(activity, context))
122
+ context = await this.#execute(activity, context)
123
+ } else if(kindWhile || kindUntil) {
124
+ // Simple if, no loop, only gainz.
108
125
  for(;;) {
109
-
110
126
  if(kindWhile)
111
- if(!await this.#hasPredicate(activity,predicate,context))
127
+ if(!await this.#evalPredicate(activity, context))
112
128
  break
113
129
 
114
- context = await this.#execute(activity,context)
130
+ let weWereOnABreak = false
131
+ const breakReceiver = this.on("loop.break", wrapper => {
132
+ if(wrapper.id === actionWrapper.id) {
133
+ weWereOnABreak = true
134
+ }
135
+ })
136
+ context = await this.#execute(activity, context)
137
+ breakReceiver()
138
+ if(weWereOnABreak)
139
+ break
115
140
 
116
141
  if(kindUntil)
117
- if(await this.#hasPredicate(activity,predicate,context))
142
+ if(await this.#evalPredicate(activity, context))
118
143
  break
119
144
  }
120
145
  } else if(kindSplit) {
@@ -129,9 +154,8 @@ export default class ActionRunner extends Piper {
129
154
  )
130
155
 
131
156
  const original = context
132
- const splitContexts = await splitter.call(
133
- activity.action, context
134
- )
157
+ const splitContexts = await splitter.call(activity.action,context)
158
+
135
159
  let settled
136
160
 
137
161
  if(activity.opKind === "ActionBuilder") {
@@ -145,7 +169,6 @@ export default class ActionRunner extends Piper {
145
169
  debug: this.#debug, name: activity.name
146
170
  })
147
171
 
148
- // pipe() returns settled results with concurrency control
149
172
  settled = await runner.pipe(splitContexts)
150
173
  } else {
151
174
  // For plain functions, process each split context
@@ -169,19 +192,28 @@ export default class ActionRunner extends Piper {
169
192
  throw Sass.new("ActionRunner running activity", error)
170
193
  }
171
194
  }
172
- } finally {
173
- // Execute done callback if registered - always runs, even on error
174
- if(actionWrapper.done) {
175
- try {
176
- context = await actionWrapper.done.call(
177
- actionWrapper.action, context
178
- )
179
- } catch(error) {
180
- throw Sass.new("ActionRunner running done callback", error)
181
- }
195
+ } catch(err) {
196
+ caughtError = err
197
+ }
198
+
199
+ // Execute done callback if registered - always runs, even on error
200
+ // Only run for top-level pipelines, not nested builders (inside loops)
201
+ if(actionWrapper.done && !parentWrapper) {
202
+ try {
203
+ context = await actionWrapper.done.call(
204
+ actionWrapper.action, caughtError ?? context
205
+ )
206
+ } catch(error) {
207
+ if(caughtError)
208
+ caughtError = new Tantrum("ActionRunner running done callback", [caughtError, error])
209
+ else
210
+ caughtError = Sass.new("ActionRunner running done callback", error)
182
211
  }
183
212
  }
184
213
 
214
+ if(caughtError)
215
+ throw caughtError
216
+
185
217
  return context
186
218
  }
187
219
 
@@ -217,14 +249,24 @@ export default class ActionRunner extends Piper {
217
249
  debug: this.#debug, name: activity.name
218
250
  })
219
251
 
220
- if(parallel) {
221
- return await runner.pipe(context)
222
- } else {
223
- return await runner.run(context)
252
+ // Forward loop.break events from nested runner to this runner
253
+ // so that parent WHILE/UNTIL loops can receive break signals.
254
+ const forwarder = runner.on("loop.break",
255
+ wrapper => this.emit("loop.break", wrapper)
256
+ )
257
+
258
+ try {
259
+ if(parallel) {
260
+ return await runner.pipe(context)
261
+ } else {
262
+ return await runner.run(context, activity.wrapper)
263
+ }
264
+ } finally {
265
+ forwarder()
224
266
  }
225
267
  } else if(opKind === "Function") {
226
268
  try {
227
- const result = await activity.run(context)
269
+ const result = await activity.run(context, activity.wrapper)
228
270
 
229
271
  if(Data.isType(result, "ActionBuilder")) {
230
272
  if(activity.action)
@@ -240,7 +282,7 @@ export default class ActionRunner extends Piper {
240
282
  if(parallel) {
241
283
  return await runner.pipe(context)
242
284
  } else {
243
- return await runner.run(context)
285
+ return await runner.run(context, activity.wrapper)
244
286
  }
245
287
  } else {
246
288
  return result
@@ -250,24 +292,21 @@ export default class ActionRunner extends Piper {
250
292
  }
251
293
  }
252
294
 
253
- console.log(activity.opKind + " " + JSON.stringify(activity))
254
-
255
295
  throw Sass.new("We buy Functions and ActionBuilders. Only. Not whatever that was.")
256
296
  }
257
297
 
258
298
  /**
259
- * Evaluate the predicate for WHILE/UNTIL activity kinds.
299
+ * Evaluate the predicate for WHILE/UNTIL/IF/BREAK/CONTINUE activity kinds.
260
300
  *
261
301
  * @param {import("./Activity.js").default} activity Activity currently executing.
262
- * @param {(context: unknown) => boolean|Promise<boolean>} predicate Predicate to evaluate.
263
302
  * @param {unknown} context Current pipeline context.
264
- * @returns {Promise<boolean>} True when the predicate allows another iteration.
303
+ * @returns {Promise<boolean>} True when the predicate condition is met.
265
304
  * @private
266
305
  */
267
- async #hasPredicate(activity,predicate,context) {
268
- Valid.type(predicate, "Function")
306
+ async #evalPredicate(activity, context) {
307
+ Valid.type(activity?.pred, "Function")
269
308
 
270
- return !!(await predicate.call(activity.action, context))
309
+ return !!(await activity.pred.call(activity.action, context))
271
310
  }
272
311
 
273
312
  toString() {
@@ -1,5 +1,12 @@
1
1
  import Activity from "./Activity.js"
2
2
 
3
+ /**
4
+ * Type imports
5
+ *
6
+ * @import {default as ActionHooks} from "./ActionHooks.js"
7
+ * @import {default as ActionRunner} from "./ActionRunner.js"
8
+ */
9
+
3
10
  /**
4
11
  * @typedef {object} WrappedActivityConfig
5
12
  * @property {string|symbol} name Activity identifier used by hooks/logs.
@@ -10,10 +17,6 @@ import Activity from "./Activity.js"
10
17
  * @property {(message: string, level?: number, ...args: Array<unknown>) => void} [debug] Optional logger reference.
11
18
  */
12
19
 
13
- /**
14
- * @typedef {import("@gesslar/toolkit").Generator<Activity, void, unknown>} ActivityIterator
15
- */
16
-
17
20
  /**
18
21
  * Thin wrapper that materialises {@link Activity} instances on demand.
19
22
  */
@@ -24,6 +27,7 @@ export default class ActionWrapper {
24
27
  * @type {Map<string|symbol, WrappedActivityConfig>}
25
28
  */
26
29
  #activities = new Map()
30
+
27
31
  /**
28
32
  * Logger invoked for wrapper lifecycle events.
29
33
  *
@@ -31,11 +35,16 @@ export default class ActionWrapper {
31
35
  */
32
36
  #debug = () => {}
33
37
 
38
+ /** @type {ActionHooks} */
34
39
  #hooks = null
35
-
40
+ /** @type {((context: unknown) => unknown|Promise<unknown>)|null} */
36
41
  #done = null
37
-
42
+ /** @type {unknown} */
38
43
  #action = null
44
+ /** @type {symbol} */
45
+ #id = Symbol(performance.now())
46
+ /** @type {ActionRunner} */
47
+ #runner
39
48
 
40
49
  /**
41
50
  * Create a wrapper from the builder payload.
@@ -47,7 +56,18 @@ export default class ActionWrapper {
47
56
  this.#hooks = hooks
48
57
  this.#done = doneCallback
49
58
  this.#action = action
50
- this.#activities = activities
59
+
60
+ for(const [key, value] of activities) {
61
+ this.#activities.set(
62
+ key,
63
+ new Activity({
64
+ ...value,
65
+ hooks: this.#hooks,
66
+ wrapper: this
67
+ })
68
+ )
69
+ }
70
+
51
71
  this.#debug(
52
72
  "Instantiating ActionWrapper with %o activities.",
53
73
  2,
@@ -55,18 +75,23 @@ export default class ActionWrapper {
55
75
  )
56
76
  }
57
77
 
58
- *#_activities() {
59
- for(const [,activity] of this.#activities)
60
- yield new Activity({...activity, hooks: this.#hooks})
78
+ /**
79
+ * Unique identifier for this wrapper instance.
80
+ * Used by BREAK/CONTINUE to match events to the correct loop.
81
+ *
82
+ * @returns {symbol} Unique symbol identifier
83
+ */
84
+ get id() {
85
+ return this.#id
61
86
  }
62
87
 
63
88
  /**
64
89
  * Iterator over the registered activities.
65
90
  *
66
- * @returns {ActivityIterator} Lazy iterator yielding Activity instances.
91
+ * @returns {Iterator<Activity>} Iterator yielding Activity instances.
67
92
  */
68
93
  get activities() {
69
- return this.#_activities()
94
+ return this.#activities.values()
70
95
  }
71
96
 
72
97
  /**
@@ -1,6 +1,10 @@
1
1
  import {Data} from "@gesslar/toolkit"
2
2
 
3
- /** @typedef {import("./ActionHooks.js").default} ActionHooks */
3
+ /**
4
+ * @import {default as ActionBuilder} from "./ActionBuilder.js"
5
+ * @import {default as ActionHooks} from "./ActionHooks.js"
6
+ * @import {default as ActionWrapper} from "./ActionWrapper.js"
7
+ **/
4
8
 
5
9
  /**
6
10
  * Activity bit flags recognised by the builder. The flag decides
@@ -8,14 +12,20 @@ import {Data} from "@gesslar/toolkit"
8
12
  *
9
13
  * @readonly
10
14
  * @enum {number}
11
- * @property {number} WHILE - Execute activity while predicate returns true (2)
12
- * @property {number} UNTIL - Execute activity until predicate returns true (4)
13
- * @property {number} SPLIT - Execute activity with split/rejoin pattern for parallel execution (8)
15
+ * @property {number} WHILE - Execute activity while predicate returns true 1
16
+ * @property {number} UNTIL - Execute activity until predicate returns true 2
17
+ * @property {number} SPLIT - Execute activity with split/rejoin pattern for parallel execution 3
18
+ * @property {number} IF - Execute activity if predicate returns true 4
19
+ * @property {number} BREAK - Break out of a WHILE/UNTIL if predicate returns true 5
20
+ * @property {number} CONTINUE - Returns to the top of a WHILE/UNTIL if predicate returns true 6
14
21
  */
15
22
  export const ACTIVITY = Object.freeze({
16
- WHILE: 1<<1,
17
- UNTIL: 1<<2,
18
- SPLIT: 1<<3,
23
+ WHILE: 1,
24
+ UNTIL: 2,
25
+ SPLIT: 3,
26
+ IF: 4,
27
+ BREAK: 5,
28
+ CONTINUE: 6,
19
29
  })
20
30
 
21
31
  export default class Activity {
@@ -23,13 +33,13 @@ export default class Activity {
23
33
  #action = null
24
34
  /** @type {unknown} */
25
35
  #context = null
26
- /** @type {ActionHooks|null} */
36
+ /** @type {ActionHooks?} */
27
37
  #hooks = null
28
- /** @type {number|null} */
38
+ /** @type {number?} */
29
39
  #kind = null
30
40
  /** @type {string|symbol} */
31
41
  #name = null
32
- /** @type {((context: unknown) => unknown|Promise<unknown>)|import("./ActionBuilder.js").default} */
42
+ /** @type {((context: unknown) => unknown|Promise<unknown>)|ActionBuilder} */
33
43
  #op = null
34
44
  /** @type {((context: unknown) => boolean|Promise<boolean>)|null} */
35
45
  #pred = null
@@ -37,6 +47,10 @@ export default class Activity {
37
47
  #rejoiner = null
38
48
  /** @type {((context: unknown) => unknown)|null} */
39
49
  #splitter = null
50
+ /** @type {ActionWrapper?} */
51
+ #wrapper = null
52
+ /** @type {symbol} */
53
+ #id = Symbol(performance.now())
40
54
 
41
55
  /**
42
56
  * Construct an Activity definition wrapper.
@@ -44,14 +58,15 @@ export default class Activity {
44
58
  * @param {object} init - Initial properties describing the activity operation, loop semantics, and predicate
45
59
  * @param {unknown} init.action - Parent action instance
46
60
  * @param {string|symbol} init.name - Activity identifier
47
- * @param {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} init.op - Operation to execute
61
+ * @param {(context: unknown) => unknown|Promise<unknown>|ActionBuilder} init.op - Operation to execute
48
62
  * @param {number} [init.kind] - Optional loop semantics flags
49
63
  * @param {(context: unknown) => boolean|Promise<boolean>} [init.pred] - Optional predicate for WHILE/UNTIL
50
64
  * @param {ActionHooks} [init.hooks] - Optional hooks instance
51
65
  * @param {(context: unknown) => unknown} [init.splitter] - Optional splitter function for SPLIT activities
52
66
  * @param {(originalContext: unknown, splitResults: unknown) => unknown} [init.rejoiner] - Optional rejoiner function for SPLIT activities
67
+ * @param {ActionWrapper} [init.wrapper] - Optional wrapper containing this activity
53
68
  */
54
- constructor({action,name,op,kind,pred,hooks,splitter,rejoiner}) {
69
+ constructor({action,name,op,kind,pred,hooks,splitter,rejoiner,wrapper}) {
55
70
  this.#action = action
56
71
  this.#hooks = hooks
57
72
  this.#kind = kind
@@ -60,6 +75,16 @@ export default class Activity {
60
75
  this.#pred = pred
61
76
  this.#rejoiner = rejoiner
62
77
  this.#splitter = splitter
78
+ this.#wrapper = wrapper ?? null
79
+ }
80
+
81
+ /**
82
+ * Unique identifier for this activity instance.
83
+ *
84
+ * @returns {symbol} Unique symbol identifier
85
+ */
86
+ get id() {
87
+ return this.#id
63
88
  }
64
89
 
65
90
  /**
@@ -81,7 +106,7 @@ export default class Activity {
81
106
  }
82
107
 
83
108
  /**
84
- * The predicate function for WHILE/UNTIL flows.
109
+ * The predicate function for WHILE/UNTIL/IF flows.
85
110
  *
86
111
  * @returns {(context: unknown) => boolean|Promise<boolean>|undefined} - Predicate used to continue/stop loops
87
112
  */
@@ -110,7 +135,7 @@ export default class Activity {
110
135
  /**
111
136
  * The operator to execute (function or nested ActionBuilder).
112
137
  *
113
- * @returns {(context: unknown) => unknown|Promise<unknown>|import("./ActionBuilder.js").default} - Activity operation
138
+ * @returns {(context: unknown) => unknown|Promise<unknown>|ActionBuilder} - Activity operation
114
139
  */
115
140
  get op() {
116
141
  return this.#op
@@ -119,7 +144,7 @@ export default class Activity {
119
144
  /**
120
145
  * The splitter function for SPLIT activities.
121
146
  *
122
- * @returns {((context: unknown) => unknown)|null} Splitter function or null
147
+ * @returns {((context: unknown) => unknown)?} Splitter function or null
123
148
  */
124
149
  get splitter() {
125
150
  return this.#splitter
@@ -128,7 +153,7 @@ export default class Activity {
128
153
  /**
129
154
  * The rejoiner function for SPLIT activities.
130
155
  *
131
- * @returns {((originalContext: unknown, splitResults: unknown) => unknown)|null} Rejoiner function or null
156
+ * @returns {((originalContext: unknown, splitResults: unknown) => unknown)?} Rejoiner function or null
132
157
  */
133
158
  get rejoiner() {
134
159
  return this.#rejoiner
@@ -143,6 +168,16 @@ export default class Activity {
143
168
  return this.#action
144
169
  }
145
170
 
171
+ /**
172
+ * Get the ActionWrapper containing this activity.
173
+ * Used by BREAK/CONTINUE to signal the parent loop.
174
+ *
175
+ * @returns {ActionWrapper?} The wrapper or null
176
+ */
177
+ get wrapper() {
178
+ return this.#wrapper ?? null
179
+ }
180
+
146
181
  /**
147
182
  * Execute the activity with before/after hooks.
148
183
  *
@@ -178,7 +213,7 @@ export default class Activity {
178
213
  /**
179
214
  * Get the hooks instance attached to this activity.
180
215
  *
181
- * @returns {ActionHooks|null} The hooks instance or null
216
+ * @returns {ActionHooks?} The hooks instance or null
182
217
  */
183
218
  get hooks() {
184
219
  return this.#hooks