@gesslar/toolkit 0.4.0 → 0.6.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.
@@ -1,209 +0,0 @@
1
- import {setTimeout as timeoutPromise} from "timers/promises"
2
- import Collection from "./Collection.js"
3
- import Data from "./Data.js"
4
- import Sass from "./Sass.js"
5
- import Valid from "./Valid.js"
6
-
7
- /**
8
- * Generic base class for managing hooks with configurable event types.
9
- * Provides common functionality for hook registration, execution, and lifecycle management.
10
- * Designed to be extended by specific implementations.
11
- */
12
- export default class BaseHookManager {
13
- #hooksFile = null
14
- #log = null
15
- #hooks = null
16
- #action = null
17
- #timeout = 1000 // Default 1 second timeout
18
- #allowedEvents = []
19
-
20
- /**
21
- * @param {object} config - Configuration object
22
- * @param {string|object} config.action - Action identifier or instance
23
- * @param {object} config.hooksFile - File object containing hooks
24
- * @param {object} config.logger - Logger instance
25
- * @param {number} [config.timeOut] - Hook execution timeout in milliseconds
26
- * @param {string[]} [config.allowedEvents] - Array of allowed event types for validation
27
- */
28
- constructor({action, hooksFile, logger, timeOut = 1000, allowedEvents = []}) {
29
- this.#action = action
30
- this.#hooksFile = hooksFile
31
- this.#log = logger
32
- this.#timeout = timeOut
33
- this.#allowedEvents = allowedEvents
34
- }
35
-
36
- get action() {
37
- return this.#action
38
- }
39
-
40
- get hooksFile() {
41
- return this.#hooksFile
42
- }
43
-
44
- get hooks() {
45
- return this.#hooks
46
- }
47
-
48
- get log() {
49
- return this.#log
50
- }
51
-
52
- get timeout() {
53
- return this.#timeout
54
- }
55
-
56
- get allowedEvents() {
57
- return this.#allowedEvents
58
- }
59
-
60
- get setup() {
61
- return this.hooks?.setup || null
62
- }
63
-
64
- get cleanup() {
65
- return this.hooks?.cleanup || null
66
- }
67
-
68
- /**
69
- * Static factory method to create and initialize a hook manager.
70
- * Override loadHooks() in subclasses to customize hook loading logic.
71
- *
72
- * @param {object} config - Same as constructor config
73
- * @returns {Promise<BaseHookManager|null>} Initialized hook manager or null if no hooks found
74
- */
75
- static async new(config) {
76
- const instance = new this(config)
77
- const debug = instance.log.newDebug()
78
-
79
- debug("Creating new HookManager instance with args: `%o`", 2, config)
80
-
81
- const hooksFile = instance.hooksFile
82
-
83
- debug("Loading hooks from `%s`", 2, hooksFile.uri)
84
-
85
- debug("Checking hooks file exists: %j", 2, hooksFile)
86
-
87
- try {
88
- const hooksFileContent = await import(hooksFile.uri)
89
-
90
- debug("Hooks file loaded successfully", 2)
91
-
92
- if(!hooksFileContent)
93
- throw new Error(`Hooks file is empty: ${hooksFile.uri}`)
94
-
95
- const hooks = await instance.loadHooks(hooksFileContent)
96
-
97
- if(Data.isEmpty(hooks))
98
- return null
99
-
100
- debug("Hooks found for action: `%s`", 2, instance.action)
101
-
102
- if(!hooks)
103
- return null
104
-
105
- // Attach common properties to hooks
106
- hooks.log = instance.log
107
- hooks.timeout = instance.timeout
108
- instance.#hooks = hooks
109
-
110
- debug("Hooks loaded successfully for `%s`", 2, instance.action)
111
-
112
- return instance
113
- } catch(error) {
114
- debug("Failed to load hooks: %s", 1, error.message)
115
-
116
- return null
117
- }
118
- }
119
-
120
- /**
121
- * Load hooks from the imported hooks file content.
122
- * Override in subclasses to customize hook loading logic.
123
- *
124
- * @param {object} hooksFileContent - Imported hooks file content
125
- * @returns {Promise<object|null>} Loaded hooks object or null if no hooks found
126
- * @protected
127
- */
128
- async loadHooks(hooksFileContent) {
129
- const hooks = hooksFileContent.default || hooksFileContent.Hooks
130
-
131
- if(!hooks)
132
- throw new Error(`\`${this.hooksFile.uri}\` contains no hooks.`)
133
-
134
- // Default implementation: look for hooks by action name
135
- const hooksObj = hooks[this.action]
136
-
137
- return hooksObj || null
138
- }
139
-
140
- /**
141
- * Trigger a hook by event name.
142
- *
143
- * @param {string} event - The type of hook to trigger
144
- * @param {object} args - The hook arguments as an object
145
- * @returns {Promise<unknown>} The result of the hook
146
- */
147
- async on(event, args) {
148
- const debug = this.log.newDebug()
149
-
150
- debug("Triggering hook for event `%s`", 4, event)
151
-
152
- if(!event)
153
- throw new Error("Event type is required for hook invocation")
154
-
155
- // Validate event type if allowed events are configured
156
- if(this.#allowedEvents.length > 0 && !this.#allowedEvents.includes(event))
157
- throw new Error(`Invalid event type: ${event}. Allowed events: ${this.#allowedEvents.join(", ")}`)
158
-
159
- const hook = this.hooks?.[event]
160
-
161
- if(hook) {
162
- Valid.type(hook, "function", `Hook "${event}" is not a function`)
163
-
164
- const hookExecution = hook.call(this.hooks, args)
165
- const hookTimeout = this.timeout
166
-
167
- const expireAsync = () =>
168
- timeoutPromise(
169
- hookTimeout,
170
- new Error(`Hook execution exceeded timeout of ${hookTimeout}ms`)
171
- )
172
-
173
- const result = await Promise.race([hookExecution, expireAsync()])
174
-
175
- if(result?.status === "error")
176
- throw Sass.new(result.error)
177
-
178
- debug("Hook executed successfully for event: `%s`", 4, event)
179
-
180
- return result
181
- } else {
182
- debug("No hook found for event: `%s`", 4, event)
183
-
184
- return null
185
- }
186
- }
187
-
188
- /**
189
- * Check if a hook exists for the given event.
190
- *
191
- * @param {string} event - Event name to check
192
- * @returns {boolean} True if hook exists
193
- */
194
- hasHook(event) {
195
- return !!(this.hooks?.[event])
196
- }
197
-
198
- /**
199
- * Get all available hook events.
200
- *
201
- * @returns {string[]} Array of available hook event names
202
- */
203
- getAvailableEvents() {
204
- return this.hooks ? Object.keys(this.hooks).filter(key =>
205
- typeof this.hooks[key] === "function" &&
206
- !["setup", "cleanup", "log", "timeout"].includes(key)
207
- ) : []
208
- }
209
- }
package/src/lib/Piper.js DELETED
@@ -1,181 +0,0 @@
1
- /**
2
- * Generic Pipeline - Process items through a series of steps with concurrency control
3
- *
4
- * This abstraction handles:
5
- * - Concurrent processing with configurable limits
6
- * - Pipeline of processing steps
7
- * - Result categorization (success/warning/error)
8
- * - Setup/cleanup lifecycle hooks
9
- * - Error handling and reporting
10
- */
11
-
12
- export default class Piper {
13
- #succeeded = []
14
- #warned = []
15
- #errored = []
16
- #steps = []
17
- #setupHooks = []
18
- #cleanupHooks = []
19
- #logger
20
-
21
- constructor(options = {}) {
22
- this.#logger = options.logger || {newDebug: () => () => {}}
23
- }
24
-
25
- /**
26
- * Add a processing step to the pipeline
27
- *
28
- * @param {(context: object) => Promise<object>} stepFn - Function that processes an item: (context) => Promise<result>
29
- * @param {object} options - Step options (name, required, etc.)
30
- * @returns {Piper} The pipeline instance (for chaining)
31
- */
32
- addStep(stepFn, options = {}) {
33
- this.#steps.push({
34
- fn: stepFn,
35
- name: options.name || `Step ${this.#steps.length + 1}`,
36
- required: options.required !== false, // Default to required
37
- ...options
38
- })
39
-
40
- return this
41
- }
42
-
43
- /**
44
- * Add setup hook that runs before processing starts
45
- *
46
- * @param {() => Promise<void>} setupFn - Setup function: () => Promise<void>
47
- * @returns {Piper} The pipeline instance (for chaining)
48
- */
49
- addSetup(setupFn) {
50
- this.#setupHooks.push(setupFn)
51
-
52
- return this
53
- }
54
-
55
- /**
56
- * Add cleanup hook that runs after processing completes
57
- *
58
- * @param {() => Promise<void>} cleanupFn - Cleanup function: () => Promise<void>
59
- * @returns {Piper} The pipeline instance (for chaining)
60
- */
61
- addCleanup(cleanupFn) {
62
- this.#cleanupHooks.push(cleanupFn)
63
-
64
- return this
65
- }
66
-
67
- /**
68
- * Process items through the pipeline with concurrency control
69
- *
70
- * @param {Array} items - Items to process
71
- * @param {number} maxConcurrent - Maximum concurrent items to process
72
- * @returns {Promise<object>} - Results object with succeeded, warned, errored arrays
73
- */
74
- async pipe(items, maxConcurrent = 10) {
75
- const itemQueue = [...items]
76
- const activePromises = []
77
-
78
- // Run setup hooks
79
- await Promise.allSettled(this.#setupHooks.map(hook => hook()))
80
-
81
- const processNextItem = item => {
82
- return this.#processItem(item).then(result => {
83
- // Categorize result
84
- if(result.status === "success") {
85
- this.#succeeded.push({input: item, ...result})
86
- } else if(result.status === "warning") {
87
- this.#warned.push({input: item, ...result})
88
- } else {
89
- this.#errored.push({input: item, ...result})
90
- }
91
-
92
- // Process next item if queue has items
93
- if(itemQueue.length > 0) {
94
- const nextItem = itemQueue.shift()
95
-
96
- return processNextItem(nextItem)
97
- }
98
- })
99
- }
100
-
101
- // Fill initial concurrent slots
102
- while(activePromises.length < maxConcurrent && itemQueue.length > 0) {
103
- const item = itemQueue.shift()
104
-
105
- activePromises.push(processNextItem(item))
106
- }
107
-
108
- // Wait for all processing to complete
109
- await Promise.allSettled(activePromises)
110
-
111
- // Run cleanup hooks
112
- await Promise.allSettled(this.#cleanupHooks.map(hook => hook()))
113
-
114
- return {
115
- succeeded: this.#succeeded,
116
- warned: this.#warned,
117
- errored: this.#errored
118
- }
119
- }
120
-
121
- /**
122
- * Process a single item through all pipeline steps
123
- *
124
- * @param {object} item - The item to process
125
- * @returns {Promise<object>} Result object with status and data
126
- * @private
127
- */
128
- async #processItem(item) {
129
- const debug = this.#logger.newDebug()
130
- const context = {item, data: {}}
131
-
132
- try {
133
- // Execute each step in sequence
134
- for(const step of this.#steps) {
135
- debug(`Executing step: ${step.name}`, 2)
136
-
137
- const result = await step.fn(context)
138
-
139
- // Handle step result
140
- if(result && typeof result === "object") {
141
- if(result.status === "error") {
142
- return result
143
- }
144
-
145
- if(result.status === "warning" && step.required) {
146
- return result
147
- }
148
-
149
- // Merge result data into context for next steps
150
- context.data = {...context.data, ...result.data}
151
- context.status = result.status || context.status
152
- }
153
- }
154
-
155
- return {
156
- status: context.status || "success",
157
- ...context.data
158
- }
159
-
160
- } catch(error) {
161
- return {
162
- status: "error",
163
- error,
164
- item
165
- }
166
- }
167
- }
168
-
169
- /**
170
- * Clear results (useful for reusing pipeline instance)
171
- *
172
- * @returns {Piper} The pipeline instance (for chaining)
173
- */
174
- clearResults() {
175
- this.#succeeded = []
176
- this.#warned = []
177
- this.#errored = []
178
-
179
- return this
180
- }
181
- }