keystone-cli 0.7.2 → 1.0.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.
Files changed (104) hide show
  1. package/README.md +486 -54
  2. package/package.json +8 -2
  3. package/src/__fixtures__/index.ts +100 -0
  4. package/src/cli.ts +841 -91
  5. package/src/db/memory-db.ts +35 -1
  6. package/src/db/workflow-db.test.ts +24 -0
  7. package/src/db/workflow-db.ts +484 -14
  8. package/src/expression/evaluator.ts +68 -4
  9. package/src/parser/agent-parser.ts +6 -3
  10. package/src/parser/config-schema.ts +38 -2
  11. package/src/parser/schema.ts +192 -7
  12. package/src/parser/test-schema.ts +29 -0
  13. package/src/parser/workflow-parser.test.ts +54 -0
  14. package/src/parser/workflow-parser.ts +153 -7
  15. package/src/runner/aggregate-error.test.ts +57 -0
  16. package/src/runner/aggregate-error.ts +46 -0
  17. package/src/runner/audit-verification.test.ts +2 -2
  18. package/src/runner/auto-heal.test.ts +1 -1
  19. package/src/runner/blueprint-executor.test.ts +63 -0
  20. package/src/runner/blueprint-executor.ts +157 -0
  21. package/src/runner/concurrency-limit.test.ts +82 -0
  22. package/src/runner/debug-repl.ts +18 -3
  23. package/src/runner/durable-timers.test.ts +200 -0
  24. package/src/runner/engine-executor.test.ts +464 -0
  25. package/src/runner/engine-executor.ts +491 -0
  26. package/src/runner/foreach-executor.ts +30 -12
  27. package/src/runner/llm-adapter.test.ts +282 -5
  28. package/src/runner/llm-adapter.ts +581 -8
  29. package/src/runner/llm-clarification.test.ts +79 -21
  30. package/src/runner/llm-errors.ts +83 -0
  31. package/src/runner/llm-executor.test.ts +258 -219
  32. package/src/runner/llm-executor.ts +226 -29
  33. package/src/runner/mcp-client.ts +70 -3
  34. package/src/runner/mcp-manager.test.ts +52 -52
  35. package/src/runner/mcp-manager.ts +12 -5
  36. package/src/runner/mcp-server.test.ts +117 -78
  37. package/src/runner/mcp-server.ts +13 -4
  38. package/src/runner/optimization-runner.ts +48 -31
  39. package/src/runner/reflexion.test.ts +1 -1
  40. package/src/runner/resource-pool.test.ts +113 -0
  41. package/src/runner/resource-pool.ts +164 -0
  42. package/src/runner/shell-executor.ts +130 -32
  43. package/src/runner/standard-tools-execution.test.ts +39 -0
  44. package/src/runner/standard-tools-integration.test.ts +36 -36
  45. package/src/runner/standard-tools.test.ts +18 -0
  46. package/src/runner/standard-tools.ts +174 -93
  47. package/src/runner/step-executor.test.ts +176 -16
  48. package/src/runner/step-executor.ts +534 -83
  49. package/src/runner/stream-utils.test.ts +14 -0
  50. package/src/runner/subflow-outputs.test.ts +103 -0
  51. package/src/runner/test-harness.ts +161 -0
  52. package/src/runner/tool-integration.test.ts +73 -79
  53. package/src/runner/workflow-runner.test.ts +549 -15
  54. package/src/runner/workflow-runner.ts +1448 -79
  55. package/src/runner/workflow-subflows.test.ts +255 -0
  56. package/src/templates/agents/keystone-architect.md +17 -12
  57. package/src/templates/agents/tester.md +21 -0
  58. package/src/templates/child-rollback.yaml +11 -0
  59. package/src/templates/decompose-implement.yaml +53 -0
  60. package/src/templates/decompose-problem.yaml +159 -0
  61. package/src/templates/decompose-research.yaml +52 -0
  62. package/src/templates/decompose-review.yaml +51 -0
  63. package/src/templates/dev.yaml +134 -0
  64. package/src/templates/engine-example.yaml +33 -0
  65. package/src/templates/fan-out-fan-in.yaml +61 -0
  66. package/src/templates/memory-service.yaml +1 -1
  67. package/src/templates/parent-rollback.yaml +16 -0
  68. package/src/templates/robust-automation.yaml +1 -1
  69. package/src/templates/scaffold-feature.yaml +29 -27
  70. package/src/templates/scaffold-generate.yaml +41 -0
  71. package/src/templates/scaffold-plan.yaml +53 -0
  72. package/src/types/status.ts +3 -0
  73. package/src/ui/dashboard.tsx +4 -3
  74. package/src/utils/assets.macro.ts +36 -0
  75. package/src/utils/auth-manager.ts +585 -8
  76. package/src/utils/blueprint-utils.test.ts +49 -0
  77. package/src/utils/blueprint-utils.ts +80 -0
  78. package/src/utils/circuit-breaker.test.ts +177 -0
  79. package/src/utils/circuit-breaker.ts +160 -0
  80. package/src/utils/config-loader.test.ts +100 -13
  81. package/src/utils/config-loader.ts +44 -17
  82. package/src/utils/constants.ts +62 -0
  83. package/src/utils/error-renderer.test.ts +267 -0
  84. package/src/utils/error-renderer.ts +320 -0
  85. package/src/utils/json-parser.test.ts +4 -0
  86. package/src/utils/json-parser.ts +18 -1
  87. package/src/utils/mermaid.ts +4 -0
  88. package/src/utils/paths.test.ts +46 -0
  89. package/src/utils/paths.ts +70 -0
  90. package/src/utils/process-sandbox.test.ts +128 -0
  91. package/src/utils/process-sandbox.ts +293 -0
  92. package/src/utils/rate-limiter.test.ts +143 -0
  93. package/src/utils/rate-limiter.ts +221 -0
  94. package/src/utils/redactor.test.ts +23 -15
  95. package/src/utils/redactor.ts +65 -25
  96. package/src/utils/resource-loader.test.ts +54 -0
  97. package/src/utils/resource-loader.ts +158 -0
  98. package/src/utils/sandbox.test.ts +69 -4
  99. package/src/utils/sandbox.ts +69 -6
  100. package/src/utils/schema-validator.ts +65 -0
  101. package/src/utils/workflow-registry.test.ts +57 -0
  102. package/src/utils/workflow-registry.ts +45 -25
  103. /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
  104. /package/src/runner/{mcp-client.audit.test.ts → mcp-client-audit.test.ts} +0 -0
