@gesslar/actioneer 1.6.0 → 2.0.2

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/README.md CHANGED
@@ -1,18 +1,97 @@
1
1
  # Actioneer
2
2
 
3
- Actioneer is a small, focused Node.js action orchestration library. It provides a fluent builder for composing activities and a concurrent runner with lifecycle hooks and simple loop semantics (while/until). The project is written as ES modules and targets Node 20+.
3
+ Actioneer is a small, focused action orchestration library for Node.js and browser environments. It provides a fluent builder for composing activities and a concurrent runner with lifecycle hooks and simple loop semantics (while/until). The project is written as ES modules and targets Node 20+ and modern browsers.
4
4
 
5
5
  This repository extracts the action orchestration pieces from a larger codebase and exposes a compact API for building pipelines of work that can run concurrently with hook support and nested pipelines.
6
6
 
7
- ## Install
7
+ ## Included Classes
8
8
 
9
- From npm:
9
+ ### Browser
10
+
11
+ These classes work in browsers, Node.js, and browser-like environments such as Tauri, Electron, and Deno.
12
+
13
+ | Name | Description |
14
+ | ---- | ----------- |
15
+ | ActionBuilder | Fluent builder for composing activities into pipelines |
16
+ | ActionHooks | Lifecycle hook management (requires pre-instantiated hooks in browser) |
17
+ | ActionRunner | Concurrent pipeline executor with configurable concurrency |
18
+ | ActionWrapper | Activity container and iterator |
19
+ | Activity | Activity definitions with WHILE, UNTIL, and SPLIT modes |
20
+ | Piper | Base concurrent processing with worker pools |
21
+
22
+ ### Node.js
23
+
24
+ Includes all browser functionality plus Node.js-specific features for file-based hook loading.
25
+
26
+ | Name | Description |
27
+ | ---- | ----------- |
28
+ | ActionHooks | Enhanced version with file-based hook loading via `withHooksFile()` |
29
+
30
+ ## Installation
10
31
 
11
32
  ```bash
12
33
  npm install @gesslar/actioneer
13
34
  ```
14
35
 
15
- ## Quick start
36
+ ## Usage
37
+
38
+ Actioneer is environment-aware and automatically detects whether it is being used in a browser or Node.js. You can optionally specify the `node` or `browser` variant explicitly.
39
+
40
+ ### Browser
41
+
42
+ #### jsDelivr (runtime only)
43
+
44
+ ```html
45
+ https://cdn.jsdelivr.net/npm/@gesslar/actioneer
46
+ ```
47
+
48
+ #### esm.sh (runtime with types)
49
+
50
+ ```html
51
+ https://esm.sh/@gesslar/actioneer
52
+ https://esm.sh/@gesslar/actioneer?dts (serves .d.ts for editors)
53
+ ```
54
+
55
+ #### Browser Import Example
56
+
57
+ ```javascript
58
+ import {ActionBuilder, ActionRunner} from "https://esm.sh/@gesslar/actioneer"
59
+
60
+ class MyAction {
61
+ setup(builder) {
62
+ builder
63
+ .do("step1", ctx => { ctx.result = ctx.input * 2 })
64
+ .do("step2", ctx => { return ctx.result })
65
+ }
66
+ }
67
+
68
+ const builder = new ActionBuilder(new MyAction())
69
+ const runner = new ActionRunner(builder)
70
+ const results = await runner.run({input: 5})
71
+ console.log(results) // 10
72
+ ```
73
+
74
+ ### Node.js
75
+
76
+ #### Auto-detected (recommended)
77
+
78
+ ```javascript
79
+ import {ActionBuilder, ActionRunner} from "@gesslar/actioneer"
80
+ ```
81
+
82
+ #### Explicit variants
83
+
84
+ ```javascript
85
+ // Explicitly use Node.js version (with file-based hooks)
86
+ import {ActionBuilder, ActionRunner, ActionHooks} from "@gesslar/actioneer/node"
87
+
88
+ // Explicitly use browser version
89
+ import {ActionBuilder, ActionRunner} from "@gesslar/actioneer/browser"
90
+ ```
91
+
92
+ **Note:** The browser version is fully functional in Node.js but lacks file-based hook loading. Use `withHooks()` with pre-instantiated hooks instead of `withHooksFile()`.
93
+
94
+ ## Quick Start
16
95
 
17
96
  Import the builder and runner, define an action and run it:
18
97
 
@@ -299,7 +378,7 @@ This design ensures error handling responsibility stays at the call site - you d
299
378
 
