@portel/photon-core 2.8.3 → 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 +362 -10
  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 +381 -10
  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,69 @@
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
+ export interface MiddlewareContext {
11
+ photon: string;
12
+ tool: string;
13
+ instance: string;
14
+ params: any;
15
+ }
16
+ export type NextFn = () => Promise<any>;
17
+ export type MiddlewareHandler = (ctx: MiddlewareContext, next: NextFn) => Promise<any>;
18
+ export interface MiddlewareDefinition<C = Record<string, any>> {
19
+ name: string;
20
+ /** Ordering — lower = outer (executes first). Default: 45 */
21
+ phase?: number;
22
+ /** Parse shorthand sugar like @cached 5m */
23
+ parseShorthand?(value: string): C;
24
+ /** Parse inline {@prop value} config */
25
+ parseConfig?(raw: Record<string, string>): C;
26
+ /** Create a handler from parsed config */
27
+ create(config: C, state: MiddlewareState): MiddlewareHandler;
28
+ }
29
+ export interface MiddlewareState {
30
+ get<T>(key: string): T | undefined;
31
+ set<T>(key: string, value: T): void;
32
+ delete(key: string): boolean;
33
+ }
34
+ /** Stored on ExtractedSchema per-tool */
35
+ export interface MiddlewareDeclaration {
36
+ name: string;
37
+ config: Record<string, any>;
38
+ phase: number;
39
+ }
40
+ export declare function createStateStore(): MiddlewareState;
41
+ export declare class MiddlewareRegistry {
42
+ private definitions;
43
+ register(def: MiddlewareDefinition): void;
44
+ get(name: string): MiddlewareDefinition | undefined;
45
+ has(name: string): boolean;
46
+ names(): string[];
47
+ }
48
+ export declare function defineMiddleware<C = Record<string, any>>(def: MiddlewareDefinition<C>): MiddlewareDefinition<C>;
49
+ /** Hash parameters for cache key */
50
+ export declare function hashParams(params: any): string;
51
+ /** Built-in validators for @validate tag */
52
+ export declare const BUILT_IN_VALIDATORS: Record<string, (value: any) => boolean>;
53
+ export declare const builtinRegistry: MiddlewareRegistry;
54
+ /**
55
+ * Build a middleware chain from declarations.
56
+ *
57
+ * Sort by phase (ascending), then reverse for wrapping:
58
+ * chain = actualExecution
59
+ * for each declaration (highest phase first → wraps innermost):
60
+ * handler = definition.create(config, stateStore)
61
+ * prev = chain
62
+ * chain = () => handler(ctx, prev)
63
+ *
64
+ * Result: lowest phase runs outermost (executes first).
65
+ */
66
+ export declare function buildMiddlewareChain(execute: () => Promise<any>, declarations: MiddlewareDeclaration[], registry: MiddlewareRegistry, stateStores: Map<string, MiddlewareState>, ctx: MiddlewareContext,
67
+ /** Optional overrides for specific middleware (e.g., locked with real lock impl) */
68
+ handlerOverrides?: Map<string, (config: any, state: MiddlewareState) => MiddlewareHandler>): () => Promise<any>;
69
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AASH,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,GAAG,CAAC;CACb;AAED,MAAM,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;AACxC,MAAM,MAAM,iBAAiB,GAAG,CAAC,GAAG,EAAE,iBAAiB,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;AAEvF,MAAM,WAAW,oBAAoB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,6DAA6D;IAC7D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,cAAc,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC;IAClC,wCAAwC;IACxC,WAAW,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7C,0CAA0C;IAC1C,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,eAAe,GAAG,iBAAiB,CAAC;CAC9D;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;IACnC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IACpC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAED,yCAAyC;AACzC,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,KAAK,EAAE,MAAM,CAAC;CACf;AAMD,wBAAgB,gBAAgB,IAAI,eAAe,CAalD;AAMD,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,WAAW,CAA2C;IAE9D,QAAQ,CAAC,GAAG,EAAE,oBAAoB,GAAG,IAAI;IAIzC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAInD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,KAAK,IAAI,MAAM,EAAE;CAGlB;AAMD,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EACtD,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC3B,oBAAoB,CAAC,CAAC,CAAC,CAYzB;AAMD,oCAAoC;AACpC,wBAAgB,UAAU,CAAC,MAAM,EAAE,GAAG,GAAG,MAAM,CAU9C;AAQD,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,OAAO,CAWvE,CAAC;AAkeF,eAAO,MAAM,eAAe,oBAA2B,CAAC;AAiBxD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,EAC3B,YAAY,EAAE,qBAAqB,EAAE,EACrC,QAAQ,EAAE,kBAAkB,EAC5B,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,GAAG,EAAE,iBAAiB;AACtB,oFAAoF;AACpF,gBAAgB,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,eAAe,KAAK,iBAAiB,CAAC,GACzF,MAAM,OAAO,CAAC,GAAG,CAAC,CA4CpB"}
@@ -0,0 +1,570 @@
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
+ import * as crypto from 'crypto';
11
+ import { parseDuration, parseRate } from './utils/duration.js';
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+ // STATE STORE
14
+ // ═══════════════════════════════════════════════════════════════════════════════
15
+ export function createStateStore() {
16
+ const store = new Map();
17
+ return {
18
+ get(key) {
19
+ return store.get(key);
20
+ },
21
+ set(key, value) {
22
+ store.set(key, value);
23
+ },
24
+ delete(key) {
25
+ return store.delete(key);
26
+ },
27
+ };
28
+ }
29
+ // ═══════════════════════════════════════════════════════════════════════════════
30
+ // REGISTRY
31
+ // ═══════════════════════════════════════════════════════════════════════════════
32
+ export class MiddlewareRegistry {
33
+ definitions = new Map();
34
+ register(def) {
35
+ this.definitions.set(def.name, def);
36
+ }
37
+ get(name) {
38
+ return this.definitions.get(name);
39
+ }
40
+ has(name) {
41
+ return this.definitions.has(name);
42
+ }
43
+ names() {
44
+ return [...this.definitions.keys()];
45
+ }
46
+ }
47
+ // ═══════════════════════════════════════════════════════════════════════════════
48
+ // DEFINE MIDDLEWARE
49
+ // ═══════════════════════════════════════════════════════════════════════════════
50
+ export function defineMiddleware(def) {
51
+ if (!def.name) {
52
+ throw new Error('MiddlewareDefinition requires a name');
53
+ }
54
+ if (typeof def.create !== 'function') {
55
+ throw new Error(`MiddlewareDefinition '${def.name}' requires a create function`);
56
+ }
57
+ // Apply default phase
58
+ if (def.phase === undefined) {
59
+ def.phase = 45;
60
+ }
61
+ return Object.freeze(def);
62
+ }
63
+ // ═══════════════════════════════════════════════════════════════════════════════
64
+ // HELPERS (moved from loader.ts)
65
+ // ═══════════════════════════════════════════════════════════════════════════════
66
+ /** Hash parameters for cache key */
67
+ export function hashParams(params) {
68
+ try {
69
+ return crypto
70
+ .createHash('sha256')
71
+ .update(JSON.stringify(params || {}))
72
+ .digest('hex')
73
+ .slice(0, 12);
74
+ }
75
+ catch {
76
+ return 'nohash';
77
+ }
78
+ }
79
+ /** Get nested value from object by dot path */
80
+ function getNestedValue(obj, path) {
81
+ if (!obj || typeof obj !== 'object')
82
+ return undefined;
83
+ return path.split('.').reduce((o, key) => o?.[key], obj);
84
+ }
85
+ /** Built-in validators for @validate tag */
86
+ export const BUILT_IN_VALIDATORS = {
87
+ 'a valid email': (v) => typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v),
88
+ 'a valid url': (v) => typeof v === 'string' && /^https?:\/\/.+/.test(v),
89
+ positive: (v) => typeof v === 'number' && v > 0,
90
+ 'non-negative': (v) => typeof v === 'number' && v >= 0,
91
+ 'non-empty': (v) => v !== null && v !== undefined && v !== '' && (!Array.isArray(v) || v.length > 0),
92
+ 'a valid uuid': (v) => typeof v === 'string' &&
93
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v),
94
+ 'an integer': (v) => typeof v === 'number' && Number.isInteger(v),
95
+ };
96
+ // --- fallback (phase 3) ---
97
+ const fallbackMiddleware = defineMiddleware({
98
+ name: 'fallback',
99
+ phase: 3,
100
+ parseShorthand(value) {
101
+ return { value: parseFallbackValue(value), warn: false };
102
+ },
103
+ parseConfig(raw) {
104
+ return {
105
+ value: raw.value !== undefined ? parseFallbackValue(raw.value) : null,
106
+ warn: raw.warn === 'true' || raw.warn === 'yes',
107
+ };
108
+ },
109
+ create(config, _state) {
110
+ return async (ctx, next) => {
111
+ try {
112
+ return await next();
113
+ }
114
+ catch (error) {
115
+ if (config.warn) {
116
+ console.error(`[fallback] ${ctx.photon}.${ctx.tool} failed: ${error instanceof Error ? error.message : String(error)} — returning default`);
117
+ }
118
+ return config.value;
119
+ }
120
+ };
121
+ },
122
+ });
123
+ /** Parse a fallback value string into a JS value */
124
+ function parseFallbackValue(raw) {
125
+ const trimmed = raw.trim();
126
+ if (trimmed === 'null')
127
+ return null;
128
+ if (trimmed === 'undefined')
129
+ return undefined;
130
+ if (trimmed === 'true')
131
+ return true;
132
+ if (trimmed === 'false')
133
+ return false;
134
+ // Try JSON parse (handles [], {}, numbers, quoted strings)
135
+ try {
136
+ return JSON.parse(trimmed);
137
+ }
138
+ catch {
139
+ // Fall back to raw string
140
+ return trimmed;
141
+ }
142
+ }
143
+ // --- logged (phase 5) ---
144
+ const loggedMiddleware = defineMiddleware({
145
+ name: 'logged',
146
+ phase: 5,
147
+ parseShorthand(value) {
148
+ const level = value.trim() || 'info';
149
+ return { level, tags: [] };
150
+ },
151
+ parseConfig(raw) {
152
+ return {
153
+ level: raw.level || 'info',
154
+ tags: raw.tags ? raw.tags.split(',').map((t) => t.trim()) : [],
155
+ };
156
+ },
157
+ create(config, _state) {
158
+ return async (ctx, next) => {
159
+ const start = Date.now();
160
+ try {
161
+ const result = await next();
162
+ const duration = Date.now() - start;
163
+ const tagStr = config.tags.length > 0 ? ` [${config.tags.join(',')}]` : '';
164
+ console.error(`[${config.level}] ${ctx.photon}.${ctx.tool}${tagStr} ${duration}ms`);
165
+ return result;
166
+ }
167
+ catch (error) {
168
+ const duration = Date.now() - start;
169
+ const tagStr = config.tags.length > 0 ? ` [${config.tags.join(',')}]` : '';
170
+ const msg = error instanceof Error ? error.message : String(error);
171
+ console.error(`[${config.level}] ${ctx.photon}.${ctx.tool}${tagStr} FAILED ${duration}ms — ${msg}`);
172
+ throw error;
173
+ }
174
+ };
175
+ },
176
+ });
177
+ const circuitBreakerMiddleware = defineMiddleware({
178
+ name: 'circuitBreaker',
179
+ phase: 8,
180
+ parseShorthand(value) {
181
+ const parts = value.trim().split(/\s+/);
182
+ const threshold = parseInt(parts[0], 10) || 5;
183
+ const resetAfterMs = parts[1] ? parseDuration(parts[1]) : 30_000;
184
+ return { threshold, resetAfterMs };
185
+ },
186
+ parseConfig(raw) {
187
+ return {
188
+ threshold: parseInt(raw.threshold || '5', 10),
189
+ resetAfterMs: raw.resetAfter ? parseDuration(raw.resetAfter) : 30_000,
190
+ };
191
+ },
192
+ create(config, state) {
193
+ return async (ctx, next) => {
194
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
195
+ let circuit = state.get(key);
196
+ if (!circuit) {
197
+ circuit = { failures: 0, state: 'closed', openedAt: 0 };
198
+ state.set(key, circuit);
199
+ }
200
+ // OPEN → check if reset period elapsed
201
+ if (circuit.state === 'open') {
202
+ if (Date.now() - circuit.openedAt >= config.resetAfterMs) {
203
+ circuit.state = 'half-open';
204
+ }
205
+ else {
206
+ const error = new Error(`Circuit open: ${ctx.photon}.${ctx.tool} has failed ${config.threshold} consecutive times. Resets in ${Math.ceil((config.resetAfterMs - (Date.now() - circuit.openedAt)) / 1000)}s`);
207
+ error.name = 'PhotonCircuitOpenError';
208
+ throw error;
209
+ }
210
+ }
211
+ try {
212
+ const result = await next();
213
+ // Success → reset circuit
214
+ circuit.failures = 0;
215
+ circuit.state = 'closed';
216
+ return result;
217
+ }
218
+ catch (error) {
219
+ circuit.failures++;
220
+ if (circuit.failures >= config.threshold) {
221
+ circuit.state = 'open';
222
+ circuit.openedAt = Date.now();
223
+ }
224
+ throw error;
225
+ }
226
+ };
227
+ },
228
+ });
229
+ // --- throttled (phase 10) ---
230
+ const throttledMiddleware = defineMiddleware({
231
+ name: 'throttled',
232
+ phase: 10,
233
+ parseShorthand(value) {
234
+ return parseRate(value);
235
+ },
236
+ parseConfig(raw) {
237
+ if (raw.rate) {
238
+ return parseRate(raw.rate);
239
+ }
240
+ return {
241
+ count: parseInt(raw.count || '10', 10),
242
+ windowMs: raw.window ? parseDuration(raw.window) : 60_000,
243
+ };
244
+ },
245
+ create(config, state) {
246
+ return async (ctx, next) => {
247
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
248
+ const now = Date.now();
249
+ let entry = state.get(key);
250
+ if (!entry) {
251
+ entry = { timestamps: [] };
252
+ state.set(key, entry);
253
+ }
254
+ // Prune old timestamps
255
+ entry.timestamps = entry.timestamps.filter((t) => now - t < config.windowMs);
256
+ if (entry.timestamps.length >= config.count) {
257
+ const error = new Error(`Rate limited: ${ctx.photon}.${ctx.tool} exceeds ${config.count} calls per ${config.windowMs}ms`);
258
+ error.name = 'PhotonRateLimitError';
259
+ throw error;
260
+ }
261
+ entry.timestamps.push(now);
262
+ return next();
263
+ };
264
+ },
265
+ });
266
+ // --- debounced (phase 20) ---
267
+ const debouncedMiddleware = defineMiddleware({
268
+ name: 'debounced',
269
+ phase: 20,
270
+ parseShorthand(value) {
271
+ return { delay: parseDuration(value) };
272
+ },
273
+ parseConfig(raw) {
274
+ return { delay: raw.delay ? parseDuration(raw.delay) : 500 };
275
+ },
276
+ create(config, state) {
277
+ return async (ctx, next) => {
278
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
279
+ const existing = state.get(key);
280
+ if (existing) {
281
+ clearTimeout(existing.timer);
282
+ existing.reject(new Error('Debounced: superseded by newer call'));
283
+ }
284
+ return new Promise((resolve, reject) => {
285
+ const timer = setTimeout(async () => {
286
+ state.delete(key);
287
+ try {
288
+ resolve(await next());
289
+ }
290
+ catch (error) {
291
+ reject(error);
292
+ }
293
+ }, config.delay);
294
+ state.set(key, { timer, resolve, reject });
295
+ });
296
+ };
297
+ },
298
+ });
299
+ // --- cached (phase 30) ---
300
+ const cachedMiddleware = defineMiddleware({
301
+ name: 'cached',
302
+ phase: 30,
303
+ parseShorthand(value) {
304
+ const ttl = parseDuration(value);
305
+ return { ttl: ttl || 300_000 };
306
+ },
307
+ parseConfig(raw) {
308
+ const config = {
309
+ ttl: raw.ttl ? parseDuration(raw.ttl) : 300_000,
310
+ };
311
+ if (raw.key)
312
+ config.key = raw.key;
313
+ return config;
314
+ },
315
+ create(config, state) {
316
+ return async (ctx, next) => {
317
+ const paramHash = config.key
318
+ ? getNestedValue(ctx.params, config.key)
319
+ : hashParams(ctx.params);
320
+ const cacheKey = `${ctx.photon}:${ctx.instance}:${ctx.tool}:${paramHash}`;
321
+ const cached = state.get(cacheKey);
322
+ if (cached && Date.now() - cached.timestamp < config.ttl) {
323
+ return cached.result;
324
+ }
325
+ const result = await next();
326
+ state.set(cacheKey, { result, timestamp: Date.now() });
327
+ return result;
328
+ };
329
+ },
330
+ });
331
+ // --- validate (phase 40) ---
332
+ const validateMiddleware = defineMiddleware({
333
+ name: 'validate',
334
+ phase: 40,
335
+ parseConfig(raw) {
336
+ // Config comes pre-parsed from extractValidations
337
+ return { validations: [] };
338
+ },
339
+ create(config, _state) {
340
+ return async (ctx, next) => {
341
+ for (const { field, rule } of config.validations) {
342
+ const value = getNestedValue(ctx.params, field);
343
+ const ruleLower = rule.toLowerCase();
344
+ let valid = false;
345
+ let builtInMatched = false;
346
+ for (const [pattern, validator] of Object.entries(BUILT_IN_VALIDATORS)) {
347
+ if (ruleLower.includes(pattern)) {
348
+ valid = validator(value);
349
+ builtInMatched = true;
350
+ break;
351
+ }
352
+ }
353
+ if (!builtInMatched && ruleLower.startsWith('must be ')) {
354
+ valid = value !== null && value !== undefined && value !== '';
355
+ }
356
+ if (!valid) {
357
+ const error = new Error(`Validation failed: ${field} ${rule} (got ${JSON.stringify(value)}) in ${ctx.photon}.${ctx.tool}`);
358
+ error.name = 'PhotonValidationError';
359
+ throw error;
360
+ }
361
+ }
362
+ return next();
363
+ };
364
+ },
365
+ });
366
+ // --- queued (phase 50) ---
367
+ const queuedMiddleware = defineMiddleware({
368
+ name: 'queued',
369
+ phase: 50,
370
+ parseShorthand(value) {
371
+ return { concurrency: parseInt(value, 10) || 1 };
372
+ },
373
+ parseConfig(raw) {
374
+ return { concurrency: parseInt(raw.concurrency || '1', 10) };
375
+ },
376
+ create(config, state) {
377
+ return async (ctx, next) => {
378
+ const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
379
+ let queueState = state.get(key);
380
+ if (!queueState) {
381
+ queueState = { running: 0, queue: [] };
382
+ state.set(key, queueState);
383
+ }
384
+ const tryDequeue = () => {
385
+ const s = state.get(key);
386
+ if (!s)
387
+ return;
388
+ while (s.running < config.concurrency && s.queue.length > 0) {
389
+ const entry = s.queue.shift();
390
+ s.running++;
391
+ entry.fn().then((result) => {
392
+ s.running--;
393
+ entry.resolve(result);
394
+ tryDequeue();
395
+ }, (error) => {
396
+ s.running--;
397
+ entry.reject(error);
398
+ tryDequeue();
399
+ });
400
+ }
401
+ };
402
+ if (queueState.running < config.concurrency) {
403
+ queueState.running++;
404
+ return next().finally(() => {
405
+ const s = state.get(key);
406
+ if (s) {
407
+ s.running--;
408
+ tryDequeue();
409
+ }
410
+ });
411
+ }
412
+ return new Promise((resolve, reject) => {
413
+ queueState.queue.push({ fn: next, resolve, reject });
414
+ });
415
+ };
416
+ },
417
+ });
418
+ // --- locked (phase 60) ---
419
+ // Note: The actual lock implementation is injected from the loader since it depends
420
+ // on the daemon's lock manager. This definition provides the structure.
421
+ const lockedMiddleware = defineMiddleware({
422
+ name: 'locked',
423
+ phase: 60,
424
+ parseShorthand(value) {
425
+ return { name: value };
426
+ },
427
+ parseConfig(raw) {
428
+ return { name: raw.name || '' };
429
+ },
430
+ create(config, _state) {
431
+ // The actual withLock implementation is injected by the loader.
432
+ // At the photon-core level, we provide a passthrough that the loader overrides.
433
+ return async (ctx, next) => {
434
+ // Loader replaces this handler with one that calls withLockHelper
435
+ // If running without the loader, locks are a no-op
436
+ return next();
437
+ };
438
+ },
439
+ });
440
+ // --- timeout (phase 70) ---
441
+ const timeoutMiddleware = defineMiddleware({
442
+ name: 'timeout',
443
+ phase: 70,
444
+ parseShorthand(value) {
445
+ return { ms: parseDuration(value) };
446
+ },
447
+ parseConfig(raw) {
448
+ return { ms: raw.ms ? parseDuration(raw.ms) : 30_000 };
449
+ },
450
+ create(config, _state) {
451
+ return async (ctx, next) => {
452
+ return Promise.race([
453
+ next(),
454
+ new Promise((_, reject) => {
455
+ setTimeout(() => {
456
+ const error = new Error(`Timeout: ${ctx.photon}.${ctx.tool} did not complete within ${config.ms}ms`);
457
+ error.name = 'PhotonTimeoutError';
458
+ reject(error);
459
+ }, config.ms);
460
+ }),
461
+ ]);
462
+ };
463
+ },
464
+ });
465
+ // --- retryable (phase 80) ---
466
+ const retryableMiddleware = defineMiddleware({
467
+ name: 'retryable',
468
+ phase: 80,
469
+ parseShorthand(value) {
470
+ const parts = value.trim().split(/\s+/);
471
+ const count = parseInt(parts[0], 10) || 3;
472
+ const delay = parts[1] ? parseDuration(parts[1]) : 1_000;
473
+ return { count, delay };
474
+ },
475
+ parseConfig(raw) {
476
+ return {
477
+ count: parseInt(raw.count || '3', 10),
478
+ delay: raw.delay ? parseDuration(raw.delay) : 1_000,
479
+ };
480
+ },
481
+ create(config, _state) {
482
+ return async (ctx, next) => {
483
+ let lastError;
484
+ for (let attempt = 0; attempt <= config.count; attempt++) {
485
+ try {
486
+ return await next();
487
+ }
488
+ catch (error) {
489
+ lastError = error instanceof Error ? error : new Error(String(error));
490
+ if (attempt < config.count) {
491
+ const backoffMs = config.delay * Math.pow(2, attempt);
492
+ await new Promise((r) => setTimeout(r, backoffMs));
493
+ }
494
+ }
495
+ }
496
+ throw lastError;
497
+ };
498
+ },
499
+ });
500
+ // ═══════════════════════════════════════════════════════════════════════════════
501
+ // GLOBAL BUILT-IN REGISTRY
502
+ // ═══════════════════════════════════════════════════════════════════════════════
503
+ export const builtinRegistry = new MiddlewareRegistry();
504
+ builtinRegistry.register(fallbackMiddleware);
505
+ builtinRegistry.register(loggedMiddleware);
506
+ builtinRegistry.register(circuitBreakerMiddleware);
507
+ builtinRegistry.register(throttledMiddleware);
508
+ builtinRegistry.register(debouncedMiddleware);
509
+ builtinRegistry.register(cachedMiddleware);
510
+ builtinRegistry.register(validateMiddleware);
511
+ builtinRegistry.register(queuedMiddleware);
512
+ builtinRegistry.register(lockedMiddleware);
513
+ builtinRegistry.register(timeoutMiddleware);
514
+ builtinRegistry.register(retryableMiddleware);
515
+ // ═══════════════════════════════════════════════════════════════════════════════
516
+ // PIPELINE ASSEMBLY
517
+ // ═══════════════════════════════════════════════════════════════════════════════
518
+ /**
519
+ * Build a middleware chain from declarations.
520
+ *
521
+ * Sort by phase (ascending), then reverse for wrapping:
522
+ * chain = actualExecution
523
+ * for each declaration (highest phase first → wraps innermost):
524
+ * handler = definition.create(config, stateStore)
525
+ * prev = chain
526
+ * chain = () => handler(ctx, prev)
527
+ *
528
+ * Result: lowest phase runs outermost (executes first).
529
+ */
530
+ export function buildMiddlewareChain(execute, declarations, registry, stateStores, ctx,
531
+ /** Optional overrides for specific middleware (e.g., locked with real lock impl) */
532
+ handlerOverrides) {
533
+ if (!declarations || declarations.length === 0) {
534
+ return execute;
535
+ }
536
+ // Resolve actual phases: use definition's phase if available (custom middleware
537
+ // defines its own phase, but schema extractor defaults to 45 for unknowns)
538
+ const resolved = declarations.map(decl => {
539
+ const def = registry.get(decl.name);
540
+ const phase = def?.phase !== undefined ? def.phase : decl.phase;
541
+ return { ...decl, phase };
542
+ });
543
+ // Stable sort by phase (preserves declaration order within same phase)
544
+ const sorted = resolved.sort((a, b) => a.phase - b.phase);
545
+ // Build chain: iterate reversed sorted list (highest phase = innermost wrapper)
546
+ let chain = execute;
547
+ for (let i = sorted.length - 1; i >= 0; i--) {
548
+ const decl = sorted[i];
549
+ const def = registry.get(decl.name);
550
+ if (!def) {
551
+ // Unknown middleware — skip with warning
552
+ continue;
553
+ }
554
+ // Get or create state store for this middleware
555
+ let state = stateStores.get(decl.name);
556
+ if (!state) {
557
+ state = createStateStore();
558
+ stateStores.set(decl.name, state);
559
+ }
560
+ // Check for handler override (e.g., locked middleware needs real lock manager)
561
+ const override = handlerOverrides?.get(decl.name);
562
+ const handler = override
563
+ ? override(decl.config, state)
564
+ : def.create(decl.config, state);
565
+ const prev = chain;
566
+ chain = () => handler(ctx, prev);
567
+ }
568
+ return chain;
569
+ }
570
+ //# sourceMappingURL=middleware.js.map