@gesslar/toolkit 0.2.8 → 0.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.2.8",
3
+ "version": "0.3.0",
4
4
  "description": "Get in, bitches, we're going toolkitting.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -25,7 +25,8 @@
25
25
  "submit": "npm publish --access public",
26
26
  "update": "npx npm-check-updates -u && npm install",
27
27
  "test": "node --test tests/unit/*.test.js",
28
- "test:unit": "node --test tests/unit/*.test.js"
28
+ "test:unit": "node --test tests/unit/*.test.js",
29
+ "pr": "gt submit --cli --publish --restack --ai --merge-when-ready"
29
30
  },
30
31
  "repository": {
31
32
  "type": "git",
@@ -49,6 +50,7 @@
49
50
  "license": "Unlicense",
50
51
  "homepage": "https://github.com/gesslar/toolkit#readme",
51
52
  "dependencies": {
53
+ "@gesslar/colours": "^0.0.1",
52
54
  "globby": "^15.0.0",
53
55
  "json5": "^2.2.3",
54
56
  "yaml": "^2.8.1"
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ export {default as Collection} from "./lib/Collection.js"
9
9
  export {default as Data} from "./lib/Data.js"
10
10
  export {default as Glog} from "./lib/Glog.js"
11
11
  export {default as Sass} from "./lib/Sass.js"
12
+ export {default as Tantrum} from "./lib/Tantrum.js"
12
13
  export {default as Term} from "./lib/Term.js"
13
14
  export {default as Type} from "./lib/TypeSpec.js"
14
15
  export {default as Util} from "./lib/Util.js"
@@ -0,0 +1,144 @@
1
+ import Valid from "./Valid.js"
2
+
3
+ /** @typedef {import("./ActionRunner.js").default} ActionRunner */
4
+
5
+ /**
6
+ * Activity bit flags recognised by {@link ActionBuilder#act}. The flag decides
7
+ * how results are accumulated for each activity.
8
+ *
9
+ * @readonly
10
+ * @enum {number}
11
+ */
12
+ export const ACTIVITY = Object.freeze({
13
+ ONCE: 1<<1,
14
+ MANY: 1<<2,
15
+ PARALLEL: 1<<3,
16
+ })
17
+
18
+ /**
19
+ * Fluent builder for describing how an action should process the context that
20
+ * flows through the {@link ActionRunner}. Consumers register named activities,
21
+ * optional hook pairs, and nested parallel pipelines before handing the
22
+ * builder back to the runner for execution.
23
+ *
24
+ * Typical usage:
25
+ *
26
+ * ```js
27
+ * const pipeline = new ActionBuilder(myAction)
28
+ * .act("prepare", ACTIVITY.ONCE, ctx => ctx.initialise())
29
+ * .parallel(parallel => parallel
30
+ * .act("step", ACTIVITY.MANY, ctx => ctx.consume())
31
+ * )
32
+ * .act("finalise", ACTIVITY.ONCE, ctx => ctx.complete())
33
+ * .build()
34
+ * ```
35
+ *
36
+ * @class ActionBuilder
37
+ */
38
+ export default class ActionBuilder {
39
+ #action = null
40
+ #activities = new Map([])
41
+
42
+ /**
43
+ * Creates a new ActionBuilder instance with the provided action callback.
44
+ *
45
+ * @param {(ctx: object) => void} action Base action invoked by the runner when a block
46
+ * satisfies the configured structure.
47
+ */
48
+ constructor(action) {
49
+ this.#action = action
50
+ }
51
+
52
+ /**
53
+ * Returns the underlying action that will receive the extracted context.
54
+ *
55
+ * @returns {(ctx: object) => void} The action callback function that processes the extracted context.
56
+ */
57
+ get action() {
58
+ return this.#action
59
+ }
60
+
61
+ /**
62
+ * Returns the registered activities keyed by their name.
63
+ *
64
+ * @returns {Map<string | symbol, {op: (context: object) => unknown, kind: number, hooks: {before: ((context: object) => void)|null, after: ((context: object) => void)|null}}>} Map of registered activities and their metadata.
65
+ */
66
+ get activities() {
67
+ return this.#activities
68
+ }
69
+
70
+ /**
71
+ * Registers a named activity that will run for each matching block.
72
+ *
73
+ * @param {string} name Unique activity identifier.
74
+ * @param {number} kind Activity accumulation strategy (see {@link ACTIVITY}).
75
+ * @param {(context: object) => unknown} op Work function executed with the runner context.
76
+ * @param {{before?: (context: object) => void, after?: (context: object) => void}} [hooks] Optional hooks to run before or after the activity operation.
77
+ * @returns {ActionBuilder} Builder instance for chaining.
78
+ */
79
+ act(name, kind, op, hooks={}) {
80
+ this.#validActivityKind(kind)
81
+ this.#dupeActivityCheck(name)
82
+
83
+ hooks = this.#normalizeHooks(hooks)
84
+
85
+ this.#activities.set(name, {op, kind, hooks})
86
+
87
+ return this
88
+ }
89
+
90
+ #normalizeHooks({before=null, after=null}) {
91
+ return {before, after}
92
+ }
93
+
94
+ /**
95
+ * Defines a nested pipeline that will run with the {@link ACTIVITY} flag PARALLEL.
96
+ *
97
+ * The callback receives a fresh {@link ActionBuilder} scoped to the current
98
+ * action. The callback must return the configured builder so the runner can
99
+ * execute the nested pipeline.
100
+ *
101
+ * @param {(builder: ActionBuilder) => ActionBuilder} func Callback configuring a nested builder.
102
+ * @returns {ActionBuilder} Builder instance for chaining.
103
+ */
104
+ parallel(func) {
105
+ const activityName = Symbol(performance.now())
106
+
107
+ this.#activities.set(activityName, {
108
+ op: func.call(this.action, new ActionBuilder(this.action)),
109
+ kind: ACTIVITY.PARALLEL
110
+ })
111
+
112
+ return this
113
+ }
114
+
115
+ #validActivityKind(kind) {
116
+ Valid.assert(
117
+ Object.values(ACTIVITY).includes(kind),
118
+ "Invalid activity kind."
119
+ )
120
+ }
121
+
122
+ /**
123
+ * Validates that an activity name has not been reused.
124
+ *
125
+ * @private
126
+ * @param {string|symbol} name Activity identifier.
127
+ */
128
+ #dupeActivityCheck(name) {
129
+ Valid.assert(
130
+ !this.#activities.has(name),
131
+ `Activity '${String(name)}' has already been registered.`
132
+ )
133
+ }
134
+
135
+ /**
136
+ * Finalises the builder and returns a payload that can be consumed by the
137
+ * runner.
138
+ *
139
+ * @returns {{action: (context: object) => unknown, build: ActionBuilder}} Payload consumed by the {@link ActionRunner} constructor.
140
+ */
141
+ build() {
142
+ return {action: this.#action, build: this}
143
+ }
144
+ }
@@ -0,0 +1,109 @@
1
+ import ActionBuilder, {ACTIVITY} from "./ActionBuilder.js"
2
+ import Data from "./Data.js"
3
+ import Piper from "./Piper.js"
4
+ import Sass from "./Sass.js"
5
+ import Glog from "./Glog.js"
6
+
7
+ /**
8
+ * Orchestrates execution of {@link ActionBuilder}-produced pipelines.
9
+ *
10
+ * Activities run in insertion order, with support for once-off work, repeated
11
+ * loops, and nested parallel pipelines. Each activity receives a mutable
12
+ * context object under `result.value` that can be replaced or enriched.
13
+ */
14
+ export default class ActionRunner {
15
+ #action = null
16
+ #build = null
17
+ #logger = null
18
+
19
+ constructor({action, build, logger}) {
20
+ this.#action = action
21
+ this.#build = build
22
+ this.#logger = logger ?? {newDebug: () => () => {}}
23
+ }
24
+
25
+ /**
26
+ * Executes the configured action pipeline.
27
+ *
28
+ * @param {unknown} content Seed value passed to the first activity.
29
+ * @returns {Promise<unknown>} Final value produced by the pipeline, or null when a parallel stage reports failures.
30
+ * @throws {Sass} When no activities are registered or required parallel builders are missing.
31
+ */
32
+ async run(content) {
33
+ const AR = ActionRunner
34
+ const result = {value: content}
35
+ const action = this.#action
36
+ const activities = this.#build.activities
37
+
38
+ if(!activities.size)
39
+ throw Sass.new("No activities defined in action.")
40
+
41
+ for(const [_,activity] of activities) {
42
+ const {op} = activity
43
+
44
+ if(activity.kind === ACTIVITY.ONCE) {
45
+
46
+ if(Data.typeOf(activity.hooks?.before) === "Function")
47
+ await activity.hooks.before.call(action, result)
48
+
49
+ const activityResult = await op.call(action, result)
50
+
51
+ if(!activityResult)
52
+ break
53
+
54
+ if(Data.typeOf(activity.hooks?.after) === "Function")
55
+ await activity.hooks.after.call(action, result)
56
+
57
+ } else if(activity.kind == ACTIVITY.MANY) {
58
+ for(;;) {
59
+
60
+ if(Data.typeOf(activity.hooks?.before) === "Function")
61
+ await activity.hooks.before.call(action, result)
62
+
63
+ const activityResult = await op.call(action, result)
64
+
65
+ if(!activityResult)
66
+ break
67
+
68
+ if(Data.typeOf(activity.hooks?.after) === "Function")
69
+ await activity.hooks.after.call(action, result)
70
+ }
71
+ } else if(activity.kind === ACTIVITY.PARALLEL) {
72
+ if(op === undefined)
73
+ throw Sass.new("Missing action builder. Did you return the builder?")
74
+
75
+ if(!op)
76
+ throw Sass.new("Okay, cheeky monkey, you need to return the builder for this to work.")
77
+
78
+ const piper = new Piper({logger: this.#logger})
79
+ .addStep(c => new AR(op.build()).run(c))
80
+
81
+ result.value = await piper.pipe()
82
+ Glog(result)
83
+ throw Sass.new("Nope")
84
+
85
+ // // wheeeeeeeeeeeeee! ZOOMZOOM!
86
+ // const settled = await Util.settleAll(
87
+ // result.value.map()
88
+ // )
89
+
90
+ // const rejected = settled
91
+ // .filter(r => r.status === "rejected")
92
+ // .map(r => {
93
+ // return r.reason instanceof Sass
94
+ // ? r.reason
95
+ // : Sass.new("Running structured parsing.", r.reason)
96
+ // })
97
+ // .map(r => r.report(true))
98
+
99
+ // if(rejected.length)
100
+ // return null
101
+
102
+ // result.value = settled.map(s => s.value)
103
+ // .sort((a,b) => a.index-b.index)
104
+ }
105
+ }
106
+
107
+ return result.value
108
+ }
109
+ }
@@ -0,0 +1,246 @@
1
+ import Data from "./Data.js"
2
+ import Sass from "./Sass.js"
3
+ import ActionBuilder from "./ActionBuilder.js"
4
+ import ActionRunner from "./ActionRunner.js"
5
+
6
+ /**
7
+ * Generic base class for managing actions with lifecycle hooks.
8
+ * Provides common functionality for action setup, execution, and cleanup.
9
+ * Designed to be extended by specific implementations.
10
+ */
11
+ export default class BaseActionManager {
12
+ #action = null
13
+ #hookManager = null
14
+ #contract = null
15
+ #log = null
16
+ #debug = null
17
+ #file = null
18
+ #variables = null
19
+ #runner = null
20
+ #id = null
21
+
22
+ /**
23
+ * @param {object} config - Configuration object
24
+ * @param {object} config.actionDefinition - Action definition with action, file, and contract
25
+ * @param {object} config.logger - Logger instance
26
+ * @param {object} [config.variables] - Variables to pass to action
27
+ */
28
+ constructor({actionDefinition, logger, variables}) {
29
+ this.#id = Symbol(performance.now())
30
+ this.#log = logger
31
+ this.#debug = this.#log.newDebug()
32
+ this.#variables = variables || {}
33
+
34
+ this.#initialize(actionDefinition)
35
+ }
36
+
37
+ get id() {
38
+ return this.#id
39
+ }
40
+
41
+ get action() {
42
+ return this.#action
43
+ }
44
+
45
+ get hookManager() {
46
+ return this.#hookManager
47
+ }
48
+
49
+ set hookManager(hookManager) {
50
+ if (this.hookManager)
51
+ throw new Error("Hook manager already set")
52
+
53
+ this.#hookManager = hookManager
54
+ this.#attachHooksToAction(hookManager)
55
+ }
56
+
57
+ get contract() {
58
+ return this.#contract
59
+ }
60
+
61
+ get meta() {
62
+ return this.#action?.meta
63
+ }
64
+
65
+ get log() {
66
+ return this.#log
67
+ }
68
+
69
+ get variables() {
70
+ return this.#variables
71
+ }
72
+
73
+ get runner() {
74
+ return this.#runner
75
+ }
76
+
77
+ get file() {
78
+ return this.#file
79
+ }
80
+
81
+ /**
82
+ * Initialize the action manager with the provided definition.
83
+ * Override in subclasses to add specific validation or setup.
84
+ *
85
+ * @param {object} actionDefinition - Action definition
86
+ * @protected
87
+ */
88
+ #initialize(actionDefinition) {
89
+ const debug = this.#debug
90
+
91
+ debug("Setting up action", 2)
92
+
93
+ const {action, file, contract} = actionDefinition
94
+
95
+ if (!action)
96
+ throw new Error("Action is required")
97
+
98
+ if (!contract)
99
+ throw new Error("Contract is required")
100
+
101
+ this.#action = action
102
+ this.#contract = contract
103
+ this.#file = file
104
+
105
+ debug("Action initialization complete", 2)
106
+ }
107
+
108
+ /**
109
+ * Attach hooks to the action instance.
110
+ * Override in subclasses to customize hook attachment.
111
+ *
112
+ * @param {object} hookManager - Hook manager instance
113
+ * @protected
114
+ */
115
+ #attachHooksToAction(hookManager) {
116
+ // Basic hook attachment - can be overridden by subclasses
117
+ this.action.hook = hookManager.on?.bind(hookManager)
118
+ this.action.hooks = hookManager.hooks
119
+ }
120
+
121
+ /**
122
+ * Setup the action by creating and configuring the runner.
123
+ * Override setupActionInstance() in subclasses for custom setup logic.
124
+ *
125
+ * @returns {Promise<void>}
126
+ */
127
+ async setupAction() {
128
+ this.#debug("Setting up action for %s on %s", 2, this.action.meta?.kind, this.id)
129
+
130
+ await this.#setupHooks()
131
+ await this.#setupActionInstance()
132
+ }
133
+
134
+ /**
135
+ * Setup the action instance and create the runner.
136
+ * Override in subclasses to customize action setup.
137
+ *
138
+ * @protected
139
+ */
140
+ async #setupActionInstance() {
141
+ const actionInstance = new this.action()
142
+ const setup = actionInstance?.setup
143
+
144
+ // Setup is required for actions.
145
+ if (Data.typeOf(setup) === "Function") {
146
+ const builder = new ActionBuilder(actionInstance)
147
+ const configuredBuilder = setup(builder)
148
+ const buildResult = configuredBuilder.build()
149
+ const runner = new ActionRunner({
150
+ action: buildResult.action,
151
+ build: buildResult.build,
152
+ logger: this.#log
153
+ })
154
+
155
+ this.#runner = runner
156
+ } else {
157
+ throw Sass.new("Action setup must be a function.")
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Run the action with the provided input.
163
+ *
164
+ * @param {unknown} result - Input to pass to the action
165
+ * @returns {Promise<unknown>} Action result
166
+ */
167
+ async runAction(result) {
168
+ if (!this.#runner)
169
+ throw new Error("Action not set up. Call setupAction() first.")
170
+
171
+ return await this.#runner.run(result)
172
+ }
173
+
174
+ /**
175
+ * Cleanup the action and hooks.
176
+ *
177
+ * @returns {Promise<void>}
178
+ */
179
+ async cleanupAction() {
180
+ this.#debug("Cleaning up action for %s on %s", 2, this.action.meta?.kind, this.id)
181
+
182
+ await this.#cleanupHooks()
183
+ await this.#cleanupActionInstance()
184
+ }
185
+
186
+ /**
187
+ * Setup hooks if hook manager is present.
188
+ * Override in subclasses to customize hook setup.
189
+ *
190
+ * @protected
191
+ */
192
+ async #setupHooks() {
193
+ const setup = this.#hookManager?.setup
194
+
195
+ const type = Data.typeOf(setup)
196
+
197
+ // No hooks attached.
198
+ if (type === "Null" || type === "Undefined")
199
+ return
200
+
201
+ if (type !== "Function")
202
+ throw Sass.new("Hook setup must be a function.")
203
+
204
+ await setup.call(
205
+ this.hookManager.hooks, {
206
+ action: this.action,
207
+ variables: this.#variables,
208
+ log: this.#log
209
+ }
210
+ )
211
+ }
212
+
213
+ /**
214
+ * Cleanup hooks if hook manager is present.
215
+ * Override in subclasses to customize hook cleanup.
216
+ *
217
+ * @protected
218
+ */
219
+ async #cleanupHooks() {
220
+ const cleanup = this.hookManager?.cleanup
221
+
222
+ if (!cleanup)
223
+ return
224
+
225
+ await cleanup.call(this.hookManager.hooks)
226
+ }
227
+
228
+ /**
229
+ * Cleanup the action instance.
230
+ * Override in subclasses to add custom cleanup logic.
231
+ *
232
+ * @protected
233
+ */
234
+ async #cleanupActionInstance() {
235
+ const cleanup = this.action?.cleanup
236
+
237
+ if (!cleanup)
238
+ return
239
+
240
+ await cleanup.call(this.action)
241
+ }
242
+
243
+ toString() {
244
+ return `${this.#file?.module || "UNDEFINED"} (${this.meta?.action || "UNDEFINED"})`
245
+ }
246
+ }