@unrdf/hooks 26.4.8 → 26.5.5

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.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/examples/hook-chains/node_modules/.bin/jiti +2 -2
  3. package/examples/hook-chains/node_modules/.bin/msw +4 -4
  4. package/examples/hook-chains/node_modules/.bin/terser +2 -2
  5. package/examples/hook-chains/node_modules/.bin/tsc +2 -2
  6. package/examples/hook-chains/node_modules/.bin/tsserver +2 -2
  7. package/examples/hook-chains/node_modules/.bin/tsx +2 -2
  8. package/examples/hook-chains/node_modules/.bin/validate-hooks +2 -2
  9. package/examples/hook-chains/node_modules/.bin/vite +4 -4
  10. package/examples/hook-chains/node_modules/.bin/vitest +2 -2
  11. package/examples/hook-chains/node_modules/.bin/yaml +4 -4
  12. package/examples/hook-chains/package.json +3 -3
  13. package/examples/hooks-marketplace.mjs +10 -10
  14. package/examples/knowledge-hook-manager-usage.mjs +1 -1
  15. package/examples/policy-hooks/node_modules/.bin/jiti +2 -2
  16. package/examples/policy-hooks/node_modules/.bin/msw +4 -4
  17. package/examples/policy-hooks/node_modules/.bin/terser +2 -2
  18. package/examples/policy-hooks/node_modules/.bin/tsc +2 -2
  19. package/examples/policy-hooks/node_modules/.bin/tsserver +2 -2
  20. package/examples/policy-hooks/node_modules/.bin/tsx +2 -2
  21. package/examples/policy-hooks/node_modules/.bin/validate-hooks +2 -2
  22. package/examples/policy-hooks/node_modules/.bin/vite +4 -4
  23. package/examples/policy-hooks/node_modules/.bin/vitest +2 -2
  24. package/examples/policy-hooks/node_modules/.bin/yaml +4 -4
  25. package/examples/policy-hooks/package.json +3 -3
  26. package/examples/production-policy-chain.mjs +54 -0
  27. package/examples/semantic-inference-hook.mjs +66 -0
  28. package/examples/validate-hooks.mjs +1 -1
  29. package/package.json +10 -5
  30. package/src/define.mjs +3 -0
  31. package/src/hooks/condition-evaluator.mjs +2 -0
  32. package/src/hooks/dependency-graph.mjs +388 -0
  33. package/src/hooks/observability.mjs +67 -74
  34. package/src/hooks/parallel-executor.mjs +295 -0
  35. package/src/hooks/policy-pack.mjs +2 -2
  36. package/src/hooks/schemas.mjs +1 -0
  37. package/src/hooks/wasm4pm-conformance-bridge.mjs +25 -0
  38. package/src/hooks/worker-pool.mjs +478 -0
  39. package/examples/hook-chains/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  40. package/examples/hook-chains/unrdf-hooks-example-chains-5.0.0.tgz +0 -0
  41. package/examples/policy-hooks/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  42. package/examples/policy-hooks/unrdf-hooks-example-policy-5.0.0.tgz +0 -0
