@portel/photon-core 2.8.4 → 2.9.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 (57) hide show
  1. package/dist/base.d.ts +7 -7
  2. package/dist/base.d.ts.map +1 -1
  3. package/dist/base.js +8 -8
  4. package/dist/base.js.map +1 -1
  5. package/dist/collections/Collection.d.ts +2 -2
  6. package/dist/collections/Collection.js +2 -2
  7. package/dist/compiler.js +7 -7
  8. package/dist/compiler.js.map +1 -1
  9. package/dist/config.d.ts +1 -1
  10. package/dist/config.js +1 -1
  11. package/dist/index.d.ts +7 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +16 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/instance-store.d.ts +64 -0
  16. package/dist/instance-store.d.ts.map +1 -0
  17. package/dist/instance-store.js +144 -0
  18. package/dist/instance-store.js.map +1 -0
  19. package/dist/memory.d.ts +2 -2
  20. package/dist/memory.js +2 -2
  21. package/dist/middleware.d.ts +69 -0
  22. package/dist/middleware.d.ts.map +1 -0
  23. package/dist/middleware.js +570 -0
  24. package/dist/middleware.js.map +1 -0
  25. package/dist/schema-extractor.d.ts +111 -1
  26. package/dist/schema-extractor.d.ts.map +1 -1
  27. package/dist/schema-extractor.js +333 -2
  28. package/dist/schema-extractor.js.map +1 -1
  29. package/dist/stateful.d.ts +2 -0
  30. package/dist/stateful.d.ts.map +1 -1
  31. package/dist/stateful.js +2 -0
  32. package/dist/stateful.js.map +1 -1
  33. package/dist/types.d.ts +111 -5
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/types.js.map +1 -1
  36. package/dist/utils/duration.d.ts +24 -0
  37. package/dist/utils/duration.d.ts.map +1 -0
  38. package/dist/utils/duration.js +64 -0
  39. package/dist/utils/duration.js.map +1 -0
  40. package/dist/watcher.d.ts +62 -0
  41. package/dist/watcher.d.ts.map +1 -0
  42. package/dist/watcher.js +270 -0
  43. package/dist/watcher.js.map +1 -0
  44. package/package.json +2 -2
  45. package/src/base.ts +8 -8
  46. package/src/collections/Collection.ts +2 -2
  47. package/src/compiler.ts +7 -7
  48. package/src/config.ts +1 -1
  49. package/src/index.ts +34 -4
  50. package/src/instance-store.ts +155 -0
  51. package/src/memory.ts +2 -2
  52. package/src/middleware.ts +714 -0
  53. package/src/schema-extractor.ts +353 -2
  54. package/src/stateful.ts +4 -0
  55. package/src/types.ts +106 -5
  56. package/src/utils/duration.ts +67 -0
  57. package/src/watcher.ts +317 -0