@@ -1,10 +1,8 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
1
  import { dirname } from 'node:path';
3
- import { stringify } from 'yaml';
4
- import { parseAgent, resolveAgentPath } from '../parser/agent-parser';
2
+ import { resolveAgentPath } from '../parser/agent-parser';
5
3
  import type { LlmStep, Step, Workflow } from '../parser/schema';
6
- import { extractJson } from '../utils/json-parser';
7
- import { getAdapter } from './llm-adapter';
4
+ import { TIMEOUTS } from '../utils/constants';
5
+ import { ConsoleLogger, type Logger } from '../utils/logger';
8
6
  import { executeLlmStep } from './llm-executor';
9
7
  import { WorkflowRunner } from './workflow-runner';
10
8
 
@@ -13,6 +11,7 @@ export interface OptimizationOptions {
13
11
  targetStepId: string;
14
12
  inputs?: Record<string, unknown>;
15
13
  iterations?: number;
14
+ logger?: Logger;
16
15
  }
17
16
 
18
17
  export class OptimizationRunner {
@@ -21,6 +20,8 @@ export class OptimizationRunner {
21
20
  private targetStepId: string;
22
21
  private iterations: number;
23
22
  private inputs: Record<string, unknown>;
23
+ private logger: Logger;
24
+ private secrets: Record<string, string>;
24
25
 
25
26
  constructor(workflow: Workflow, options: OptimizationOptions) {
26
27
  this.workflow = workflow;
@@ -28,6 +29,18 @@ export class OptimizationRunner {
28
29
  this.targetStepId = options.targetStepId;
29
30
  this.iterations = options.iterations || 5;
30
31
  this.inputs = options.inputs || {};
32
+ this.logger = options.logger || new ConsoleLogger();
33
+ this.secrets = OptimizationRunner.extractSecrets(Bun.env);
34
+ }
35
+
36
+ private static extractSecrets(env: Record<string, string | undefined>): Record<string, string> {
37
+ const secrets: Record<string, string> = {};
38
+ for (const [key, value] of Object.entries(env)) {
39
+ if (typeof value === 'string') {
40
+ secrets[key] = value;
41
+ }
42
+ }
43
+ return secrets;
31
44
  }
32
45
 
33
46
  public async optimize(): Promise<{ bestPrompt: string; bestScore: number }> {
@@ -40,8 +53,8 @@ export class OptimizationRunner {
40
53
  throw new Error(`Target step "${this.targetStepId}" not found or is not an LLM/Shell step`);
41
54
  }
42
55
 
43
- console.log(`\n🚀 Optimizing step: ${this.targetStepId} (${targetStep.type})`);
44
- console.log(`📊 Iterations: ${this.iterations}`);
56
+ this.logger.log(`\n🚀 Optimizing step: ${this.targetStepId} (${targetStep.type})`);
57
+ this.logger.log(`📊 Iterations: ${this.iterations}`);
45
58
 
46
59
  let bestPrompt =
47
60
  targetStep.type === 'llm'
@@ -52,8 +65,8 @@ export class OptimizationRunner {
52
65
  let currentPrompt = bestPrompt;
53
66
 
54
67
  for (let i = 1; i <= this.iterations; i++) {
55
- console.log(`\n--- Iteration ${i}/${this.iterations} ---`);
56
- console.log(
68
+ this.logger.log(`\n--- Iteration ${i}/${this.iterations} ---`);
69
+ this.logger.log(
57
70
  `Current Prompt: ${currentPrompt.substring(0, 100)}${currentPrompt.length > 100 ? '...' : ''}`
58
71
  );
59
72
 
@@ -74,18 +87,19 @@ export class OptimizationRunner {
74
87
  const runner = new WorkflowRunner(modifiedWorkflow, {
75
88
  inputs: this.inputs,
76
89
  workflowDir: dirname(this.workflowPath),
90
+ logger: this.logger,
77
91
  });
78
92
 
79
93
  const outputs = await runner.run();
80
94
 
81
95
  // 2. Evaluate the result
82
96
  const score = await this.evaluate(outputs);
83
- console.log(`Score: ${score}/100`);
97
+ this.logger.log(`Score: ${score}/100`);
84
98
 
85
99
  if (score > bestScore) {
86
100
  bestScore = score;
87
101
  bestPrompt = currentPrompt;
88
- console.log(`✨ New best score: ${bestScore}`);
102
+ this.logger.log(`✨ New best score: ${bestScore}`);
89
103
  }
90
104
 
91
105
  // 3. Suggest next prompt (if not last iteration)
@@ -103,15 +117,18 @@ export class OptimizationRunner {
103
117
  if (!evalConfig) return 0;
104
118
 
105
119
  if (evalConfig.scorer === 'script') {
106
- // Note: getAdapter already imported at top level
107
- const { executeStep } = await import('./step-executor');
120
+ if (!evalConfig.allowInsecure) {
121
+ throw new Error(
122
+ 'Eval script execution is disabled by default. Set eval.allowInsecure: true to enable.'
123
+ );
124
+ }
125
+ const allowSecrets = evalConfig.allowSecrets === true;
108
126
 
109
127
  // Create a context with outputs available
110
128
  const context = {
111
129
  inputs: this.inputs,
112
130
  steps: {},
113
- // biome-ignore lint/suspicious/noExplicitAny: environment access
114
- secrets: Bun.env as any,
131
+ secrets: allowSecrets ? this.secrets : {},
115
132
  env: this.workflow.env,
116
133
  outputs, // Direct access
117
134
  output: outputs, // For convenience
@@ -136,7 +153,9 @@ export class OptimizationRunner {
136
153
  // Note: OptimizationRunner should probably import executeStep
137
154
  const { SafeSandbox } = await import('../utils/sandbox');
138
155
  try {
139
- const result = await SafeSandbox.execute(scriptStep.run, context, { timeout: 5000 });
156
+ const result = await SafeSandbox.execute(scriptStep.run, context, {
157
+ timeout: TIMEOUTS.DEFAULT_SCRIPT_TIMEOUT_MS,
158
+ });
140
159
  if (typeof result === 'object' && result !== null && 'stdout' in result) {
141
160
  // biome-ignore lint/suspicious/noExplicitAny: result typing
142
161
  const match = (result as any).stdout.match(/\d+/);
@@ -150,14 +169,14 @@ export class OptimizationRunner {
150
169
  if (match) return Number.parseInt(match[0], 10);
151
170
  }
152
171
  } catch (e) {
153
- console.error('Eval script failed:', e);
172
+ this.logger.error(`Eval script failed: ${String(e)}`);
154
173
  }
155
174
  return 0;
156
175
  }
157
176
 
158
177
  // LLM Scorer
159
178
  if (!evalConfig.agent || !evalConfig.prompt) {
160
- console.warn('Skipping LLM evaluation: agent or prompt missing');
179
+ this.logger.warn('Skipping LLM evaluation: agent or prompt missing');
161
180
  return 0;
162
181
  }
163
182
 
@@ -168,7 +187,7 @@ export class OptimizationRunner {
168
187
  prompt: `${evalConfig.prompt}\n\nOutputs to evaluate:\n${JSON.stringify(outputs, null, 2)}`,
169
188
  needs: [],
170
189
  maxIterations: 10,
171
- schema: {
190
+ outputSchema: {
172
191
  type: 'object',
173
192
  properties: {
174
193
  score: { type: 'number', minimum: 0, maximum: 100 },
@@ -182,8 +201,7 @@ export class OptimizationRunner {
182
201
  const context = {
183
202
  inputs: this.inputs,
184
203
  steps: {},
185
- // biome-ignore lint/suspicious/noExplicitAny: environment access
186
- secrets: Bun.env as any,
204
+ secrets: this.secrets,
187
205
  env: this.workflow.env,
188
206
  };
189
207
 
@@ -194,7 +212,7 @@ export class OptimizationRunner {
194
212
  async () => {
195
213
  throw new Error('Tools not supported in eval');
196
214
  },
197
- console
215
+ this.logger
198
216
  );
199
217
 
200
218
  if (result.status === 'success' && result.output && typeof result.output === 'object') {
@@ -244,8 +262,7 @@ Return ONLY the new prompt text.`,
244
262
  const context = {
245
263
  inputs: this.inputs,
246
264
  steps: {},
247
- // biome-ignore lint/suspicious/noExplicitAny: environment access
248
- secrets: Bun.env as any,
265
+ secrets: this.secrets,
249
266
  env: this.workflow.env,
250
267
  };
251
268
 
@@ -257,7 +274,7 @@ Return ONLY the new prompt text.`,
257
274
  async () => {
258
275
  throw new Error('Tools not supported in meta-opt');
259
276
  },
260
- console,
277
+ this.logger,
261
278
  undefined,
262
279
  dirname(this.workflowPath) // Pass workflowDir to resolve agent
263
280
  );
@@ -265,7 +282,7 @@ Return ONLY the new prompt text.`,
265
282
  return result.output.trim();
266
283
  }
267
284
  } catch (e) {
268
- console.warn(` ⚠️ Meta-optimizer failed: ${e instanceof Error ? e.message : String(e)}`);
285
+ this.logger.warn(` ⚠️ Meta-optimizer failed: ${e instanceof Error ? e.message : String(e)}`);
269
286
  // Adding a dummy mutation for testing purposes if env var is set
270
287
  if (Bun.env.TEST_OPTIMIZER) {
271
288
  return `${currentPrompt}!`;
@@ -276,28 +293,28 @@ Return ONLY the new prompt text.`,
276
293
  }
277
294
 
278
295
  private async saveBestPrompt(prompt: string): Promise<void> {
279
- console.log(`\n💾 Saving best prompt to ${this.workflowPath}`);
296
+ this.logger.log(`\n💾 Saving best prompt to ${this.workflowPath}`);
280
297
 
281
298
  // We need to be careful here. The prompt might be in the workflow YAML directly,
282
299
  // or it might be in an agent file.
283
300
 
284
301
  const targetStep = this.workflow.steps.find((s) => s.id === this.targetStepId);
285
302
 
286
- console.log(`--- BEST PROMPT/RUN ---\n${prompt}\n-----------------------`);
303
+ this.logger.log(`--- BEST PROMPT/RUN ---\n${prompt}\n-----------------------`);
287
304
 
288
305
  if (targetStep?.type === 'llm') {
289
306
  const agentPath = resolveAgentPath((targetStep as LlmStep).agent, dirname(this.workflowPath));
290
307
  try {
291
308
  // For MVP, we just logged it. Automatic replacement in arbitrary files is risky without robust parsing.
292
309
  // But we can try to warn/notify.
293
- console.log(
310
+ this.logger.log(
294
311
  `To apply this optimization, update the 'systemPrompt' or instruction in: ${agentPath}`
295
312
  );
296
313
  } catch (e) {
297
- console.warn(`Could not locate agent file: ${e}`);
314
+ this.logger.warn(`Could not locate agent file: ${String(e)}`);
298
315
  }
299
316
  } else {
300
- console.log(
317
+ this.logger.log(
301
318
  `To apply this optimization, update the 'run' command for step '${this.targetStepId}' in ${this.workflowPath}`
302
319
  );
303
320
  }
@@ -46,7 +46,7 @@ describe('WorkflowRunner Reflexion', () => {
46
46
 
47
47
  // biome-ignore lint/suspicious/noExplicitAny: Accessing private property for testing
48
48
  const db = (runner as any).db;
49
- await db.createRun(runner.getRunId(), workflow.name, {});
49
+ await db.createRun(runner.runId, workflow.name, {});
50
50
 
51
51
  const spy = jest.spyOn(StepExecutor, 'executeStep');
52
52
 
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it, mock } from 'bun:test';
2
+ import { ConsoleLogger } from '../utils/logger';
3
+ import { ResourcePoolManager } from './resource-pool';
4
+
5
+ describe('ResourcePoolManager', () => {
6
+ const logger = new ConsoleLogger();
7
+
8
+ it('should respect pool limits', async () => {
9
+ const manager = new ResourcePoolManager(logger, {
10
+ pools: { test: 2 },
11
+ });
12
+
13
+ let activeCount = 0;
14
+ const run = async () => {
15
+ const release = await manager.acquire('test');
16
+ activeCount++;
17
+ expect(activeCount).toBeLessThanOrEqual(2);
18
+ await Bun.sleep(50);
19
+ activeCount--;
20
+ release();
21
+ };
22
+
23
+ await Promise.all([run(), run(), run(), run()]);
24
+ });
25
+
26
+ it('should use default limit for unknown pools', async () => {
27
+ const manager = new ResourcePoolManager(logger, {
28
+ defaultLimit: 3,
29
+ });
30
+
31
+ let activeCount = 0;
32
+ const run = async () => {
33
+ const release = await manager.acquire('unknown');
34
+ activeCount++;
35
+ expect(activeCount).toBeLessThanOrEqual(3);
36
+ await Bun.sleep(50);
37
+ activeCount--;
38
+ release();
39
+ };
40
+
41
+ await Promise.all([run(), run(), run(), run(), run()]);
42
+ });
43
+
44
+ it('should handle cancellation via AbortSignal', async () => {
45
+ const manager = new ResourcePoolManager(logger, {
46
+ pools: { test: 1 },
47
+ });
48
+
49
+ // Acquire the only slot
50
+ const release1 = await manager.acquire('test');
51
+
52
+ const controller = new AbortController();
53
+ const pendingAcquisition = manager.acquire('test', { signal: controller.signal });
54
+
55
+ // Cancel after a bit
56
+ setTimeout(() => controller.abort(), 10);
57
+
58
+ await expect(pendingAcquisition).rejects.toThrow('Acquisition aborted');
59
+ release1();
60
+ });
61
+
62
+ it('should respect priority in queue', async () => {
63
+ const manager = new ResourcePoolManager(logger, {
64
+ pools: { test: 1 },
65
+ });
66
+
67
+ const results: number[] = [];
68
+ const release1 = await manager.acquire('test');
69
+
70
+ const p1 = manager.acquire('test', { priority: 1 }).then((r) => {
71
+ results.push(1);
72
+ r();
73
+ });
74
+ const p2 = manager.acquire('test', { priority: 10 }).then((r) => {
75
+ results.push(10);
76
+ r();
77
+ });
78
+ const p3 = manager.acquire('test', { priority: 5 }).then((r) => {
79
+ results.push(5);
80
+ r();
81
+ });
82
+
83
+ release1();
84
+ await Promise.all([p1, p2, p3]);
85
+
86
+ // Priority 10 should run first, then 5, then 1
87
+ expect(results).toEqual([10, 5, 1]);
88
+ });
89
+
90
+ it('should provide metrics', async () => {
91
+ const manager = new ResourcePoolManager(logger, {
92
+ pools: { test: 2 },
93
+ });
94
+
95
+ const release1 = await manager.acquire('test');
96
+ const release2 = await manager.acquire('test');
97
+
98
+ const metrics = manager.getMetrics('test');
99
+ expect(metrics?.active).toBe(2);
100
+ expect(metrics?.queued).toBe(0);
101
+
102
+ const p3 = manager.acquire('test');
103
+ const metrics2 = manager.getMetrics('test');
104
+ expect(metrics2?.queued).toBe(1);
105
+
106
+ release1();
107
+ release2();
108
+ await p3.then((r) => r());
109
+
110
+ const metrics3 = manager.getMetrics('test');
111
+ expect(metrics3?.totalAcquired).toBe(3);
112
+ });
113
+ });
@@ -0,0 +1,164 @@
1
+ import type { Logger } from '../utils/logger.ts';
2
+
3
+ export type ReleaseFunction = () => void;
4
+
5
+ export interface PoolMetrics {
6
+ name: string;
7
+ limit: number;
8
+ active: number;
9
+ queued: number;
10
+ totalAcquired: number;
11
+ totalWaitTimeMs: number;
12
+ }
13
+
14
+ interface QueuedRequest {
15
+ resolve: (release: ReleaseFunction) => void;
16
+ reject: (error: Error) => void;
17
+ signal?: AbortSignal;
18
+ priority: number;
19
+ timestamp: number;
20
+ }
21
+
22
+ export class ResourcePoolManager {
23
+ private pools = new Map<
24
+ string,
25
+ {
26
+ limit: number;
27
+ active: number;
28
+ queue: QueuedRequest[];
29
+ totalAcquired: number;
30
+ totalWaitTimeMs: number;
31
+ }
32
+ >();
33
+ private globalLimit: number;
34
+
35
+ constructor(
36
+ private logger: Logger,
37
+ options: { defaultLimit?: number; pools?: Record<string, number> } = {}
38
+ ) {
39
+ this.globalLimit = options.defaultLimit || 10;
40
+ if (options.pools) {
41
+ for (const [name, limit] of Object.entries(options.pools)) {
42
+ this.pools.set(name, { limit, active: 0, queue: [], totalAcquired: 0, totalWaitTimeMs: 0 });
43
+ }
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Acquire a resource from a pool.
49
+ * If the pool doesn't exist, it uses the global limit.
50
+ */
51
+ async acquire(
52
+ poolName: string,
53
+ options: { priority?: number; signal?: AbortSignal } = {}
54
+ ): Promise<ReleaseFunction> {
55
+ let pool = this.pools.get(poolName);
56
+ if (!pool) {
57
+ // Create a pool for this name if it doesn't exist, using global limit
58
+ pool = {
59
+ limit: this.globalLimit,
60
+ active: 0,
61
+ queue: [],
62
+ totalAcquired: 0,
63
+ totalWaitTimeMs: 0,
64
+ };
65
+ this.pools.set(poolName, pool);
66
+ }
67
+
68
+ if (pool.active < pool.limit && pool.queue.length === 0) {
69
+ pool.active++;
70
+ pool.totalAcquired++;
71
+ return this.createReleaseFn(poolName);
72
+ }
73
+
74
+ // Queue the request
75
+ const timestamp = Date.now();
76
+ const poolRef = pool;
77
+ return new Promise<ReleaseFunction>((resolve, reject) => {
78
+ const request: QueuedRequest = {
79
+ resolve,
80
+ reject,
81
+ signal: options.signal,
82
+ priority: options.priority || 0,
83
+ timestamp,
84
+ };
85
+
86
+ // Add to queue and sort by priority (desc) then timestamp (asc)
87
+ poolRef.queue.push(request);
88
+ poolRef.queue.sort((a, b) => {
89
+ if (b.priority !== a.priority) return b.priority - a.priority;
90
+ return a.timestamp - b.timestamp;
91
+ });
92
+
93
+ // Handle abort signal
94
+ if (options.signal) {
95
+ options.signal.addEventListener(
96
+ 'abort',
97
+ () => {
98
+ const index = poolRef.queue.indexOf(request);
99
+ if (index !== -1) {
100
+ poolRef.queue.splice(index, 1);
101
+ reject(new Error('Acquisition aborted'));
102
+ }
103
+ },
104
+ { once: true }
105
+ );
106
+ }
107
+ });
108
+ }
109
+
110
+ private createReleaseFn(poolName: string): ReleaseFunction {
111
+ let released = false;
112
+ return () => {
113
+ if (released) return;
114
+ released = true;
115
+ this.release(poolName);
116
+ };
117
+ }
118
+
119
+ private release(poolName: string) {
120
+ const pool = this.pools.get(poolName);
121
+ if (!pool) return;
122
+
123
+ pool.active--;
124
+
125
+ // Process queue
126
+ while (pool.active < pool.limit && pool.queue.length > 0) {
127
+ const request = pool.queue.shift();
128
+ if (!request) break;
129
+
130
+ // Skip if signal already aborted
131
+ if (request.signal?.aborted) continue;
132
+
133
+ pool.active++;
134
+ pool.totalAcquired++;
135
+ pool.totalWaitTimeMs += Date.now() - request.timestamp;
136
+ request.resolve(this.createReleaseFn(poolName));
137
+ }
138
+ }
139
+
140
+ getMetrics(poolName: string): PoolMetrics | undefined {
141
+ const pool = this.pools.get(poolName);
142
+ if (!pool) return undefined;
143
+
144
+ return {
145
+ name: poolName,
146
+ limit: pool.limit,
147
+ active: pool.active,
148
+ queued: pool.queue.length,
149
+ totalAcquired: pool.totalAcquired,
150
+ totalWaitTimeMs: pool.totalWaitTimeMs,
151
+ };
152
+ }
153
+
154
+ getAllMetrics(): PoolMetrics[] {
155
+ return Array.from(this.pools.entries()).map(([name, pool]) => ({
156
+ name,
157
+ limit: pool.limit,
158
+ active: pool.active,
159
+ queued: pool.queue.length,
160
+ totalAcquired: pool.totalAcquired,
161
+ totalWaitTimeMs: pool.totalWaitTimeMs,
162
+ }));
163
+ }
164
+ }