@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.
- package/dist/base.d.ts +7 -7
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +8 -8
- package/dist/base.js.map +1 -1
- package/dist/collections/Collection.d.ts +2 -2
- package/dist/collections/Collection.js +2 -2
- package/dist/compiler.js +7 -7
- package/dist/compiler.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/index.d.ts +7 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/instance-store.d.ts +64 -0
- package/dist/instance-store.d.ts.map +1 -0
- package/dist/instance-store.js +144 -0
- package/dist/instance-store.js.map +1 -0
- package/dist/memory.d.ts +2 -2
- package/dist/memory.js +2 -2
- package/dist/middleware.d.ts +69 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +570 -0
- package/dist/middleware.js.map +1 -0
- package/dist/schema-extractor.d.ts +111 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +333 -2
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +2 -0
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +2 -0
- package/dist/stateful.js.map +1 -1
- package/dist/types.d.ts +111 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/duration.d.ts +24 -0
- package/dist/utils/duration.d.ts.map +1 -0
- package/dist/utils/duration.js +64 -0
- package/dist/utils/duration.js.map +1 -0
- package/dist/watcher.d.ts +62 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +270 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +2 -2
- package/src/base.ts +8 -8
- package/src/collections/Collection.ts +2 -2
- package/src/compiler.ts +7 -7
- package/src/config.ts +1 -1
- package/src/index.ts +34 -4
- package/src/instance-store.ts +155 -0
- package/src/memory.ts +2 -2
- package/src/middleware.ts +714 -0
- package/src/schema-extractor.ts +353 -2
- package/src/stateful.ts +4 -0
- package/src/types.ts +106 -5
- package/src/utils/duration.ts +67 -0
- package/src/watcher.ts +317 -0
package/src/schema-extractor.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import * as fs from 'fs/promises';
|
|
12
12
|
import * as ts from 'typescript';
|
|
13
13
|
import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputFormat, YieldInfo, MCPDependency, PhotonDependency, CLIDependency, ResolvedInjection, PhotonAssets, UIAsset, PromptAsset, ResourceAsset, ConfigSchema, ConfigParam } from './types.js';
|
|
14
|
+
import { parseDuration, parseRate } from './utils/duration.js';
|
|
15
|
+
import { builtinRegistry, type MiddlewareDeclaration } from './middleware.js';
|
|
14
16
|
|
|
15
17
|
export interface ExtractedMetadata {
|
|
16
18
|
tools: ExtractedSchema[];
|
|
@@ -182,6 +184,22 @@ export class SchemaExtractor {
|
|
|
182
184
|
const scheduled = this.extractScheduled(jsdoc, methodName);
|
|
183
185
|
const locked = this.extractLocked(jsdoc, methodName);
|
|
184
186
|
|
|
187
|
+
// Functional tags — individual fields kept for backward compat
|
|
188
|
+
const fallback = this.extractFallback(jsdoc);
|
|
189
|
+
const logged = this.extractLogged(jsdoc);
|
|
190
|
+
const circuitBreaker = this.extractCircuitBreaker(jsdoc);
|
|
191
|
+
const cached = this.extractCached(jsdoc);
|
|
192
|
+
const timeout = this.extractTimeout(jsdoc);
|
|
193
|
+
const retryable = this.extractRetryable(jsdoc);
|
|
194
|
+
const throttled = this.extractThrottled(jsdoc);
|
|
195
|
+
const debounced = this.extractDebounced(jsdoc);
|
|
196
|
+
const queued = this.extractQueued(jsdoc);
|
|
197
|
+
const validations = this.extractValidations(jsdoc);
|
|
198
|
+
const deprecated = this.extractDeprecated(jsdoc);
|
|
199
|
+
|
|
200
|
+
// Build unified middleware declarations (single source of truth for runtime)
|
|
201
|
+
const middleware = this.buildMiddlewareDeclarations(jsdoc);
|
|
202
|
+
|
|
185
203
|
// Check for static keyword on the method
|
|
186
204
|
const isStaticMethod = member.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword) || false;
|
|
187
205
|
|
|
@@ -204,6 +222,20 @@ export class SchemaExtractor {
|
|
|
204
222
|
...(webhook !== undefined ? { webhook } : {}),
|
|
205
223
|
...(scheduled ? { scheduled } : {}),
|
|
206
224
|
...(locked !== undefined ? { locked } : {}),
|
|
225
|
+
// Functional tags (individual fields for backward compat)
|
|
226
|
+
...(fallback ? { fallback } : {}),
|
|
227
|
+
...(logged ? { logged } : {}),
|
|
228
|
+
...(circuitBreaker ? { circuitBreaker } : {}),
|
|
229
|
+
...(cached ? { cached } : {}),
|
|
230
|
+
...(timeout ? { timeout } : {}),
|
|
231
|
+
...(retryable ? { retryable } : {}),
|
|
232
|
+
...(throttled ? { throttled } : {}),
|
|
233
|
+
...(debounced ? { debounced } : {}),
|
|
234
|
+
...(queued ? { queued } : {}),
|
|
235
|
+
...(validations && validations.length > 0 ? { validations } : {}),
|
|
236
|
+
...(deprecated !== undefined ? { deprecated } : {}),
|
|
237
|
+
// Unified middleware declarations (new — runtime uses this)
|
|
238
|
+
...(middleware.length > 0 ? { middleware } : {}),
|
|
207
239
|
});
|
|
208
240
|
}
|
|
209
241
|
};
|
|
@@ -712,13 +744,159 @@ export class SchemaExtractor {
|
|
|
712
744
|
return initializer.getText(sourceFile);
|
|
713
745
|
}
|
|
714
746
|
|
|
747
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
748
|
+
// INLINE CONFIG + @use PARSING
|
|
749
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Parse inline {@prop value} config from a string
|
|
753
|
+
* @example parseInlineConfig('{@ttl 5m} {@key params.userId}')
|
|
754
|
+
* → { ttl: '5m', key: 'params.userId' }
|
|
755
|
+
*/
|
|
756
|
+
parseInlineConfig(text: string): Record<string, string> {
|
|
757
|
+
const config: Record<string, string> = {};
|
|
758
|
+
const regex = /\{@(\w+)\s+([^}]+)\}/g;
|
|
759
|
+
let match;
|
|
760
|
+
while ((match = regex.exec(text)) !== null) {
|
|
761
|
+
config[match[1]] = match[2].trim();
|
|
762
|
+
}
|
|
763
|
+
return config;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Extract @use declarations from JSDoc
|
|
768
|
+
* @example extractUseDeclarations('* @use audit {@level info} {@tags api}')
|
|
769
|
+
* → [{ name: 'audit', rawConfig: { level: 'info', tags: 'api' } }]
|
|
770
|
+
*/
|
|
771
|
+
extractUseDeclarations(jsdocContent: string): Array<{ name: string; rawConfig: Record<string, string> }> {
|
|
772
|
+
const declarations: Array<{ name: string; rawConfig: Record<string, string> }> = [];
|
|
773
|
+
const regex = /@use\s+([\w][\w-]*)((?:\s+\{@\w+\s+[^}]+\})*)/g;
|
|
774
|
+
let match;
|
|
775
|
+
while ((match = regex.exec(jsdocContent)) !== null) {
|
|
776
|
+
const name = match[1];
|
|
777
|
+
const configStr = match[2] || '';
|
|
778
|
+
const rawConfig = this.parseInlineConfig(configStr);
|
|
779
|
+
declarations.push({ name, rawConfig });
|
|
780
|
+
}
|
|
781
|
+
return declarations;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* Build middleware declarations from all tags on a method's JSDoc.
|
|
786
|
+
* Converts both sugar tags (@cached 5m) and @use tags into a unified
|
|
787
|
+
* MiddlewareDeclaration[] array.
|
|
788
|
+
*/
|
|
789
|
+
buildMiddlewareDeclarations(jsdocContent: string): MiddlewareDeclaration[] {
|
|
790
|
+
const declarations: MiddlewareDeclaration[] = [];
|
|
791
|
+
|
|
792
|
+
// 1. Extract sugar tags → convert to declarations
|
|
793
|
+
|
|
794
|
+
// @fallback
|
|
795
|
+
const fallback = this.extractFallback(jsdocContent);
|
|
796
|
+
if (fallback) {
|
|
797
|
+
const def = builtinRegistry.get('fallback');
|
|
798
|
+
declarations.push({ name: 'fallback', config: fallback, phase: def?.phase ?? 3 });
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// @logged
|
|
802
|
+
const logged = this.extractLogged(jsdocContent);
|
|
803
|
+
if (logged) {
|
|
804
|
+
const def = builtinRegistry.get('logged');
|
|
805
|
+
declarations.push({ name: 'logged', config: logged, phase: def?.phase ?? 5 });
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// @circuitBreaker
|
|
809
|
+
const circuitBreaker = this.extractCircuitBreaker(jsdocContent);
|
|
810
|
+
if (circuitBreaker) {
|
|
811
|
+
const def = builtinRegistry.get('circuitBreaker');
|
|
812
|
+
declarations.push({ name: 'circuitBreaker', config: circuitBreaker, phase: def?.phase ?? 8 });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// @cached
|
|
816
|
+
const cached = this.extractCached(jsdocContent);
|
|
817
|
+
if (cached) {
|
|
818
|
+
const def = builtinRegistry.get('cached');
|
|
819
|
+
declarations.push({ name: 'cached', config: cached, phase: def?.phase ?? 30 });
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// @timeout
|
|
823
|
+
const timeout = this.extractTimeout(jsdocContent);
|
|
824
|
+
if (timeout) {
|
|
825
|
+
const def = builtinRegistry.get('timeout');
|
|
826
|
+
declarations.push({ name: 'timeout', config: timeout, phase: def?.phase ?? 70 });
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// @retryable
|
|
830
|
+
const retryable = this.extractRetryable(jsdocContent);
|
|
831
|
+
if (retryable) {
|
|
832
|
+
const def = builtinRegistry.get('retryable');
|
|
833
|
+
declarations.push({ name: 'retryable', config: retryable, phase: def?.phase ?? 80 });
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// @throttled
|
|
837
|
+
const throttled = this.extractThrottled(jsdocContent);
|
|
838
|
+
if (throttled) {
|
|
839
|
+
const def = builtinRegistry.get('throttled');
|
|
840
|
+
declarations.push({ name: 'throttled', config: throttled, phase: def?.phase ?? 10 });
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// @debounced
|
|
844
|
+
const debounced = this.extractDebounced(jsdocContent);
|
|
845
|
+
if (debounced) {
|
|
846
|
+
const def = builtinRegistry.get('debounced');
|
|
847
|
+
declarations.push({ name: 'debounced', config: debounced, phase: def?.phase ?? 20 });
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// @queued
|
|
851
|
+
const queued = this.extractQueued(jsdocContent);
|
|
852
|
+
if (queued) {
|
|
853
|
+
const def = builtinRegistry.get('queued');
|
|
854
|
+
declarations.push({ name: 'queued', config: queued, phase: def?.phase ?? 50 });
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// @validate
|
|
858
|
+
const validations = this.extractValidations(jsdocContent);
|
|
859
|
+
if (validations && validations.length > 0) {
|
|
860
|
+
const def = builtinRegistry.get('validate');
|
|
861
|
+
declarations.push({ name: 'validate', config: { validations }, phase: def?.phase ?? 40 });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// @locked (handled as middleware when it appears as a functional tag)
|
|
865
|
+
const lockedMatch = jsdocContent.match(/@locked(?:\s+(\w[\w\-:]*))?/i);
|
|
866
|
+
if (lockedMatch) {
|
|
867
|
+
const lockName = lockedMatch[1]?.trim() || '';
|
|
868
|
+
const def = builtinRegistry.get('locked');
|
|
869
|
+
declarations.push({ name: 'locked', config: { name: lockName }, phase: def?.phase ?? 60 });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// 2. Extract @use declarations
|
|
873
|
+
const useDecls = this.extractUseDeclarations(jsdocContent);
|
|
874
|
+
for (const { name, rawConfig } of useDecls) {
|
|
875
|
+
// Check if this is a built-in (allow @use cached {@ttl 5m} syntax)
|
|
876
|
+
const def = builtinRegistry.get(name);
|
|
877
|
+
if (def) {
|
|
878
|
+
// Use parseConfig if available, otherwise pass raw
|
|
879
|
+
const config = def.parseConfig ? def.parseConfig(rawConfig) : rawConfig;
|
|
880
|
+
// Don't duplicate if sugar tag already added this middleware
|
|
881
|
+
if (!declarations.some(d => d.name === name)) {
|
|
882
|
+
declarations.push({ name, config, phase: def.phase ?? 45 });
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
// Custom middleware — phase defaults to 45
|
|
886
|
+
declarations.push({ name, config: rawConfig, phase: 45 });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
return declarations;
|
|
891
|
+
}
|
|
892
|
+
|
|
715
893
|
/**
|
|
716
894
|
* Extract main description from JSDoc comment
|
|
717
895
|
*/
|
|
718
896
|
private extractDescription(jsdocContent: string): string {
|
|
719
897
|
// Split by @tags that appear at start of a JSDoc line (after optional * prefix)
|
|
720
898
|
// This avoids matching @tag references inline in description text
|
|
721
|
-
const beforeTags = jsdocContent.split(/(?:^|\n)\s*\*?\s*@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility)\b/)[0];
|
|
899
|
+
const beforeTags = jsdocContent.split(/(?:^|\n)\s*\*?\s*@(?:param|example|returns?|throws?|see|since|deprecated|version|author|license|ui|icon|format|stateful|autorun|async|webhook|cron|scheduled|locked|fallback|logged|circuitBreaker|cached|timeout|retryable|throttled|debounced|queued|validate|use|Template|Static|mcp|photon|cli|tags|dependencies|csp|visibility)\b/)[0];
|
|
722
900
|
|
|
723
901
|
// Remove leading * from each line and trim
|
|
724
902
|
const lines = beforeTags
|
|
@@ -1237,6 +1415,153 @@ export class SchemaExtractor {
|
|
|
1237
1415
|
return undefined;
|
|
1238
1416
|
}
|
|
1239
1417
|
|
|
1418
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1419
|
+
// FUNCTIONAL TAG EXTRACTION
|
|
1420
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* Extract logging config from @logged tag
|
|
1424
|
+
* - @logged → info level
|
|
1425
|
+
* - @logged debug → debug level
|
|
1426
|
+
*/
|
|
1427
|
+
private extractLogged(jsdocContent: string): { level: string } | undefined {
|
|
1428
|
+
const match = jsdocContent.match(/@logged(?:\s+(\w+))?/i);
|
|
1429
|
+
if (!match) return undefined;
|
|
1430
|
+
return { level: match[1]?.trim() || 'info' };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
/**
|
|
1434
|
+
* Extract fallback value from @fallback tag
|
|
1435
|
+
* - @fallback [] → empty array on error
|
|
1436
|
+
* - @fallback {} → empty object on error
|
|
1437
|
+
* - @fallback null → null on error
|
|
1438
|
+
* - @fallback 0 → zero on error
|
|
1439
|
+
*/
|
|
1440
|
+
private extractFallback(jsdocContent: string): { value: string } | undefined {
|
|
1441
|
+
const match = jsdocContent.match(/@fallback\s+(.+?)(?:\s*$|\s*\n|\s*\*)/m);
|
|
1442
|
+
if (!match) return undefined;
|
|
1443
|
+
return { value: match[1].trim() };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Extract circuit breaker configuration from @circuitBreaker tag
|
|
1448
|
+
* - @circuitBreaker 5 30s → 5 failures, 30s reset
|
|
1449
|
+
* - @circuitBreaker 3 1m → 3 failures, 1 minute reset
|
|
1450
|
+
*/
|
|
1451
|
+
private extractCircuitBreaker(jsdocContent: string): { threshold: number; resetAfter: string } | undefined {
|
|
1452
|
+
const match = jsdocContent.match(/@circuitBreaker\s+(\d+)\s+(\d+(?:ms|s|sec|m|min|h|hr|d|day))/i);
|
|
1453
|
+
if (!match) return undefined;
|
|
1454
|
+
return { threshold: parseInt(match[1], 10), resetAfter: match[2] };
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Extract cache configuration from @cached tag
|
|
1459
|
+
* - @cached → default 5m TTL
|
|
1460
|
+
* - @cached 30s → 30 second TTL
|
|
1461
|
+
* - @cached 1h → 1 hour TTL
|
|
1462
|
+
*/
|
|
1463
|
+
private extractCached(jsdocContent: string): { ttl: number } | undefined {
|
|
1464
|
+
const match = jsdocContent.match(/@cached(?:\s+(\d+(?:ms|s|sec|m|min|h|hr|d|day)))?/i);
|
|
1465
|
+
if (!match) return undefined;
|
|
1466
|
+
const ttl = match[1] ? parseDuration(match[1]) : 300_000; // default 5m
|
|
1467
|
+
return { ttl };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Extract timeout configuration from @timeout tag
|
|
1472
|
+
* - @timeout 30s → 30 second timeout
|
|
1473
|
+
* - @timeout 5m → 5 minute timeout
|
|
1474
|
+
*/
|
|
1475
|
+
private extractTimeout(jsdocContent: string): { ms: number } | undefined {
|
|
1476
|
+
const match = jsdocContent.match(/@timeout\s+(\d+(?:ms|s|sec|m|min|h|hr|d|day))/i);
|
|
1477
|
+
if (!match) return undefined;
|
|
1478
|
+
return { ms: parseDuration(match[1]) };
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
/**
|
|
1482
|
+
* Extract retry configuration from @retryable tag
|
|
1483
|
+
* - @retryable → default 3 retries, 1s delay
|
|
1484
|
+
* - @retryable 5 → 5 retries, 1s delay
|
|
1485
|
+
* - @retryable 3 2s → 3 retries, 2s delay
|
|
1486
|
+
*/
|
|
1487
|
+
private extractRetryable(jsdocContent: string): { count: number; delay: number } | undefined {
|
|
1488
|
+
const match = jsdocContent.match(/@retryable(?:\s+(\d+))?(?:\s+(\d+(?:ms|s|sec|m|min|h|hr|d|day)))?/i);
|
|
1489
|
+
if (!match) return undefined;
|
|
1490
|
+
const count = match[1] ? parseInt(match[1], 10) : 3;
|
|
1491
|
+
const delay = match[2] ? parseDuration(match[2]) : 1_000;
|
|
1492
|
+
return { count, delay };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Extract throttle configuration from @throttled tag
|
|
1497
|
+
* - @throttled 10/min → 10 calls per minute
|
|
1498
|
+
* - @throttled 100/h → 100 calls per hour
|
|
1499
|
+
*/
|
|
1500
|
+
private extractThrottled(jsdocContent: string): { count: number; windowMs: number } | undefined {
|
|
1501
|
+
const match = jsdocContent.match(/@throttled\s+(\d+\/(?:s|sec|m|min|h|hr|d|day))/i);
|
|
1502
|
+
if (!match) return undefined;
|
|
1503
|
+
return parseRate(match[1]);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
/**
|
|
1507
|
+
* Extract debounce configuration from @debounced tag
|
|
1508
|
+
* - @debounced → default 500ms
|
|
1509
|
+
* - @debounced 200ms → 200ms delay
|
|
1510
|
+
* - @debounced 1s → 1 second delay
|
|
1511
|
+
*/
|
|
1512
|
+
private extractDebounced(jsdocContent: string): { delay: number } | undefined {
|
|
1513
|
+
const match = jsdocContent.match(/@debounced(?:\s+(\d+(?:ms|s|sec|m|min|h|hr|d|day)))?/i);
|
|
1514
|
+
if (!match) return undefined;
|
|
1515
|
+
const delay = match[1] ? parseDuration(match[1]) : 500;
|
|
1516
|
+
return { delay };
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Extract queue configuration from @queued tag
|
|
1521
|
+
* - @queued → default concurrency 1
|
|
1522
|
+
* - @queued 3 → concurrency 3
|
|
1523
|
+
*/
|
|
1524
|
+
private extractQueued(jsdocContent: string): { concurrency: number } | undefined {
|
|
1525
|
+
const match = jsdocContent.match(/@queued(?:\s+(\d+))?/i);
|
|
1526
|
+
if (!match) return undefined;
|
|
1527
|
+
const concurrency = match[1] ? parseInt(match[1], 10) : 1;
|
|
1528
|
+
return { concurrency };
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Extract validation rules from @validate tags
|
|
1533
|
+
* - @validate params.email must be a valid email
|
|
1534
|
+
* - @validate params.amount must be positive
|
|
1535
|
+
*/
|
|
1536
|
+
private extractValidations(jsdocContent: string): Array<{ field: string; rule: string }> | undefined {
|
|
1537
|
+
const validations: Array<{ field: string; rule: string }> = [];
|
|
1538
|
+
const regex = /@validate\s+([\w.]+)\s+(.+)/g;
|
|
1539
|
+
let match;
|
|
1540
|
+
while ((match = regex.exec(jsdocContent)) !== null) {
|
|
1541
|
+
validations.push({
|
|
1542
|
+
field: match[1].replace(/^params\./, ''), // strip params. prefix
|
|
1543
|
+
rule: match[2].trim().replace(/\*\/$/, '').trim(), // strip JSDoc closing
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
return validations.length > 0 ? validations : undefined;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Extract deprecation notice from @deprecated tag (class-level, not param-level)
|
|
1551
|
+
* - @deprecated → true
|
|
1552
|
+
* - @deprecated Use addV2 instead → "Use addV2 instead"
|
|
1553
|
+
*
|
|
1554
|
+
* Note: Only matches @deprecated at the start of a JSDoc line (after * prefix),
|
|
1555
|
+
* NOT inside {@deprecated} inline tags (those are param-level).
|
|
1556
|
+
*/
|
|
1557
|
+
private extractDeprecated(jsdocContent: string): string | true | undefined {
|
|
1558
|
+
// Match @deprecated at start of line (with optional * prefix), not inside {}
|
|
1559
|
+
const match = jsdocContent.match(/(?:^|\n)\s*\*?\s*@deprecated(?:\s+([^\n*]+))?/i);
|
|
1560
|
+
if (!match) return undefined;
|
|
1561
|
+
const message = match[1]?.trim();
|
|
1562
|
+
return message || true;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1240
1565
|
/**
|
|
1241
1566
|
* Extract URI pattern from @Static tag
|
|
1242
1567
|
* Example: @Static github://repos/{owner}/{repo}/readme
|
|
@@ -1663,7 +1988,7 @@ export class SchemaExtractor {
|
|
|
1663
1988
|
* * @prompt system ./prompts/system.md
|
|
1664
1989
|
* * @resource config ./resources/config.json
|
|
1665
1990
|
* *\/
|
|
1666
|
-
* export default class MyPhoton extends
|
|
1991
|
+
* export default class MyPhoton extends Photon { ... }
|
|
1667
1992
|
* ```
|
|
1668
1993
|
*/
|
|
1669
1994
|
extractAssets(source: string, assetFolder?: string): PhotonAssets {
|
|
@@ -1828,3 +2153,29 @@ export class SchemaExtractor {
|
|
|
1828
2153
|
return mimeTypes[ext] || 'application/octet-stream';
|
|
1829
2154
|
}
|
|
1830
2155
|
}
|
|
2156
|
+
|
|
2157
|
+
/**
|
|
2158
|
+
* Capability types that can be auto-detected from source code
|
|
2159
|
+
*/
|
|
2160
|
+
export type PhotonCapability = 'emit' | 'memory' | 'call' | 'mcp' | 'lock' | 'instanceMeta' | 'allInstances';
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* Detect capabilities used by a Photon from its source code.
|
|
2164
|
+
*
|
|
2165
|
+
* Scans for `this.emit(`, `this.memory`, `this.call(`, etc. patterns
|
|
2166
|
+
* and returns the set of capabilities that the runtime should inject.
|
|
2167
|
+
*
|
|
2168
|
+
* This enables plain classes (no extends Photon) to use all framework
|
|
2169
|
+
* features — the loader detects usage and injects automatically.
|
|
2170
|
+
*/
|
|
2171
|
+
export function detectCapabilities(source: string): Set<PhotonCapability> {
|
|
2172
|
+
const caps = new Set<PhotonCapability>();
|
|
2173
|
+
if (/this\.emit\s*\(/.test(source)) caps.add('emit');
|
|
2174
|
+
if (/this\.memory\b/.test(source)) caps.add('memory');
|
|
2175
|
+
if (/this\.call\s*\(/.test(source)) caps.add('call');
|
|
2176
|
+
if (/this\.mcp\s*\(/.test(source)) caps.add('mcp');
|
|
2177
|
+
if (/this\.withLock\s*\(/.test(source)) caps.add('lock');
|
|
2178
|
+
if (/this\.instanceMeta\b/.test(source)) caps.add('instanceMeta');
|
|
2179
|
+
if (/this\.allInstances\s*\(/.test(source)) caps.add('allInstances');
|
|
2180
|
+
return caps;
|
|
2181
|
+
}
|
package/src/stateful.ts
CHANGED
|
@@ -690,6 +690,8 @@ export interface MaybeStatefulResult<T> {
|
|
|
690
690
|
result?: T;
|
|
691
691
|
/** Error message (if failed) */
|
|
692
692
|
error?: string;
|
|
693
|
+
/** Original error object (preserves .name for typed errors like PhotonTimeoutError) */
|
|
694
|
+
originalError?: Error;
|
|
693
695
|
/** Run ID (only if stateful - checkpoint was yielded) */
|
|
694
696
|
runId?: string;
|
|
695
697
|
/** Was this a stateful workflow? */
|
|
@@ -821,6 +823,7 @@ export async function maybeStatefulExecute<T>(
|
|
|
821
823
|
} catch (error: any) {
|
|
822
824
|
return {
|
|
823
825
|
error: error.message,
|
|
826
|
+
originalError: error instanceof Error ? error : undefined,
|
|
824
827
|
isStateful: false,
|
|
825
828
|
resumed: false,
|
|
826
829
|
checkpointsCompleted: 0,
|
|
@@ -947,6 +950,7 @@ export async function maybeStatefulExecute<T>(
|
|
|
947
950
|
|
|
948
951
|
return {
|
|
949
952
|
error: error.message,
|
|
953
|
+
originalError: error instanceof Error ? error : undefined,
|
|
950
954
|
runId,
|
|
951
955
|
isStateful,
|
|
952
956
|
resumed: false,
|
package/src/types.ts
CHANGED
|
@@ -96,9 +96,104 @@ export interface ExtractedSchema {
|
|
|
96
96
|
* - string: custom lock name (e.g., @locked board:write)
|
|
97
97
|
*/
|
|
98
98
|
locked?: boolean | string;
|
|
99
|
+
|
|
100
|
+
// ═══ FUNCTIONAL TAGS ═══
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Logging configuration (from @logged tag)
|
|
104
|
+
* Auto-logs method execution with timing and error details
|
|
105
|
+
* @example @logged or @logged debug
|
|
106
|
+
*/
|
|
107
|
+
logged?: { level: string };
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fallback value on error (from @fallback tag)
|
|
111
|
+
* Returns this value instead of throwing when the method fails
|
|
112
|
+
* @example @fallback []
|
|
113
|
+
*/
|
|
114
|
+
fallback?: { value: string };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Circuit breaker configuration (from @circuitBreaker tag)
|
|
118
|
+
* Fast-rejects after N consecutive failures, probes after reset period
|
|
119
|
+
* @example @circuitBreaker 5 30s
|
|
120
|
+
*/
|
|
121
|
+
circuitBreaker?: { threshold: number; resetAfter: string };
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Cache configuration (from @cached tag)
|
|
125
|
+
* Memoize return value for the specified TTL
|
|
126
|
+
* @example @cached 5m
|
|
127
|
+
*/
|
|
128
|
+
cached?: { ttl: number };
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execution timeout in ms (from @timeout tag)
|
|
132
|
+
* Rejects with TimeoutError if method doesn't resolve in time
|
|
133
|
+
* @example @timeout 30s
|
|
134
|
+
*/
|
|
135
|
+
timeout?: { ms: number };
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Auto-retry configuration (from @retryable tag)
|
|
139
|
+
* Retries on failure with delay between attempts
|
|
140
|
+
* @example @retryable 3 1s
|
|
141
|
+
*/
|
|
142
|
+
retryable?: { count: number; delay: number };
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Rate limiting configuration (from @throttled tag)
|
|
146
|
+
* At most N calls per time window
|
|
147
|
+
* @example @throttled 10/min
|
|
148
|
+
*/
|
|
149
|
+
throttled?: { count: number; windowMs: number };
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Debounce configuration (from @debounced tag)
|
|
153
|
+
* Collapses rapid repeated calls — only the last one executes
|
|
154
|
+
* @example @debounced 500ms
|
|
155
|
+
*/
|
|
156
|
+
debounced?: { delay: number };
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Queue configuration (from @queued tag)
|
|
160
|
+
* At most N concurrent executions, excess calls are queued
|
|
161
|
+
* @example @queued 1
|
|
162
|
+
*/
|
|
163
|
+
queued?: { concurrency: number };
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Custom validation rules (from @validate tag)
|
|
167
|
+
* Runtime validation beyond JSON Schema
|
|
168
|
+
* @example @validate params.email must be a valid email
|
|
169
|
+
*/
|
|
170
|
+
validations?: Array<{ field: string; rule: string }>;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Deprecation notice (from @deprecated tag)
|
|
174
|
+
* Tool still works but shows warnings everywhere
|
|
175
|
+
* @example @deprecated Use addV2 instead
|
|
176
|
+
*/
|
|
177
|
+
deprecated?: string | true;
|
|
178
|
+
|
|
179
|
+
// ═══ MIDDLEWARE PIPELINE ═══
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unified middleware declarations (from @use tags and sugar tags)
|
|
183
|
+
* Single source of truth for the runtime middleware pipeline.
|
|
184
|
+
* Replaces individual functional tag fields for execution purposes.
|
|
185
|
+
*/
|
|
186
|
+
middleware?: Array<{
|
|
187
|
+
name: string;
|
|
188
|
+
config: Record<string, any>;
|
|
189
|
+
phase: number;
|
|
190
|
+
}>;
|
|
191
|
+
|
|
192
|
+
/** When true, method uses individual params instead of a single params object */
|
|
193
|
+
simpleParams?: boolean;
|
|
99
194
|
}
|
|
100
195
|
|
|
101
|
-
export interface
|
|
196
|
+
export interface PhotonClass {
|
|
102
197
|
name: string;
|
|
103
198
|
description?: string;
|
|
104
199
|
tools: PhotonTool[];
|
|
@@ -107,6 +202,9 @@ export interface PhotonMCPClass {
|
|
|
107
202
|
classConstructor?: Record<string, Function>;
|
|
108
203
|
}
|
|
109
204
|
|
|
205
|
+
/** @deprecated Use PhotonClass instead */
|
|
206
|
+
export type PhotonMCPClass = PhotonClass;
|
|
207
|
+
|
|
110
208
|
export interface ConstructorParam {
|
|
111
209
|
name: string;
|
|
112
210
|
type: string;
|
|
@@ -154,7 +252,7 @@ export interface ResolvedInjection {
|
|
|
154
252
|
* * @mcp github anthropics/mcp-server-github
|
|
155
253
|
* * @mcp fs npm:@modelcontextprotocol/server-filesystem
|
|
156
254
|
* *\/
|
|
157
|
-
* export default class MyPhoton extends
|
|
255
|
+
* export default class MyPhoton extends Photon {
|
|
158
256
|
* async doSomething() {
|
|
159
257
|
* const issues = await this.github.list_issues({ repo: 'owner/repo' });
|
|
160
258
|
* }
|
|
@@ -380,9 +478,9 @@ export interface StaticInfo {
|
|
|
380
478
|
}
|
|
381
479
|
|
|
382
480
|
/**
|
|
383
|
-
* Extended
|
|
481
|
+
* Extended PhotonClass with templates and statics
|
|
384
482
|
*/
|
|
385
|
-
export interface
|
|
483
|
+
export interface PhotonClassExtended extends PhotonClass {
|
|
386
484
|
templates: TemplateInfo[];
|
|
387
485
|
statics: StaticInfo[];
|
|
388
486
|
/** Assets from the Photon's asset folder (UI, prompts, resources) */
|
|
@@ -391,6 +489,9 @@ export interface PhotonMCPClassExtended extends PhotonMCPClass {
|
|
|
391
489
|
injectedPhotons?: string[];
|
|
392
490
|
}
|
|
393
491
|
|
|
492
|
+
/** @deprecated Use PhotonClassExtended instead */
|
|
493
|
+
export type PhotonMCPClassExtended = PhotonClassExtended;
|
|
494
|
+
|
|
394
495
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
395
496
|
// STATEFUL WORKFLOW TYPES
|
|
396
497
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -575,7 +676,7 @@ export interface ConfigParam {
|
|
|
575
676
|
*
|
|
576
677
|
* Example:
|
|
577
678
|
* ```typescript
|
|
578
|
-
* export default class MyPhoton extends
|
|
679
|
+
* export default class MyPhoton extends Photon {
|
|
579
680
|
* async configure(params: {
|
|
580
681
|
* apiEndpoint: string;
|
|
581
682
|
* maxRetries?: number;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration and rate parsing utilities for functional tags
|
|
3
|
+
*
|
|
4
|
+
* Supports duration strings: 30s, 5m, 1h, 1d, 500ms
|
|
5
|
+
* Supports rate expressions: 10/min, 100/h, 5/s
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const DURATION_MULTIPLIERS: Record<string, number> = {
|
|
9
|
+
ms: 1,
|
|
10
|
+
s: 1_000,
|
|
11
|
+
sec: 1_000,
|
|
12
|
+
m: 60_000,
|
|
13
|
+
min: 60_000,
|
|
14
|
+
h: 3_600_000,
|
|
15
|
+
hr: 3_600_000,
|
|
16
|
+
d: 86_400_000,
|
|
17
|
+
day: 86_400_000,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a duration string into milliseconds
|
|
22
|
+
* @example parseDuration('30s') → 30000
|
|
23
|
+
* @example parseDuration('5m') → 300000
|
|
24
|
+
* @example parseDuration('500ms') → 500
|
|
25
|
+
* @example parseDuration('1234') → 1234 (raw ms fallback)
|
|
26
|
+
*/
|
|
27
|
+
export function parseDuration(input: string): number {
|
|
28
|
+
const trimmed = input.trim();
|
|
29
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)(ms|s|sec|m|min|h|hr|d|day)$/i);
|
|
30
|
+
if (match) {
|
|
31
|
+
const value = parseFloat(match[1]);
|
|
32
|
+
const unit = match[2].toLowerCase();
|
|
33
|
+
return Math.round(value * DURATION_MULTIPLIERS[unit]);
|
|
34
|
+
}
|
|
35
|
+
// Fallback: raw milliseconds
|
|
36
|
+
const raw = parseInt(trimmed, 10);
|
|
37
|
+
return isNaN(raw) ? 0 : raw;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RATE_WINDOW_MULTIPLIERS: Record<string, number> = {
|
|
41
|
+
s: 1_000,
|
|
42
|
+
sec: 1_000,
|
|
43
|
+
m: 60_000,
|
|
44
|
+
min: 60_000,
|
|
45
|
+
h: 3_600_000,
|
|
46
|
+
hr: 3_600_000,
|
|
47
|
+
d: 86_400_000,
|
|
48
|
+
day: 86_400_000,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse a rate expression into count and window
|
|
53
|
+
* @example parseRate('10/min') → { count: 10, windowMs: 60000 }
|
|
54
|
+
* @example parseRate('100/h') → { count: 100, windowMs: 3600000 }
|
|
55
|
+
*/
|
|
56
|
+
export function parseRate(input: string): { count: number; windowMs: number } {
|
|
57
|
+
const trimmed = input.trim();
|
|
58
|
+
const match = trimmed.match(/^(\d+)\/(s|sec|m|min|h|hr|d|day)$/i);
|
|
59
|
+
if (match) {
|
|
60
|
+
const count = parseInt(match[1], 10);
|
|
61
|
+
const unit = match[2].toLowerCase();
|
|
62
|
+
return { count, windowMs: RATE_WINDOW_MULTIPLIERS[unit] };
|
|
63
|
+
}
|
|
64
|
+
// Fallback: treat as count per minute
|
|
65
|
+
const count = parseInt(trimmed, 10);
|
|
66
|
+
return { count: isNaN(count) ? 10 : count, windowMs: 60_000 };
|
|
67
|
+
}
|