@@ -0,0 +1,714 @@
1
+ /**
2
+ * Extensible Middleware System
3
+ *
4
+ * Every functional tag (@cached, @timeout, @retryable, etc.) is a MiddlewareDefinition.
5
+ * Custom middleware uses the same API via @use tag + defineMiddleware().
6
+ *
7
+ * Pipeline assembly: declarations sorted by phase, composed inner→outer.
8
+ * Lower phase = outer wrapper (executes first, returns last).
9
+ */
10
+
11
+ import * as crypto from 'crypto';
12
+ import { parseDuration, parseRate } from './utils/duration.js';
13
+
14
+ // ═══════════════════════════════════════════════════════════════════════════════
15
+ // TYPES
16
+ // ═══════════════════════════════════════════════════════════════════════════════
17
+
18
+ export interface MiddlewareContext {
19
+ photon: string;
20
+ tool: string;
21
+ instance: string;
22
+ params: any;
23
+ }
24
+
25
+ export type NextFn = () => Promise<any>;
26
+ export type MiddlewareHandler = (ctx: MiddlewareContext, next: NextFn) => Promise<any>;
27
+
28
+ export interface MiddlewareDefinition<C = Record<string, any>> {
29
+ name: string;
30
+ /** Ordering — lower = outer (executes first). Default: 45 */
31
+ phase?: number;
32
+ /** Parse shorthand sugar like @cached 5m */
33
+ parseShorthand?(value: string): C;
34
+ /** Parse inline {@prop value} config */
35
+ parseConfig?(raw: Record<string, string>): C;
36
+ /** Create a handler from parsed config */
37
+ create(config: C, state: MiddlewareState): MiddlewareHandler;
38
+ }
39
+
40
+ export interface MiddlewareState {
41
+ get<T>(key: string): T | undefined;
42
+ set<T>(key: string, value: T): void;
43
+ delete(key: string): boolean;
44
+ }
45
+
46
+ /** Stored on ExtractedSchema per-tool */
47
+ export interface MiddlewareDeclaration {
48
+ name: string;
49
+ config: Record<string, any>;
50
+ phase: number;
51
+ }
52
+
53
+ // ═══════════════════════════════════════════════════════════════════════════════
54
+ // STATE STORE
55
+ // ═══════════════════════════════════════════════════════════════════════════════
56
+
57
+ export function createStateStore(): MiddlewareState {
58
+ const store = new Map<string, any>();
59
+ return {
60
+ get<T>(key: string): T | undefined {
61
+ return store.get(key);
62
+ },
63
+ set<T>(key: string, value: T): void {
64
+ store.set(key, value);
65
+ },
66
+ delete(key: string): boolean {
67
+ return store.delete(key);
68
+ },
69
+ };
70
+ }
71
+
72
+ // ═══════════════════════════════════════════════════════════════════════════════
73
+ // REGISTRY
74
+ // ═══════════════════════════════════════════════════════════════════════════════
75
+
76
+ export class MiddlewareRegistry {
77
+ private definitions = new Map<string, MiddlewareDefinition>();
78
+
79
+ register(def: MiddlewareDefinition): void {
80
+ this.definitions.set(def.name, def);
81
+ }
82
+
83
+ get(name: string): MiddlewareDefinition | undefined {
84
+ return this.definitions.get(name);
85
+ }
86
+
87
+ has(name: string): boolean {
88
+ return this.definitions.has(name);
89
+ }
90
+
91
+ names(): string[] {
92
+ return [...this.definitions.keys()];
93
+ }
94
+ }
95
+
96
+ // ═══════════════════════════════════════════════════════════════════════════════
97
+ // DEFINE MIDDLEWARE
98
+ // ═══════════════════════════════════════════════════════════════════════════════
99
+
100
+ export function defineMiddleware<C = Record<string, any>>(
101
+ def: MiddlewareDefinition<C>
102
+ ): MiddlewareDefinition<C> {
103
+ if (!def.name) {
104
+ throw new Error('MiddlewareDefinition requires a name');
105
+ }
106
+ if (typeof def.create !== 'function') {
107
+ throw new Error(`MiddlewareDefinition '${def.name}' requires a create function`);
108
+ }
109
+ // Apply default phase
110
+ if (def.phase === undefined) {
111
+ def.phase = 45;
112
+ }
113
+ return Object.freeze(def) as MiddlewareDefinition<C>;
114
+ }
115
+
116
+ // ═══════════════════════════════════════════════════════════════════════════════
117
+ // HELPERS (moved from loader.ts)
118
+ // ═══════════════════════════════════════════════════════════════════════════════
119
+
120
+ /** Hash parameters for cache key */
121
+ export function hashParams(params: any): string {
122
+ try {
123
+ return crypto
124
+ .createHash('sha256')
125
+ .update(JSON.stringify(params || {}))
126
+ .digest('hex')
127
+ .slice(0, 12);
128
+ } catch {
129
+ return 'nohash';
130
+ }
131
+ }
132
+
133
+ /** Get nested value from object by dot path */
134
+ function getNestedValue(obj: any, path: string): any {
135
+ if (!obj || typeof obj !== 'object') return undefined;
136
+ return path.split('.').reduce((o, key) => o?.[key], obj);
137
+ }
138
+
139
+ /** Built-in validators for @validate tag */
140
+ export const BUILT_IN_VALIDATORS: Record<string, (value: any) => boolean> = {
141
+ 'a valid email': (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
142
+ 'a valid url': (v) => typeof v === 'string' && /^https?:\/\/.+/.test(v),
143
+ positive: (v) => typeof v === 'number' && v > 0,
144
+ 'non-negative': (v) => typeof v === 'number' && v >= 0,
145
+ 'non-empty': (v) =>
146
+ v !== null && v !== undefined && v !== '' && (!Array.isArray(v) || v.length > 0),
147
+ 'a valid uuid': (v) =>
148
+ typeof v === 'string' &&
149
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v),
150
+ 'an integer': (v) => typeof v === 'number' && Number.isInteger(v),
151
+ };
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════════
154
+ // BUILT-IN MIDDLEWARE DEFINITIONS
155
+ // ═══════════════════════════════════════════════════════════════════════════════
156
+
157
+ interface CacheEntry {
158
+ result: any;
159
+ timestamp: number;
160
+ }
161
+
162
+ interface ThrottleStateEntry {
163
+ timestamps: number[];
164
+ }
165
+
166
+ interface DebouncePending {
167
+ timer: ReturnType<typeof setTimeout>;
168
+ resolve: (value: any) => void;
169
+ reject: (error: any) => void;
170
+ }
171
+
172
+ interface QueueState {
173
+ running: number;
174
+ queue: Array<{
175
+ fn: () => Promise<any>;
176
+ resolve: (value: any) => void;
177
+ reject: (error: any) => void;
178
+ }>;
179
+ }
180
+
181
+ // --- fallback (phase 3) ---
182
+
183
+ const fallbackMiddleware = defineMiddleware<{ value: any; warn: boolean }>({
184
+ name: 'fallback',
185
+ phase: 3,
186
+ parseShorthand(value: string) {
187
+ return { value: parseFallbackValue(value), warn: false };
188
+ },
189
+ parseConfig(raw) {
190
+ return {
191
+ value: raw.value !== undefined ? parseFallbackValue(raw.value) : null,
192
+ warn: raw.warn === 'true' || raw.warn === 'yes',
193
+ };
194
+ },
195
+ create(config, _state) {
196
+ return async (ctx, next) => {
197
+ try {
198
+ return await next();
199
+ } catch (error) {
200
+ if (config.warn) {
201
+ console.error(
202
+ `[fallback] ${ctx.photon}.${ctx.tool} failed: ${error instanceof Error ? error.message : String(error)} — returning default`
203
+ );
204
+ }
205
+ return config.value;
206
+ }
207
+ };
208
+ },
209
+ });
210
+
211
+ /** Parse a fallback value string into a JS value */
212
+ function parseFallbackValue(raw: string): any {
213
+ const trimmed = raw.trim();
214
+ if (trimmed === 'null') return null;
215
+ if (trimmed === 'undefined') return undefined;
216
+ if (trimmed === 'true') return true;
217
+ if (trimmed === 'false') return false;
218
+ // Try JSON parse (handles [], {}, numbers, quoted strings)
219
+ try {
220
+ return JSON.parse(trimmed);
221
+ } catch {
222
+ // Fall back to raw string
223
+ return trimmed;
224
+ }
225
+ }
226
+
227
+ // --- logged (phase 5) ---
228
+
229
+ const loggedMiddleware = defineMiddleware<{ level: string; tags: string[] }>({
230
+ name: 'logged',
231
+ phase: 5,
232
+ parseShorthand(value: string) {
233
+ const level = value.trim() || 'info';
234
+ return { level, tags: [] };
235
+ },
236
+ parseConfig(raw) {
237
+ return {
238
+ level: raw.level || 'info',
239
+ tags: raw.tags ? raw.tags.split(',').map((t: string) => t.trim()) : [],
240
+ };
241
+ },
242
+ create(config, _state) {
243
+ return async (ctx, next) => {
244
+ const start = Date.now();
245
+ try {
246
+ const result = await next();
247
+ const duration = Date.now() - start;
248
+ const tagStr = config.tags.length > 0 ? ` [${config.tags.join(',')}]` : '';
249
+ console.error(
250
+ `[${config.level}] ${ctx.photon}.${ctx.tool}${tagStr} ${duration}ms`
251
+ );
252
+ return result;
253
+ } catch (error) {
254
+ const duration = Date.now() - start;
255
+ const tagStr = config.tags.length > 0 ? ` [${config.tags.join(',')}]` : '';
256
+ const msg = error instanceof Error ? error.message : String(error);
257
+ console.error(
258
+ `[${config.level}] ${ctx.photon}.${ctx.tool}${tagStr} FAILED ${duration}ms — ${msg}`
259
+ );
260
+ throw error;
261
+ }
262
+ };
263
+ },
264
+ });
265
+
266
+ // --- circuitBreaker (phase 8) ---
267
+
268
+ interface CircuitState {
269
+ failures: number;
270
+ state: 'closed' | 'open' | 'half-open';
271
+ openedAt: number;
272
+ }
273
+
274
+ const circuitBreakerMiddleware = defineMiddleware<{ threshold: number; resetAfterMs: number }>({
275
+ name: 'circuitBreaker',
276
+ phase: 8,
277
+ parseShorthand(value: string) {
278
+ const parts = value.trim().split(/\s+/);
279
+ const threshold = parseInt(parts[0], 10) || 5;
280
+ const resetAfterMs = parts[1] ? parseDuration(parts[1]) : 30_000;
281
+ return { threshold, resetAfterMs };
282
+ },
283
+ parseConfig(raw) {
284
+ return {
285
+ threshold: parseInt(raw.threshold || '5', 10),
286
+ resetAfterMs: raw.resetAfter ? parseDuration(raw.resetAfter) : 30_000,
287
+ };
288
+ },
289
+ create(config, state) {
290
+ return async (ctx, next) => {
291
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
292
+ let circuit = state.get<CircuitState>(key);
293
+ if (!circuit) {
294
+ circuit = { failures: 0, state: 'closed', openedAt: 0 };
295
+ state.set(key, circuit);
296
+ }
297
+
298
+ // OPEN → check if reset period elapsed
299
+ if (circuit.state === 'open') {
300
+ if (Date.now() - circuit.openedAt >= config.resetAfterMs) {
301
+ circuit.state = 'half-open';
302
+ } else {
303
+ const error = new Error(
304
+ `Circuit open: ${ctx.photon}.${ctx.tool} has failed ${config.threshold} consecutive times. Resets in ${Math.ceil((config.resetAfterMs - (Date.now() - circuit.openedAt)) / 1000)}s`
305
+ );
306
+ error.name = 'PhotonCircuitOpenError';
307
+ throw error;
308
+ }
309
+ }
310
+
311
+ try {
312
+ const result = await next();
313
+ // Success → reset circuit
314
+ circuit.failures = 0;
315
+ circuit.state = 'closed';
316
+ return result;
317
+ } catch (error) {
318
+ circuit.failures++;
319
+ if (circuit.failures >= config.threshold) {
320
+ circuit.state = 'open';
321
+ circuit.openedAt = Date.now();
322
+ }
323
+ throw error;
324
+ }
325
+ };
326
+ },
327
+ });
328
+
329
+ // --- throttled (phase 10) ---
330
+
331
+ const throttledMiddleware = defineMiddleware<{ count: number; windowMs: number }>({
332
+ name: 'throttled',
333
+ phase: 10,
334
+ parseShorthand(value: string) {
335
+ return parseRate(value);
336
+ },
337
+ parseConfig(raw) {
338
+ if (raw.rate) {
339
+ return parseRate(raw.rate);
340
+ }
341
+ return {
342
+ count: parseInt(raw.count || '10', 10),
343
+ windowMs: raw.window ? parseDuration(raw.window) : 60_000,
344
+ };
345
+ },
346
+ create(config, state) {
347
+ return async (ctx, next) => {
348
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
349
+ const now = Date.now();
350
+ let entry = state.get<ThrottleStateEntry>(key);
351
+ if (!entry) {
352
+ entry = { timestamps: [] };
353
+ state.set(key, entry);
354
+ }
355
+ // Prune old timestamps
356
+ entry.timestamps = entry.timestamps.filter((t) => now - t < config.windowMs);
357
+ if (entry.timestamps.length >= config.count) {
358
+ const error = new Error(
359
+ `Rate limited: ${ctx.photon}.${ctx.tool} exceeds ${config.count} calls per ${config.windowMs}ms`
360
+ );
361
+ error.name = 'PhotonRateLimitError';
362
+ throw error;
363
+ }
364
+ entry.timestamps.push(now);
365
+ return next();
366
+ };
367
+ },
368
+ });
369
+
370
+ // --- debounced (phase 20) ---
371
+
372
+ const debouncedMiddleware = defineMiddleware<{ delay: number }>({
373
+ name: 'debounced',
374
+ phase: 20,
375
+ parseShorthand(value: string) {
376
+ return { delay: parseDuration(value) };
377
+ },
378
+ parseConfig(raw) {
379
+ return { delay: raw.delay ? parseDuration(raw.delay) : 500 };
380
+ },
381
+ create(config, state) {
382
+ return async (ctx, next) => {
383
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
384
+ const existing = state.get<DebouncePending>(key);
385
+ if (existing) {
386
+ clearTimeout(existing.timer);
387
+ existing.reject(new Error('Debounced: superseded by newer call'));
388
+ }
389
+
390
+ return new Promise((resolve, reject) => {
391
+ const timer = setTimeout(async () => {
392
+ state.delete(key);
393
+ try {
394
+ resolve(await next());
395
+ } catch (error) {
396
+ reject(error);
397
+ }
398
+ }, config.delay);
399
+
400
+ state.set(key, { timer, resolve, reject });
401
+ });
402
+ };
403
+ },
404
+ });
405
+
406
+ // --- cached (phase 30) ---
407
+
408
+ const cachedMiddleware = defineMiddleware<{ ttl: number; key?: string }>({
409
+ name: 'cached',
410
+ phase: 30,
411
+ parseShorthand(value: string) {
412
+ const ttl = parseDuration(value);
413
+ return { ttl: ttl || 300_000 };
414
+ },
415
+ parseConfig(raw) {
416
+ const config: { ttl: number; key?: string } = {
417
+ ttl: raw.ttl ? parseDuration(raw.ttl) : 300_000,
418
+ };
419
+ if (raw.key) config.key = raw.key;
420
+ return config;
421
+ },
422
+ create(config, state) {
423
+ return async (ctx, next) => {
424
+ const paramHash = config.key
425
+ ? getNestedValue(ctx.params, config.key)
426
+ : hashParams(ctx.params);
427
+ const cacheKey = `${ctx.photon}:${ctx.instance}:${ctx.tool}:${paramHash}`;
428
+ const cached = state.get<CacheEntry>(cacheKey);
429
+ if (cached && Date.now() - cached.timestamp < config.ttl) {
430
+ return cached.result;
431
+ }
432
+ const result = await next();
433
+ state.set(cacheKey, { result, timestamp: Date.now() });
434
+ return result;
435
+ };
436
+ },
437
+ });
438
+
439
+ // --- validate (phase 40) ---
440
+
441
+ const validateMiddleware = defineMiddleware<{ validations: Array<{ field: string; rule: string }> }>({
442
+ name: 'validate',
443
+ phase: 40,
444
+ parseConfig(raw) {
445
+ // Config comes pre-parsed from extractValidations
446
+ return { validations: [] };
447
+ },
448
+ create(config, _state) {
449
+ return async (ctx, next) => {
450
+ for (const { field, rule } of config.validations) {
451
+ const value = getNestedValue(ctx.params, field);
452
+ const ruleLower = rule.toLowerCase();
453
+
454
+ let valid = false;
455
+ let builtInMatched = false;
456
+ for (const [pattern, validator] of Object.entries(BUILT_IN_VALIDATORS)) {
457
+ if (ruleLower.includes(pattern)) {
458
+ valid = validator(value);
459
+ builtInMatched = true;
460
+ break;
461
+ }
462
+ }
463
+
464
+ if (!builtInMatched && ruleLower.startsWith('must be ')) {
465
+ valid = value !== null && value !== undefined && value !== '';
466
+ }
467
+
468
+ if (!valid) {
469
+ const error = new Error(
470
+ `Validation failed: ${field} ${rule} (got ${JSON.stringify(value)}) in ${ctx.photon}.${ctx.tool}`
471
+ );
472
+ error.name = 'PhotonValidationError';
473
+ throw error;
474
+ }
475
+ }
476
+ return next();
477
+ };
478
+ },
479
+ });
480
+
481
+ // --- queued (phase 50) ---
482
+
483
+ const queuedMiddleware = defineMiddleware<{ concurrency: number }>({
484
+ name: 'queued',
485
+ phase: 50,
486
+ parseShorthand(value: string) {
487
+ return { concurrency: parseInt(value, 10) || 1 };
488
+ },
489
+ parseConfig(raw) {
490
+ return { concurrency: parseInt(raw.concurrency || '1', 10) };
491
+ },
492
+ create(config, state) {
493
+ return async (ctx, next) => {
494
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
495
+ let queueState = state.get<QueueState>(key);
496
+ if (!queueState) {
497
+ queueState = { running: 0, queue: [] };
498
+ state.set(key, queueState);
499
+ }
500
+
501
+ const tryDequeue = () => {
502
+ const s = state.get<QueueState>(key);
503
+ if (!s) return;
504
+ while (s.running < config.concurrency && s.queue.length > 0) {
505
+ const entry = s.queue.shift()!;
506
+ s.running++;
507
+ entry.fn().then(
508
+ (result) => {
509
+ s.running--;
510
+ entry.resolve(result);
511
+ tryDequeue();
512
+ },
513
+ (error) => {
514
+ s.running--;
515
+ entry.reject(error);
516
+ tryDequeue();
517
+ }
518
+ );
519
+ }
520
+ };
521
+
522
+ if (queueState.running < config.concurrency) {
523
+ queueState.running++;
524
+ return next().finally(() => {
525
+ const s = state.get<QueueState>(key);
526
+ if (s) {
527
+ s.running--;
528
+ tryDequeue();
529
+ }
530
+ });
531
+ }
532
+
533
+ return new Promise((resolve, reject) => {
534
+ queueState!.queue.push({ fn: next, resolve, reject });
535
+ });
536
+ };
537
+ },
538
+ });
539
+
540
+ // --- locked (phase 60) ---
541
+ // Note: The actual lock implementation is injected from the loader since it depends
542
+ // on the daemon's lock manager. This definition provides the structure.
543
+
544
+ const lockedMiddleware = defineMiddleware<{ name: string }>({
545
+ name: 'locked',
546
+ phase: 60,
547
+ parseShorthand(value: string) {
548
+ return { name: value };
549
+ },
550
+ parseConfig(raw) {
551
+ return { name: raw.name || '' };
552
+ },
553
+ create(config, _state) {
554
+ // The actual withLock implementation is injected by the loader.
555
+ // At the photon-core level, we provide a passthrough that the loader overrides.
556
+ return async (ctx, next) => {
557
+ // Loader replaces this handler with one that calls withLockHelper
558
+ // If running without the loader, locks are a no-op
559
+ return next();
560
+ };
561
+ },
562
+ });
563
+
564
+ // --- timeout (phase 70) ---
565
+
566
+ const timeoutMiddleware = defineMiddleware<{ ms: number }>({
567
+ name: 'timeout',
568
+ phase: 70,
569
+ parseShorthand(value: string) {
570
+ return { ms: parseDuration(value) };
571
+ },
572
+ parseConfig(raw) {
573
+ return { ms: raw.ms ? parseDuration(raw.ms) : 30_000 };
574
+ },
575
+ create(config, _state) {
576
+ return async (ctx, next) => {
577
+ return Promise.race([
578
+ next(),
579
+ new Promise<never>((_, reject) => {
580
+ setTimeout(() => {
581
+ const error = new Error(
582
+ `Timeout: ${ctx.photon}.${ctx.tool} did not complete within ${config.ms}ms`
583
+ );
584
+ error.name = 'PhotonTimeoutError';
585
+ reject(error);
586
+ }, config.ms);
587
+ }),
588
+ ]);
589
+ };
590
+ },
591
+ });
592
+
593
+ // --- retryable (phase 80) ---
594
+
595
+ const retryableMiddleware = defineMiddleware<{ count: number; delay: number }>({
596
+ name: 'retryable',
597
+ phase: 80,
598
+ parseShorthand(value: string) {
599
+ const parts = value.trim().split(/\s+/);
600
+ const count = parseInt(parts[0], 10) || 3;
601
+ const delay = parts[1] ? parseDuration(parts[1]) : 1_000;
602
+ return { count, delay };
603
+ },
604
+ parseConfig(raw) {
605
+ return {
606
+ count: parseInt(raw.count || '3', 10),
607
+ delay: raw.delay ? parseDuration(raw.delay) : 1_000,
608
+ };
609
+ },
610
+ create(config, _state) {
611
+ return async (ctx, next) => {
612
+ let lastError: Error | undefined;
613
+ for (let attempt = 0; attempt <= config.count; attempt++) {
614
+ try {
615
+ return await next();
616
+ } catch (error) {
617
+ lastError = error instanceof Error ? error : new Error(String(error));
618
+ if (attempt < config.count) {
619
+ const backoffMs = config.delay * Math.pow(2, attempt);
620
+ await new Promise((r) => setTimeout(r, backoffMs));
621
+ }
622
+ }
623
+ }
624
+ throw lastError;
625
+ };
626
+ },
627
+ });
628
+
629
+ // ═══════════════════════════════════════════════════════════════════════════════
630
+ // GLOBAL BUILT-IN REGISTRY
631
+ // ═══════════════════════════════════════════════════════════════════════════════
632
+
633
+ export const builtinRegistry = new MiddlewareRegistry();
634
+ builtinRegistry.register(fallbackMiddleware);
635
+ builtinRegistry.register(loggedMiddleware);
636
+ builtinRegistry.register(circuitBreakerMiddleware);
637
+ builtinRegistry.register(throttledMiddleware);
638
+ builtinRegistry.register(debouncedMiddleware);
639
+ builtinRegistry.register(cachedMiddleware);
640
+ builtinRegistry.register(validateMiddleware);
641
+ builtinRegistry.register(queuedMiddleware);
642
+ builtinRegistry.register(lockedMiddleware);
643
+ builtinRegistry.register(timeoutMiddleware);
644
+ builtinRegistry.register(retryableMiddleware);
645
+
646
+ // ═══════════════════════════════════════════════════════════════════════════════
647
+ // PIPELINE ASSEMBLY
648
+ // ═══════════════════════════════════════════════════════════════════════════════
649
+
650
+ /**
651
+ * Build a middleware chain from declarations.
652
+ *
653
+ * Sort by phase (ascending), then reverse for wrapping:
654
+ * chain = actualExecution
655
+ * for each declaration (highest phase first → wraps innermost):
656
+ * handler = definition.create(config, stateStore)
657
+ * prev = chain
658
+ * chain = () => handler(ctx, prev)
659
+ *
660
+ * Result: lowest phase runs outermost (executes first).
661
+ */
662
+ export function buildMiddlewareChain(
663
+ execute: () => Promise<any>,
664
+ declarations: MiddlewareDeclaration[],
665
+ registry: MiddlewareRegistry,
666
+ stateStores: Map<string, MiddlewareState>,
667
+ ctx: MiddlewareContext,
668
+ /** Optional overrides for specific middleware (e.g., locked with real lock impl) */
669
+ handlerOverrides?: Map<string, (config: any, state: MiddlewareState) => MiddlewareHandler>,
670
+ ): () => Promise<any> {
671
+ if (!declarations || declarations.length === 0) {
672
+ return execute;
673
+ }
674
+
675
+ // Resolve actual phases: use definition's phase if available (custom middleware
676
+ // defines its own phase, but schema extractor defaults to 45 for unknowns)
677
+ const resolved = declarations.map(decl => {
678
+ const def = registry.get(decl.name);
679
+ const phase = def?.phase !== undefined ? def.phase : decl.phase;
680
+ return { ...decl, phase };
681
+ });
682
+
683
+ // Stable sort by phase (preserves declaration order within same phase)
684
+ const sorted = resolved.sort((a, b) => a.phase - b.phase);
685
+
686
+ // Build chain: iterate reversed sorted list (highest phase = innermost wrapper)
687
+ let chain = execute;
688
+ for (let i = sorted.length - 1; i >= 0; i--) {
689
+ const decl = sorted[i];
690
+ const def = registry.get(decl.name);
691
+ if (!def) {
692
+ // Unknown middleware — skip with warning
693
+ continue;
694
+ }
695
+
696
+ // Get or create state store for this middleware
697
+ let state = stateStores.get(decl.name);
698
+ if (!state) {
699
+ state = createStateStore();
700
+ stateStores.set(decl.name, state);
701
+ }
702
+
703
+ // Check for handler override (e.g., locked middleware needs real lock manager)
704
+ const override = handlerOverrides?.get(decl.name);
705
+ const handler = override
706
+ ? override(decl.config, state)
707
+ : def.create(decl.config, state);
708
+
709
+ const prev = chain;
710
+ chain = () => handler(ctx, prev);
711
+ }
712
+
713
+ return chain;
714
+ }