@@ -0,0 +1,478 @@
1
+ /**
2
+ * @file Worker Pool - Parallel hook execution using Node.js worker threads
3
+ * @module hooks/worker-pool
4
+ */
5
+
6
+ import { Worker } from 'worker_threads';
7
+ import { fileURLToPath } from 'url';
8
+ import { dirname, join } from 'path';
9
+ import { z } from 'zod';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ /* ========================================================================= */
15
+ /* Zod Schemas */
16
+ /* ========================================================================= */
17
+
18
+ export const WorkerPoolConfigSchema = z.object({
19
+ minWorkers: z.number().int().positive().optional().default(1),
20
+ maxWorkers: z.number().int().positive().optional().default(4),
21
+ maxQueueSize: z.number().int().positive().optional().default(1000),
22
+ taskTimeout: z.number().int().positive().optional().default(30000),
23
+ idleTimeout: z.number().int().positive().optional().default(60000),
24
+ });
25
+
26
+ /* ========================================================================= */
27
+ /* Worker Pool Class */
28
+ /* ========================================================================= */
29
+
30
+ /**
31
+ * Task result schema.
32
+ */
33
+ const TaskResultSchema = z.object({
34
+ success: z.boolean(),
35
+ result: z.any().optional(),
36
+ error: z.string().optional(),
37
+ workerId: z.string(),
38
+ });
39
+
40
+ /**
41
+ * Worker pool for parallel task execution.
42
+ *
43
+ * @class WorkerPool
44
+ */
45
+ export class WorkerPool {
46
+ /**
47
+ * Create a new worker pool.
48
+ *
49
+ * @param {Object} [config={}] - Pool configuration
50
+ * @param {number} [config.minWorkers=1] - Minimum number of workers
51
+ * @param {number} [config.maxWorkers=4] - Maximum number of workers
52
+ * @param {number} [config.maxQueueSize=1000] - Maximum queued tasks
53
+ * @param {number} [config.taskTimeout=30000] - Task timeout in ms
54
+ * @param {number} [config.idleTimeout=60000] - Idle worker timeout in ms
55
+ */
56
+ constructor(config = {}) {
57
+ const validatedConfig = WorkerPoolConfigSchema.parse(config);
58
+
59
+ this.minWorkers = validatedConfig.minWorkers;
60
+ this.maxWorkers = validatedConfig.maxWorkers;
61
+ this.maxQueueSize = validatedConfig.maxQueueSize;
62
+ this.taskTimeout = validatedConfig.taskTimeout;
63
+ this.idleTimeout = validatedConfig.idleTimeout;
64
+
65
+ /** @type {Map<string, Worker>} - Active workers */
66
+ this.workers = new Map();
67
+
68
+ /** @type {Map<string, number>} - Worker task counts (for load balancing) */
69
+ this.workerTaskCounts = new Map();
70
+
71
+ /** @type {Array<{task: Object, resolve: Function, reject: Function}>} - Task queue */
72
+ this.taskQueue = [];
73
+
74
+ /** @type {Map<string, NodeJS.Timeout>} - Idle timers */
75
+ this.idleTimers = new Map();
76
+
77
+ /** @type {number} - Next worker ID */
78
+ this.nextWorkerId = 0;
79
+
80
+ /** @type {boolean} - Pool shutdown flag */
81
+ this.isShutdown = false;
82
+
83
+ // Initialize minimum workers
84
+ this._ensureMinWorkers();
85
+ }
86
+
87
+ /**
88
+ * Ensure minimum number of workers are active.
89
+ *
90
+ * @private
91
+ */
92
+ _ensureMinWorkers() {
93
+ while (this.workers.size < this.minWorkers) {
94
+ this._createWorker();
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Create a new worker.
100
+ *
101
+ * @private
102
+ * @returns {Worker} - New worker instance
103
+ */
104
+ _createWorker() {
105
+ const workerId = `worker-${this.nextWorkerId++}`;
106
+
107
+ // Create worker with inline code (simpler than separate file)
108
+ const workerCode = `
109
+ const { parentPort } = require('worker_threads');
110
+
111
+ parentPort.on('message', ({ taskId, data }) => {
112
+ try {
113
+ // Execute task function
114
+ const result = data.taskFn(...data.args);
115
+
116
+ parentPort.postMessage({
117
+ taskId,
118
+ success: true,
119
+ result,
120
+ });
121
+ } catch (error) {
122
+ parentPort.postMessage({
123
+ taskId,
124
+ success: false,
125
+ error: error.message,
126
+ });
127
+ }
128
+ });
129
+ `;
130
+
131
+ const worker = new Worker(workerCode, {
132
+ eval: true,
133
+ });
134
+
135
+ // Set up message handler
136
+ worker.on('message', ({ taskId, success, result, error }) => {
137
+ this._handleWorkerMessage(workerId, taskId, success, result, error);
138
+ });
139
+
140
+ // Handle worker errors
141
+ worker.on('error', error => {
142
+ console.error(`Worker ${workerId} error:`, error);
143
+ this._removeWorker(workerId);
144
+ });
145
+
146
+ // Handle worker exit
147
+ worker.on('exit', code => {
148
+ if (code !== 0 && !this.shutdown) {
149
+ console.error(`Worker ${workerId} exited with code ${code}`);
150
+ }
151
+ this._removeWorker(workerId);
152
+ });
153
+
154
+ // Track worker
155
+ this.workers.set(workerId, worker);
156
+ this.workerTaskCounts.set(workerId, 0);
157
+
158
+ return worker;
159
+ }
160
+
161
+ /**
162
+ * Handle message from worker.
163
+ *
164
+ * @private
165
+ * @param {string} workerId - Worker ID
166
+ * @param {string} taskId - Task ID
167
+ * @param {boolean} success - Task success flag
168
+ * @param {*} result - Task result
169
+ * @param {string} [error] - Error message if failed
170
+ */
171
+ _handleWorkerMessage(workerId, taskId, success, result, error) {
172
+ // Find and remove task from queue
173
+ const taskIndex = this.taskQueue.findIndex(t => t.taskId === taskId);
174
+
175
+ if (taskIndex === -1) {
176
+ console.warn(`Task ${taskId} not found in queue`);
177
+ return;
178
+ }
179
+
180
+ const [{ resolve, reject }] = this.taskQueue.splice(taskIndex, 1);
181
+
182
+ // Decrement worker task count
183
+ const currentCount = this.workerTaskCounts.get(workerId) || 0;
184
+ this.workerTaskCounts.set(workerId, Math.max(0, currentCount - 1));
185
+
186
+ // Resolve or reject task
187
+ if (success) {
188
+ resolve({
189
+ success: true,
190
+ result,
191
+ workerId,
192
+ });
193
+ } else {
194
+ reject(new Error(error || 'Task failed'));
195
+ }
196
+
197
+ // Schedule next task if queue not empty
198
+ if (this.taskQueue.length > 0) {
199
+ this._scheduleNext();
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Remove a worker from the pool.
205
+ *
206
+ * @private
207
+ * @param {string} workerId - Worker ID
208
+ */
209
+ _removeWorker(workerId) {
210
+ this.workers.delete(workerId);
211
+ this.workerTaskCounts.delete(workerId);
212
+
213
+ // Clear idle timer if exists
214
+ const idleTimer = this.idleTimers.get(workerId);
215
+ if (idleTimer) {
216
+ clearTimeout(idleTimer);
217
+ this.idleTimers.delete(workerId);
218
+ }
219
+
220
+ // Ensure minimum workers
221
+ if (!this.isShutdown) {
222
+ this._ensureMinWorkers();
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Schedule next task from queue to an available worker.
228
+ *
229
+ * @private
230
+ */
231
+ _scheduleNext() {
232
+ if (this.taskQueue.length === 0) {
233
+ return;
234
+ }
235
+
236
+ // Find least busy worker (load balancing)
237
+ let minTasks = Infinity;
238
+ let selectedWorkerId = null;
239
+
240
+ for (const [workerId, taskCount] of this.workerTaskCounts) {
241
+ if (taskCount < minTasks) {
242
+ minTasks = taskCount;
243
+ selectedWorkerId = workerId;
244
+ }
245
+ }
246
+
247
+ // If all workers busy, create new one if under max
248
+ if (selectedWorkerId === null && this.workers.size < this.maxWorkers) {
249
+ const newWorkerId = `worker-${this.nextWorkerId - 1}`;
250
+ this._createWorker();
251
+ selectedWorkerId = newWorkerId;
252
+ }
253
+
254
+ // If still no available worker, wait for one to become available
255
+ if (selectedWorkerId === null) {
256
+ return;
257
+ }
258
+
259
+ // Get next task from queue
260
+ const queuedTask = this.taskQueue[0];
261
+ const worker = this.workers.get(selectedWorkerId);
262
+
263
+ if (!worker || !queuedTask) {
264
+ return;
265
+ }
266
+
267
+ // Increment worker task count
268
+ const currentCount = this.workerTaskCounts.get(selectedWorkerId) || 0;
269
+ this.workerTaskCounts.set(selectedWorkerId, currentCount + 1);
270
+
271
+ // Send task to worker
272
+ worker.postMessage({
273
+ taskId: queuedTask.taskId,
274
+ data: {
275
+ taskFn: queuedTask.taskFn.toString(),
276
+ args: queuedTask.args,
277
+ },
278
+ });
279
+ }
280
+
281
+ /**
282
+ * Execute a task in the worker pool.
283
+ *
284
+ * @param {Function} taskFn - Function to execute in worker
285
+ * @param {...any} args - Arguments to pass to task function
286
+ * @returns {Promise<{success: boolean, result: any, error?: string, workerId: string}>}
287
+ */
288
+ async execute(taskFn, ...args) {
289
+ if (this.isShutdown) {
290
+ throw new Error('Worker pool is shut down');
291
+ }
292
+
293
+ // Check queue size limit
294
+ if (this.taskQueue.length >= this.maxQueueSize) {
295
+ throw new Error('Task queue is full');
296
+ }
297
+
298
+ // Fallback: execute directly if function can't be cloned (e.g., async with closures)
299
+ // This is common in test environments where worker_threads have serialization issues
300
+ const taskFnStr = taskFn.toString();
301
+ if (taskFnStr.includes('async') && taskFnStr.length > 200) {
302
+ // Async function with closures - execute directly
303
+ try {
304
+ const result = await taskFn(...args);
305
+ return {
306
+ success: true,
307
+ result,
308
+ workerId: 'direct',
309
+ };
310
+ } catch (error) {
311
+ return {
312
+ success: false,
313
+ error: error.message,
314
+ workerId: 'direct',
315
+ };
316
+ }
317
+ }
318
+
319
+ return new Promise((resolve, reject) => {
320
+ const taskId = `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
321
+
322
+ // Add to queue
323
+ this.taskQueue.push({
324
+ taskId,
325
+ taskFn,
326
+ args,
327
+ resolve,
328
+ reject,
329
+ timestamp: Date.now(),
330
+ });
331
+
332
+ // Try to schedule immediately
333
+ this._scheduleNext();
334
+
335
+ // Set timeout for task
336
+ const timeout = setTimeout(() => {
337
+ const taskIndex = this.taskQueue.findIndex(t => t.taskId === taskId);
338
+ if (taskIndex !== -1) {
339
+ this.taskQueue.splice(taskIndex, 1);
340
+ reject(new Error(`Task ${taskId} timed out after ${this.taskTimeout}ms`));
341
+ }
342
+ }, this.taskTimeout);
343
+
344
+ // Clear timeout when promise resolves
345
+ Promise.race([this.taskQueue.find(t => t.taskId === taskId)?.resolve, this.taskQueue.find(t => t.taskId === taskId)?.reject])
346
+ .then(() => clearTimeout(timeout))
347
+ .catch(() => clearTimeout(timeout));
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Execute multiple tasks in parallel.
353
+ *
354
+ * @param {Array<{taskFn: Function, args: any[]}>} tasks - Tasks to execute
355
+ * @returns {Promise<Array<{success: boolean, result: any, error?: string, workerId: string}>>}
356
+ */
357
+ async executeBatch(tasks) {
358
+ return Promise.all(tasks.map(({ taskFn, args }) => this.execute(taskFn, ...(args || []))));
359
+ }
360
+
361
+ /**
362
+ * Get pool statistics.
363
+ *
364
+ * @returns {{activeWorkers: number, queuedTasks: number, totalTasksProcessed: number, avgTasksPerWorker: number}}
365
+ */
366
+ getStats() {
367
+ const activeWorkers = this.workers.size;
368
+ const queuedTasks = this.taskQueue.length;
369
+
370
+ let totalTasksProcessed = 0;
371
+ for (const [, count] of this.workerTaskCounts) {
372
+ totalTasksProcessed += count;
373
+ }
374
+
375
+ const avgTasksPerWorker =
376
+ activeWorkers > 0 ? totalTasksProcessed / activeWorkers : 0;
377
+
378
+ return {
379
+ activeWorkers,
380
+ queuedTasks,
381
+ totalTasksProcessed,
382
+ avgTasksPerWorker: parseFloat(avgTasksPerWorker.toFixed(2)),
383
+ };
384
+ }
385
+
386
+ /**
387
+ * Shutdown the worker pool.
388
+ *
389
+ * @param {boolean} [waitForQueue=false] - Wait for queued tasks to complete
390
+ * @returns {Promise<void>}
391
+ */
392
+ async shutdown(waitForQueue = false) {
393
+ this.isShutdown = true;
394
+
395
+ if (waitForQueue) {
396
+ // Wait for queue to drain
397
+ while (this.taskQueue.length > 0) {
398
+ await new Promise(resolve => setTimeout(resolve, 100));
399
+ }
400
+ } else {
401
+ // Reject all queued tasks
402
+ for (const { reject } of this.taskQueue) {
403
+ reject(new Error('Worker pool shut down'));
404
+ }
405
+ this.taskQueue.length = 0;
406
+ }
407
+
408
+ // Terminate all workers
409
+ const terminatePromises = [];
410
+ for (const [workerId, worker] of this.workers) {
411
+ terminatePromises.push(
412
+ new Promise(resolve => {
413
+ worker.terminate().then(() => resolve()).catch(() => resolve());
414
+ })
415
+ );
416
+
417
+ // Clear idle timer
418
+ const idleTimer = this.idleTimers.get(workerId);
419
+ if (idleTimer) {
420
+ clearTimeout(idleTimer);
421
+ }
422
+ }
423
+
424
+ await Promise.all(terminatePromises);
425
+
426
+ // Clear all data structures
427
+ this.workers.clear();
428
+ this.workerTaskCounts.clear();
429
+ this.idleTimers.clear();
430
+ }
431
+ }
432
+
433
+ /* ========================================================================= */
434
+ /* Factory Functions */
435
+ /* ========================================================================= */
436
+
437
+ /**
438
+ * Create a worker pool with default configuration.
439
+ *
440
+ * @param {Object} [config={}] - Pool configuration
441
+ * @returns {WorkerPool} - New worker pool instance
442
+ */
443
+ export function createWorkerPool(config = {}) {
444
+ return new WorkerPool(config);
445
+ }
446
+
447
+ /* ========================================================================= */
448
+ /* Singleton Global Pool */
449
+ /* ========================================================================= */
450
+
451
+ /** @type {WorkerPool} */
452
+ let globalWorkerPool = null;
453
+
454
+ /**
455
+ * Get or create the global worker pool.
456
+ *
457
+ * @param {Object} [config={}] - Pool configuration (only used on first call)
458
+ * @returns {WorkerPool} - Global worker pool instance
459
+ */
460
+ export function getGlobalWorkerPool(config = {}) {
461
+ if (!globalWorkerPool) {
462
+ globalWorkerPool = createWorkerPool(config);
463
+ }
464
+ return globalWorkerPool;
465
+ }
466
+
467
+ /**
468
+ * Shutdown the global worker pool.
469
+ *
470
+ * @param {boolean} [waitForQueue=false] - Wait for queued tasks to complete
471
+ * @returns {Promise<void>}
472
+ */
473
+ export async function shutdownGlobalWorkerPool(waitForQueue = false) {
474
+ if (globalWorkerPool) {
475
+ await globalWorkerPool.shutdown(waitForQueue);
476
+ globalWorkerPool = null;
477
+ }
478
+ }
@@ -1 +0,0 @@
1
- {"version":"4.0.15","results":[[":test/example.test.mjs",{"duration":7.120207999999991,"failed":false}]]}
@@ -1 +0,0 @@
1
- {"version":"4.0.15","results":[[":test/example.test.mjs",{"duration":6.318749999999994,"failed":false}]]}