@portel/photon-core 2.20.0 → 2.22.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.
- package/dist/audit.d.ts.map +1 -1
- package/dist/audit.js +37 -13
- package/dist/audit.js.map +1 -1
- package/dist/memory.d.ts +16 -0
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +21 -0
- package/dist/memory.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +35 -0
- package/dist/middleware.js.map +1 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +11 -0
- package/dist/schema-extractor.js.map +1 -1
- package/dist/validation.d.ts +8 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +7 -6
- package/dist/validation.js.map +1 -1
- package/package.json +2 -2
- package/src/audit.ts +34 -14
- package/src/memory.ts +29 -0
- package/src/middleware.ts +49 -0
- package/src/schema-extractor.ts +12 -0
- package/src/validation.ts +14 -1
package/src/memory.ts
CHANGED
|
@@ -55,6 +55,11 @@ export interface MemoryBackend {
|
|
|
55
55
|
* uses a per-key promise chain.
|
|
56
56
|
*/
|
|
57
57
|
update(namespace: string, key: string, updater: (current: any | null) => any): Promise<any>;
|
|
58
|
+
/**
|
|
59
|
+
* List all key-value pairs in the namespace, optionally filtered by key prefix.
|
|
60
|
+
* Aligns with Deno KV's list() surface for minimal, predictable enumeration.
|
|
61
|
+
*/
|
|
62
|
+
list(namespace: string, prefix?: string): Promise<Array<{ key: string; value: any }>>;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
@@ -185,6 +190,26 @@ export class FileMemoryBackend implements MemoryBackend {
|
|
|
185
190
|
return updated;
|
|
186
191
|
});
|
|
187
192
|
}
|
|
193
|
+
|
|
194
|
+
async list(namespace: string, prefix?: string): Promise<Array<{ key: string; value: any }>> {
|
|
195
|
+
let allKeys: string[];
|
|
196
|
+
try {
|
|
197
|
+
const files = await fs.readdir(namespace);
|
|
198
|
+
allKeys = files.filter(f => f.endsWith('.json') && !f.endsWith('.tmp')).map(f => f.slice(0, -5));
|
|
199
|
+
} catch (error: any) {
|
|
200
|
+
if (error.code === 'ENOENT') return [];
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const filtered = prefix ? allKeys.filter(k => k.startsWith(prefix)) : allKeys;
|
|
205
|
+
const entries = await Promise.all(
|
|
206
|
+
filtered.map(async key => {
|
|
207
|
+
const value = await this.get(namespace, key);
|
|
208
|
+
return { key, value };
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
return entries.filter(e => e.value !== null);
|
|
212
|
+
}
|
|
188
213
|
}
|
|
189
214
|
|
|
190
215
|
// ════════════════════════════════════════════════════════════════════════════════
|
|
@@ -331,6 +356,10 @@ export class MemoryProvider {
|
|
|
331
356
|
return result;
|
|
332
357
|
}
|
|
333
358
|
|
|
359
|
+
async list<T = any>(prefix?: string, scope: MemoryScope = 'photon'): Promise<Array<{ key: string; value: T }>> {
|
|
360
|
+
return this._backend.list(this.ns(scope), prefix) as Promise<Array<{ key: string; value: T }>>;
|
|
361
|
+
}
|
|
362
|
+
|
|
334
363
|
async update<T = any>(
|
|
335
364
|
key: string,
|
|
336
365
|
updater: (current: T | null) => T,
|
package/src/middleware.ts
CHANGED
|
@@ -626,6 +626,54 @@ const retryableMiddleware = defineMiddleware<{ count: number; delay: number }>({
|
|
|
626
626
|
},
|
|
627
627
|
});
|
|
628
628
|
|
|
629
|
+
// --- bulkhead (phase 15) ---
|
|
630
|
+
// Caps concurrent in-flight executions per photon:instance:tool. Unlike
|
|
631
|
+
// @throttled (which rate-limits over a time window), bulkhead protects
|
|
632
|
+
// downstream resources from being overwhelmed by concurrent load.
|
|
633
|
+
// Fast-fails when the cap is hit — callers should back off or queue.
|
|
634
|
+
|
|
635
|
+
interface BulkheadStateEntry {
|
|
636
|
+
inFlight: number;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const bulkheadMiddleware = defineMiddleware<{ maxConcurrent: number }>({
|
|
640
|
+
name: 'bulkhead',
|
|
641
|
+
phase: 15,
|
|
642
|
+
parseShorthand(value: string) {
|
|
643
|
+
return { maxConcurrent: Math.max(1, parseInt(value.trim(), 10) || 1) };
|
|
644
|
+
},
|
|
645
|
+
parseConfig(raw) {
|
|
646
|
+
return {
|
|
647
|
+
maxConcurrent: Math.max(1, parseInt(raw.maxConcurrent || raw.max || '1', 10)),
|
|
648
|
+
};
|
|
649
|
+
},
|
|
650
|
+
create(config, state) {
|
|
651
|
+
return async (ctx, next) => {
|
|
652
|
+
const key = `${ctx.photon}:${ctx.instance}:${ctx.tool}`;
|
|
653
|
+
let entry = state.get<BulkheadStateEntry>(key);
|
|
654
|
+
if (!entry) {
|
|
655
|
+
entry = { inFlight: 0 };
|
|
656
|
+
state.set(key, entry);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (entry.inFlight >= config.maxConcurrent) {
|
|
660
|
+
const error = new Error(
|
|
661
|
+
`Bulkhead full: ${ctx.photon}.${ctx.tool} has ${entry.inFlight} concurrent executions (cap: ${config.maxConcurrent})`
|
|
662
|
+
);
|
|
663
|
+
error.name = 'PhotonBulkheadFullError';
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
entry.inFlight++;
|
|
668
|
+
try {
|
|
669
|
+
return await next();
|
|
670
|
+
} finally {
|
|
671
|
+
entry.inFlight = Math.max(0, entry.inFlight - 1);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
});
|
|
676
|
+
|
|
629
677
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
630
678
|
// GLOBAL BUILT-IN REGISTRY
|
|
631
679
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
@@ -634,6 +682,7 @@ export const builtinRegistry = new MiddlewareRegistry();
|
|
|
634
682
|
builtinRegistry.register(fallbackMiddleware);
|
|
635
683
|
builtinRegistry.register(loggedMiddleware);
|
|
636
684
|
builtinRegistry.register(circuitBreakerMiddleware);
|
|
685
|
+
builtinRegistry.register(bulkheadMiddleware);
|
|
637
686
|
builtinRegistry.register(throttledMiddleware);
|
|
638
687
|
builtinRegistry.register(debouncedMiddleware);
|
|
639
688
|
builtinRegistry.register(cachedMiddleware);
|
package/src/schema-extractor.ts
CHANGED
|
@@ -1180,6 +1180,18 @@ export class SchemaExtractor {
|
|
|
1180
1180
|
declarations.push({ name: 'circuitBreaker', config: circuitBreaker, phase: def?.phase ?? 8 });
|
|
1181
1181
|
}
|
|
1182
1182
|
|
|
1183
|
+
// @bulkhead
|
|
1184
|
+
const bulkheadMatch = jsdocContent.match(/@bulkhead(?:\s+(\d+))?/i);
|
|
1185
|
+
if (bulkheadMatch) {
|
|
1186
|
+
const maxConcurrent = Math.max(1, parseInt(bulkheadMatch[1] || '1', 10));
|
|
1187
|
+
const def = builtinRegistry.get('bulkhead');
|
|
1188
|
+
declarations.push({
|
|
1189
|
+
name: 'bulkhead',
|
|
1190
|
+
config: { maxConcurrent },
|
|
1191
|
+
phase: def?.phase ?? 15,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1183
1195
|
// @cached
|
|
1184
1196
|
const cached = this.extractCached(jsdocContent);
|
|
1185
1197
|
if (cached) {
|
package/src/validation.ts
CHANGED
|
@@ -12,15 +12,27 @@
|
|
|
12
12
|
// ERROR BASE CLASSES
|
|
13
13
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
14
14
|
|
|
15
|
+
export interface PhotonErrorOptions {
|
|
16
|
+
/** Root cause per ECMAScript Error `cause` proposal. Preserved on the error
|
|
17
|
+
* so OTel `recordException` can capture the original stack trace. */
|
|
18
|
+
cause?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
export class PhotonError extends Error {
|
|
22
|
+
public readonly cause?: unknown;
|
|
23
|
+
|
|
16
24
|
constructor(
|
|
17
25
|
message: string,
|
|
18
26
|
public readonly code: string,
|
|
19
27
|
public readonly details?: Record<string, unknown>,
|
|
20
28
|
public readonly suggestion?: string,
|
|
29
|
+
options?: PhotonErrorOptions,
|
|
21
30
|
) {
|
|
22
31
|
super(message);
|
|
23
32
|
this.name = 'PhotonError';
|
|
33
|
+
if (options?.cause !== undefined) {
|
|
34
|
+
this.cause = options.cause;
|
|
35
|
+
}
|
|
24
36
|
Error.captureStackTrace?.(this, this.constructor);
|
|
25
37
|
}
|
|
26
38
|
}
|
|
@@ -30,8 +42,9 @@ export class ValidationError extends PhotonError {
|
|
|
30
42
|
message: string,
|
|
31
43
|
details?: Record<string, unknown>,
|
|
32
44
|
suggestion?: string,
|
|
45
|
+
options?: PhotonErrorOptions,
|
|
33
46
|
) {
|
|
34
|
-
super(message, 'VALIDATION_ERROR', details, suggestion);
|
|
47
|
+
super(message, 'VALIDATION_ERROR', details, suggestion, options);
|
|
35
48
|
this.name = 'ValidationError';
|
|
36
49
|
}
|
|
37
50
|
}
|