300
379
  ## ActionHooks
301
380
 
302
- Actioneer supports lifecycle hooks that can execute before and after each activity in your pipeline. Hooks are loaded from a module and can be configured either by file path or by providing a pre-instantiated hooks object.
381
+ Actioneer supports lifecycle hooks that can execute before and after each activity in your pipeline. Hooks can be configured by file path (Node.js only) or by providing a pre-instantiated hooks object (Node.js and browser).
303
382
 
304
383
  ### Hook System Overview
305
384
 
@@ -312,12 +391,49 @@ The hook system allows you to:
312
391
 
313
392
  ### Configuring Hooks
314
393
 
315
- You can attach hooks to an ActionBuilder in two ways:
394
+ #### Browser: Pre-instantiated Hooks
316
395
 
317
- #### 1. Load hooks from a file
396
+ In browser environments, you must provide pre-instantiated hooks objects:
318
397
 
319
398
  ```js
320
- import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
399
+ import {ActionBuilder, ActionRunner} from "@gesslar/actioneer"
400
+
401
+ class MyActionHooks {
402
+ constructor({debug}) {
403
+ this.debug = debug
404
+ }
405
+
406
+ async before$prepare(context) {
407
+ this.debug("About to prepare", context)
408
+ }
409
+
410
+ async after$prepare(context) {
411
+ this.debug("Finished preparing", context)
412
+ }
413
+ }
414
+
415
+ const hooks = new MyActionHooks({debug: console.log})
416
+
417
+ class MyAction {
418
+ setup(builder) {
419
+ builder
420
+ .withHooks(hooks)
421
+ .do("prepare", ctx => { ctx.count = 0 })
422
+ .do("work", ctx => { ctx.count += 1 })
423
+ }
424
+ }
425
+
426
+ const builder = new ActionBuilder(new MyAction())
427
+ const runner = new ActionRunner(builder)
428
+ const result = await runner.pipe([{}], 4)
429
+ ```
430
+
431
+ #### Node.js: File-based or Pre-instantiated
432
+
433
+ **Option 1: Load hooks from a file** (Node.js only)
434
+
435
+ ```js
436
+ import {ActionBuilder, ActionRunner} from "@gesslar/actioneer"
321
437
 
322
438
  class MyAction {
323
439
  setup(builder) {
@@ -333,13 +449,13 @@ const runner = new ActionRunner(builder)
333
449
  const result = await runner.pipe([{}], 4)
334
450
  ```
335
451
 
336
- #### 2. Provide a pre-instantiated hooks object
452
+ **Option 2: Provide a pre-instantiated hooks object** (Node.js and browser)
337
453
 
338
454
  ```js
339
- import { ActionBuilder, ActionRunner } from "@gesslar/actioneer"
340
- import { MyActionHooks } from "./hooks/MyActionHooks.js"
455
+ import {ActionBuilder, ActionRunner} from "@gesslar/actioneer"
456
+ import {MyActionHooks} from "./hooks/MyActionHooks.js"
341
457
 
342
- const hooks = new MyActionHooks({ debug: console.log })
458
+ const hooks = new MyActionHooks({debug: console.log})
343
459
 
344
460
  class MyAction {
345
461
  setup(builder) {
package/package.json CHANGED
@@ -3,10 +3,9 @@
3
3
  "description": "The best action pipeline thingie this side of Tatooine, or your money back.",
4
4
  "author": {
5
5
  "name": "gesslar",
6
- "email": "bmw@gesslar.dev",
7
6
  "url": "https://gesslar.dev"
8
7
  },
9
- "version": "1.6.0",
8
+ "version": "2.0.2",
10
9
  "license": "Unlicense",
11
10
  "homepage": "https://github.com/gesslar/toolkit#readme",
12
11
  "repository": {
@@ -27,6 +26,16 @@
27
26
  "type": "module",
28
27
  "exports": {
29
28
  ".": {
29
+ "types": "./src/types/index.d.ts",
30
+ "browser": "./src/browser/index.js",
31
+ "node": "./src/index.js",
32
+ "default": "./src/index.js"
33
+ },
34
+ "./browser": {
35
+ "types": "./src/types/index.d.ts",
36
+ "default": "./src/browser/index.js"
37
+ },
38
+ "./node": {
30
39
  "types": "./src/types/index.d.ts",
31
40
  "default": "./src/index.js"
32
41
  }
@@ -40,7 +49,7 @@
40
49
  "node": ">=22"
41
50
  },
42
51
  "dependencies": {
43
- "@gesslar/toolkit": "^3.6.0"
52
+ "@gesslar/toolkit": "^3.6.2"
44
53
  },
45
54
  "devDependencies": {
46
55
  "@gesslar/uglier": "^0.5.0",
@@ -50,11 +59,12 @@
50
59
  "scripts": {
51
60
  "lint": "eslint src/",
52
61
  "lint:fix": "eslint src/ --fix",
53
- "types:build": "tsc -p tsconfig.types.json && eslint --fix \"src/types/**/*.d.ts\"",
62
+ "types:build": "tsc -p tsconfig.types.json",
54
63
  "submit": "pnpm publish --access public --//registry.npmjs.org/:_authToken=\"${NPM_ACCESS_TOKEN}\"",
55
64
  "update": "pnpm up --latest --recursive",
56
- "test": "node --test tests/unit/*.test.js",
57
- "test:unit": "node --test tests/unit/*.test.js",
65
+ "test": "node --test tests/**/*.test.js",
66
+ "test:browser": "node --test tests/browser/*.test.js",
67
+ "test:node": "node --test tests/node/*.test.js",
58
68
  "pr": "gt submit -p --ai",
59
69
  "patch": "pnpm version patch",
60
70
  "minor": "pnpm version minor",
@@ -0,0 +1,9 @@
1
+ // Browser-compatible exports
2
+ // Pure JavaScript modules that work in browsers and Node.js
3
+
4
+ export {default as ActionBuilder} from "./lib/ActionBuilder.js"
5
+ export {default as ActionHooks} from "./lib/ActionHooks.js"
6
+ export {default as ActionRunner} from "./lib/ActionRunner.js"
7
+ export {default as ActionWrapper} from "./lib/ActionWrapper.js"
8
+ export {default as Activity, ACTIVITY} from "./lib/Activity.js"
9
+ export {default as Piper} from "./lib/Piper.js"
@@ -0,0 +1,198 @@
1
+ import {Data, Sass, Promised, Time, Util, Valid} from "@gesslar/toolkit"
2
+
3
+ /**
4
+ * @typedef {(message: string, level?: number, ...args: Array<unknown>) => void} DebugFn
5
+ */
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
+ /**
16
+ * @typedef {Record<string, (context: unknown) => Promise<unknown>|unknown>} HookModule
17
+ */
18
+
19
+ /**
20
+ * Generic base class for managing hooks with configurable event types.
21
+ * Provides common functionality for hook registration, execution, and lifecycle management.
22
+ * Designed to be extended by specific implementations.
23
+ *
24
+ * Browser version: Requires pre-instantiated hooks. File-based loading is not supported.
25
+ */
26
+ export default class ActionHooks {
27
+ /** @type {HookModule|null} */
28
+ #hooks = null
29
+ /** @type {string|null} */
30
+ #actionKind = null
31
+ /** @type {number} */
32
+ #timeout = 1_000 // Default 1 second timeout
33
+ /** @type {DebugFn|null} */
34
+ #debug = null
35
+
36
+ /**
37
+ * Creates a new ActionHook instance.
38
+ *
39
+ * @param {ActionHooksConfig} config Configuration values describing how to load the hooks.
40
+ */
41
+ constructor({actionKind, hooks, hookTimeout = 1_000, debug}) {
42
+ this.#actionKind = actionKind
43
+ this.#hooks = hooks
44
+ this.#timeout = hookTimeout
45
+ this.#debug = debug
46
+ }
47
+
48
+ /**
49
+ * Gets the action identifier.
50
+ *
51
+ * @returns {string} Action identifier or instance
52
+ */
53
+ get actionKind() {
54
+ return this.#actionKind
55
+ }
56
+
57
+ /**
58
+ * Gets the loaded hooks object.
59
+ *
60
+ * @returns {object|null} Hooks object or null if not loaded
61
+ */
62
+ get hooks() {
63
+ return this.#hooks
64
+ }
65
+
66
+ /**
67
+ * Gets the hook execution timeout in milliseconds.
68
+ *
69
+ * @returns {number} Timeout in milliseconds
70
+ */
71
+ get timeout() {
72
+ return this.#timeout
73
+ }
74
+
75
+ /**
76
+ * Gets the setup hook function if available.
77
+ *
78
+ * @returns {(args: object) => unknown|null} Setup hook function or null
79
+ */
80
+ get setup() {
81
+ return this.hooks?.setup || null
82
+ }
83
+
84
+ /**
85
+ * Gets the cleanup hook function if available.
86
+ *
87
+ * @returns {(args: object) => unknown|null} Cleanup hook function or null
88
+ */
89
+ get cleanup() {
90
+ return this.hooks?.cleanup || null
91
+ }
92
+
93
+ /**
94
+ * Static factory method to create and initialize a hook manager.
95
+ * Browser version: Only works with pre-instantiated hooks passed via config.hooks.
96
+ *
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
100
+ */
101
+ static async new(config, debug) {
102
+ debug("Creating new HookManager instance with args: %o", 2, config)
103
+
104
+ if(!config.hooks) {
105
+ debug("No hooks provided (browser mode requires pre-instantiated hooks)", 2)
106
+
107
+ return null
108
+ }
109
+
110
+ const instance = new ActionHooks({...config, debug})
111
+
112
+ debug("Hooks loaded successfully for %o", 2, instance.actionKind)
113
+
114
+ return instance
115
+ }
116
+
117
+ /**
118
+ * Invoke a dynamically-named hook such as `before$foo`.
119
+ *
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.
123
+ * @returns {Promise<void>}
124
+ */
125
+ async callHook(kind, activityName, context) {
126
+ try {
127
+ const debug = this.#debug
128
+ const hooks = this.#hooks
129
+
130
+ if(!hooks)
131
+ return
132
+
133
+ const stringActivityName = Data.isType(activityName, "Symbol")
134
+ ? activityName.description
135
+ : activityName
136
+
137
+ const hookName = this.#getActivityHookName(kind, stringActivityName)
138
+
139
+ debug("Looking for hook: %o", 4, hookName)
140
+
141
+ const hook = hooks[hookName]
142
+ if(!hook)
143
+ return
144
+
145
+ debug("Triggering hook: %o", 4, hookName)
146
+ Valid.type(hook, "Function", `Hook "${hookName}" is not a function`)
147
+
148
+ const hookFunction = async() => {
149
+ debug("Hook function starting execution: %o", 4, hookName)
150
+
151
+ const duration = (
152
+ await Util.time(() => hook.call(this.#hooks, context))
153
+ ).cost
154
+
155
+ debug("Hook function completed successfully: %o, after %oms", 4, hookName, duration)
156
+ }
157
+
158
+ const hookTimeout = this.timeout
159
+ const expireAsync = (async() => {
160
+ await Time.after(hookTimeout)
161
+ throw Sass.new(`Hook ${hookName} execution exceeded timeout of ${hookTimeout}ms`)
162
+ })()
163
+
164
+ try {
165
+ debug("Starting Promise race for hook: %o", 4, hookName)
166
+ await Promised.race([
167
+ hookFunction(),
168
+ expireAsync
169
+ ])
170
+ } catch(error) {
171
+ throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
172
+ }
173
+
174
+ debug("We made it throoough the wildernessss", 4)
175
+
176
+ } catch(error) {
177
+ throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
178
+ }
179
+ }
180
+
181
+ #getActivityHookName(event, activityName) {
182
+ const name = activityName
183
+ .split(" ")
184
+ .map(a => a.trim())
185
+ .filter(Boolean)
186
+ .map(a => a
187
+ .split("")
188
+ .filter(b => /[\w]/.test(b))
189
+ .filter(Boolean)
190
+ .join("")
191
+ )
192
+ .map(a => a.toLowerCase())
193
+ .map((a, i) => i === 0 ? a : Util.capitalize(a))
194
+ .join("")
195
+
196
+ return `${event}$${name}`
197
+ }
198
+ }
package/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
- export {default as ActionBuilder} from "./lib/ActionBuilder.js"
1
+ // Browser-compatible base classes
2
+ export {default as ActionBuilder} from "./browser/lib/ActionBuilder.js"
3
+ export {default as ActionRunner} from "./browser/lib/ActionRunner.js"
4
+ export {default as ActionWrapper} from "./browser/lib/ActionWrapper.js"
5
+ export {default as Activity, ACTIVITY} from "./browser/lib/Activity.js"
6
+ export {default as Piper} from "./browser/lib/Piper.js"
7
+
8
+ // Node-enhanced version (extends browser, adds FileObject support)
2
9
  export {default as ActionHooks} from "./lib/ActionHooks.js"
3
- export {default as ActionRunner} from "./lib/ActionRunner.js"
4
- export {default as ActionWrapper} from "./lib/ActionWrapper.js"
5
- export {default as Activity, ACTIVITY} from "./lib/Activity.js"
6
- export {default as Piper} from "./lib/Piper.js"