@portel/photon-core 2.21.0 → 2.23.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 +19 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +29 -0
- package/dist/base.js.map +1 -1
- package/dist/bases-registry.d.ts +57 -0
- package/dist/bases-registry.d.ts.map +1 -0
- package/dist/bases-registry.js +127 -0
- package/dist/bases-registry.js.map +1 -0
- package/dist/data-paths.d.ts +31 -18
- package/dist/data-paths.d.ts.map +1 -1
- package/dist/data-paths.js +42 -43
- package/dist/data-paths.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +81 -1
- 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/path-resolver.d.ts +13 -1
- package/dist/path-resolver.d.ts.map +1 -1
- package/dist/path-resolver.js +23 -1
- package/dist/path-resolver.js.map +1 -1
- package/dist/photon-loader-lite.d.ts +17 -2
- package/dist/photon-loader-lite.d.ts.map +1 -1
- package/dist/photon-loader-lite.js +162 -26
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schema-extractor.d.ts +7 -2
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +32 -3
- package/dist/schema-extractor.js.map +1 -1
- package/dist/stateful.d.ts +3 -2
- package/dist/stateful.d.ts.map +1 -1
- package/dist/stateful.js +18 -6
- package/dist/stateful.js.map +1 -1
- package/package.json +2 -2
- package/src/base.ts +43 -0
- package/src/bases-registry.ts +141 -0
- package/src/data-paths.ts +43 -49
- package/src/index.ts +14 -1
- package/src/memory.ts +81 -0
- package/src/middleware.ts +49 -0
- package/src/path-resolver.ts +26 -1
- package/src/photon-loader-lite.ts +176 -33
- package/src/schema-extractor.ts +36 -3
- package/src/stateful.ts +19 -6
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import * as fs from 'fs/promises';
|
|
21
|
+
import * as fsSync from 'fs';
|
|
21
22
|
import * as path from 'path';
|
|
23
|
+
import * as os from 'os';
|
|
22
24
|
import { pathToFileURL } from 'url';
|
|
23
25
|
import { compilePhotonTS } from './compiler.js';
|
|
24
26
|
import { findPhotonClass } from './class-detection.js';
|
|
@@ -135,10 +137,79 @@ export async function photon<T = any>(
|
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/**
|
|
138
|
-
* Clear the photon instance cache.
|
|
140
|
+
* Clear the photon instance cache. Fires onShutdown on each cached
|
|
141
|
+
* instance first so resources held across loads get a chance to drain.
|
|
139
142
|
*/
|
|
140
|
-
export function clearPhotonCache(): void {
|
|
143
|
+
export async function clearPhotonCache(): Promise<void> {
|
|
144
|
+
await disposeAllPhotons('clear-cache');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Dispose one cached photon instance (by absolute path + optional instance
|
|
149
|
+
* name). Invokes onShutdown with the given reason before evicting the
|
|
150
|
+
* cache entry. Errors from onShutdown are logged and swallowed.
|
|
151
|
+
*/
|
|
152
|
+
export async function disposePhoton(
|
|
153
|
+
filePath: string,
|
|
154
|
+
opts: { instanceName?: string; reason?: string } = {},
|
|
155
|
+
): Promise<boolean> {
|
|
156
|
+
const absolutePath = path.isAbsolute(filePath)
|
|
157
|
+
? filePath
|
|
158
|
+
: path.resolve(process.cwd(), filePath);
|
|
159
|
+
const cacheKey = opts.instanceName ? `${absolutePath}::${opts.instanceName}` : absolutePath;
|
|
160
|
+
const proxy = instanceCache.get(cacheKey);
|
|
161
|
+
if (!proxy) return false;
|
|
162
|
+
await invokeShutdownQuietly(proxy, opts.reason || 'dispose');
|
|
163
|
+
instanceCache.delete(cacheKey);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Dispose every cached photon instance and clear the cache. Fires
|
|
169
|
+
* onShutdown on each with the given reason.
|
|
170
|
+
*/
|
|
171
|
+
export async function disposeAllPhotons(reason = 'dispose'): Promise<void> {
|
|
172
|
+
const proxies = Array.from(instanceCache.values());
|
|
141
173
|
instanceCache.clear();
|
|
174
|
+
await Promise.all(proxies.map((p) => invokeShutdownQuietly(p, reason)));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Fire onShutdown with a bounded timeout; never propagates errors. */
|
|
178
|
+
async function invokeShutdownQuietly(proxy: any, reason: string): Promise<void> {
|
|
179
|
+
try {
|
|
180
|
+
const hook = proxy?.onShutdown;
|
|
181
|
+
if (typeof hook !== 'function') return;
|
|
182
|
+
const TIMEOUT_MS = 10_000;
|
|
183
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
184
|
+
try {
|
|
185
|
+
await Promise.race([
|
|
186
|
+
Promise.resolve(hook.call(proxy, { reason })),
|
|
187
|
+
new Promise<never>((_, reject) => {
|
|
188
|
+
timer = setTimeout(
|
|
189
|
+
() => reject(new Error(`onShutdown hook exceeded ${TIMEOUT_MS}ms`)),
|
|
190
|
+
TIMEOUT_MS,
|
|
191
|
+
);
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
194
|
+
} finally {
|
|
195
|
+
if (timer) clearTimeout(timer);
|
|
196
|
+
}
|
|
197
|
+
} catch (err: any) {
|
|
198
|
+
// eslint-disable-next-line no-console
|
|
199
|
+
console.error(`[photon] onShutdown failed during ${reason}: ${err?.message ?? err}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Register a single beforeExit handler that drains cached instances. */
|
|
204
|
+
let exitHookRegistered = false;
|
|
205
|
+
function registerExitHook(): void {
|
|
206
|
+
if (exitHookRegistered) return;
|
|
207
|
+
exitHookRegistered = true;
|
|
208
|
+
process.on('beforeExit', () => {
|
|
209
|
+
// Fire and forget — beforeExit allows async work, but we don't
|
|
210
|
+
// block process exit if a photon's onShutdown misbehaves.
|
|
211
|
+
void disposeAllPhotons('process-exit');
|
|
212
|
+
});
|
|
142
213
|
}
|
|
143
214
|
|
|
144
215
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -192,9 +263,12 @@ async function loadPhotonInternal(
|
|
|
192
263
|
// 9. Instantiate
|
|
193
264
|
const instance = new EnhancedClass(...constructorArgs) as Record<string, any>;
|
|
194
265
|
|
|
195
|
-
// 10. Set photon identity
|
|
266
|
+
// 10. Set photon identity. Namespace mirrors the file's position under
|
|
267
|
+
// baseDir (same rule the classic loader applies). Falls back to 'local'
|
|
268
|
+
// when no baseDir context is available. See
|
|
269
|
+
// docs/internals/PHOTON-DIR-AND-NAMESPACE.md §3.
|
|
196
270
|
instance._photonName = photonName;
|
|
197
|
-
instance._photonNamespace = options.namespace
|
|
271
|
+
instance._photonNamespace = options.namespace ?? deriveNamespace(absolutePath, options.baseDir);
|
|
198
272
|
if (options.instanceName) {
|
|
199
273
|
instance.instanceName = options.instanceName;
|
|
200
274
|
}
|
|
@@ -219,7 +293,7 @@ async function loadPhotonInternal(
|
|
|
219
293
|
method: string,
|
|
220
294
|
params: Record<string, any>,
|
|
221
295
|
) => {
|
|
222
|
-
const targetPath = resolvePhotonPath(targetPhotonName, absolutePath);
|
|
296
|
+
const targetPath = resolvePhotonPath(targetPhotonName, absolutePath, options.baseDir);
|
|
223
297
|
const target = await photon(targetPath, {
|
|
224
298
|
baseDir: options.baseDir,
|
|
225
299
|
mcpFactory: options.mcpFactory,
|
|
@@ -233,15 +307,35 @@ async function loadPhotonInternal(
|
|
|
233
307
|
await instance.onInitialize();
|
|
234
308
|
}
|
|
235
309
|
|
|
236
|
-
// 16. Build middleware proxy
|
|
310
|
+
// 16. Build middleware proxy (wraps dispatch with onError observability)
|
|
237
311
|
const proxy = buildMiddlewareProxy(instance, photonName, toolSchemas, options);
|
|
238
312
|
|
|
313
|
+
// 17. Register for shutdown on process exit so onShutdown fires for
|
|
314
|
+
// lite-loaded photons the caller never explicitly disposed.
|
|
315
|
+
registerExitHook();
|
|
316
|
+
|
|
239
317
|
return proxy;
|
|
240
318
|
} finally {
|
|
241
319
|
loadingPaths.delete(absolutePath);
|
|
242
320
|
}
|
|
243
321
|
}
|
|
244
322
|
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
324
|
+
// Namespace derivation (mirrors classic loader's resolveNamespace)
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
326
|
+
|
|
327
|
+
function deriveNamespace(absolutePath: string, baseDir?: string): string {
|
|
328
|
+
if (!baseDir) return 'local';
|
|
329
|
+
const resolvedBase = path.resolve(baseDir);
|
|
330
|
+
const rel = path.relative(resolvedBase, absolutePath);
|
|
331
|
+
// File outside baseDir — fall back to 'local'.
|
|
332
|
+
if (rel.startsWith('..')) return 'local';
|
|
333
|
+
const parts = rel.split(path.sep);
|
|
334
|
+
// Flat file at baseDir root — use 'local' (equivalent to '' in getPhotonDataDir).
|
|
335
|
+
if (parts.length < 2) return 'local';
|
|
336
|
+
return parts.slice(0, -1).join(path.sep);
|
|
337
|
+
}
|
|
338
|
+
|
|
245
339
|
// ═══════════════════════════════════════════════════════════════════
|
|
246
340
|
// Constructor injection
|
|
247
341
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -268,7 +362,12 @@ async function resolveConstructorArgs(
|
|
|
268
362
|
case 'photon': {
|
|
269
363
|
// Recursive photon loading
|
|
270
364
|
const dep = injection.photonDependency!;
|
|
271
|
-
const depPath = resolvePhotonDepPath(
|
|
365
|
+
const depPath = resolvePhotonDepPath(
|
|
366
|
+
dep.source,
|
|
367
|
+
dep.sourceType,
|
|
368
|
+
currentPath,
|
|
369
|
+
options.baseDir,
|
|
370
|
+
);
|
|
272
371
|
const depInstance = await photon(depPath, {
|
|
273
372
|
baseDir: options.baseDir,
|
|
274
373
|
mcpFactory: options.mcpFactory,
|
|
@@ -507,14 +606,18 @@ function buildMiddlewareProxy(
|
|
|
507
606
|
|
|
508
607
|
const schema = toolMap.get(prop);
|
|
509
608
|
const declarations: MiddlewareDeclaration[] = schema?.middleware || [];
|
|
609
|
+
const hasErrorHook = typeof instance.onError === 'function';
|
|
510
610
|
|
|
511
|
-
// No middleware —
|
|
512
|
-
|
|
611
|
+
// No middleware AND no onError — preserve the bound-method fast path
|
|
612
|
+
// (keeps sync methods sync for callers that don't need the hook).
|
|
613
|
+
if (declarations.length === 0 && !hasErrorHook) {
|
|
513
614
|
return value.bind(target);
|
|
514
615
|
}
|
|
515
616
|
|
|
516
|
-
// Return a function that
|
|
517
|
-
|
|
617
|
+
// Return a function that runs through the middleware chain (if any)
|
|
618
|
+
// and routes any thrown error through the onError observability hook
|
|
619
|
+
// before re-throwing. Hook cannot suppress or transform the error.
|
|
620
|
+
return async (...args: any[]) => {
|
|
518
621
|
const ctx: MiddlewareContext = {
|
|
519
622
|
photon: photonName,
|
|
520
623
|
tool: prop,
|
|
@@ -523,20 +626,52 @@ function buildMiddlewareProxy(
|
|
|
523
626
|
};
|
|
524
627
|
|
|
525
628
|
const execute = () => value.apply(target, args);
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
629
|
+
const dispatch =
|
|
630
|
+
declarations.length === 0
|
|
631
|
+
? execute
|
|
632
|
+
: buildMiddlewareChain(execute, declarations, registry, stateStores, ctx);
|
|
633
|
+
|
|
634
|
+
try {
|
|
635
|
+
return await dispatch();
|
|
636
|
+
} catch (err) {
|
|
637
|
+
await invokeErrorHookLite(instance, err, { tool: prop, params: args[0] ?? {} });
|
|
638
|
+
throw err;
|
|
639
|
+
}
|
|
535
640
|
};
|
|
536
641
|
},
|
|
537
642
|
});
|
|
538
643
|
}
|
|
539
644
|
|
|
645
|
+
/** Fire onError with a bounded 5s timeout. Never throws, never suppresses. */
|
|
646
|
+
async function invokeErrorHookLite(
|
|
647
|
+
instance: Record<string, any>,
|
|
648
|
+
error: unknown,
|
|
649
|
+
ctx: { tool: string; params: any },
|
|
650
|
+
): Promise<void> {
|
|
651
|
+
const hook = instance.onError;
|
|
652
|
+
if (typeof hook !== 'function') return;
|
|
653
|
+
const TIMEOUT_MS = 5_000;
|
|
654
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
655
|
+
try {
|
|
656
|
+
await Promise.race([
|
|
657
|
+
Promise.resolve(hook.call(instance, error, ctx)),
|
|
658
|
+
new Promise<never>((_, reject) => {
|
|
659
|
+
timer = setTimeout(
|
|
660
|
+
() => reject(new Error(`onError hook exceeded ${TIMEOUT_MS}ms`)),
|
|
661
|
+
TIMEOUT_MS,
|
|
662
|
+
);
|
|
663
|
+
}),
|
|
664
|
+
]);
|
|
665
|
+
} catch (hookErr: any) {
|
|
666
|
+
// eslint-disable-next-line no-console
|
|
667
|
+
console.error(
|
|
668
|
+
`onError hook failed for ${ctx.tool}: ${hookErr?.message ?? String(hookErr)}`,
|
|
669
|
+
);
|
|
670
|
+
} finally {
|
|
671
|
+
if (timer) clearTimeout(timer);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
540
675
|
// ═══════════════════════════════════════════════════════════════════
|
|
541
676
|
// Path utilities
|
|
542
677
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -553,12 +688,15 @@ function derivePhotonName(filePath: string): string {
|
|
|
553
688
|
}
|
|
554
689
|
|
|
555
690
|
/**
|
|
556
|
-
* Resolve a photon dependency source to an absolute path.
|
|
691
|
+
* Resolve a photon dependency source to an absolute path. Marketplace
|
|
692
|
+
* lookups respect the resolved PHOTON_DIR so lite-loaded photons find
|
|
693
|
+
* their dependencies under the same base the caller configured.
|
|
557
694
|
*/
|
|
558
695
|
function resolvePhotonDepPath(
|
|
559
696
|
source: string,
|
|
560
697
|
sourceType: string,
|
|
561
698
|
currentPhotonPath: string,
|
|
699
|
+
baseDir?: string,
|
|
562
700
|
): string {
|
|
563
701
|
if (sourceType === 'local') {
|
|
564
702
|
if (source.startsWith('./') || source.startsWith('../')) {
|
|
@@ -567,10 +705,14 @@ function resolvePhotonDepPath(
|
|
|
567
705
|
return source;
|
|
568
706
|
}
|
|
569
707
|
|
|
570
|
-
//
|
|
708
|
+
// Marketplace photons live under the resolved PHOTON_DIR (not hardcoded
|
|
709
|
+
// to ~/.photon as before). Canonical layout is `{base}/{source}.photon.ts`
|
|
710
|
+
// for flat installs, `{base}/<ns>/{source}.photon.ts` for namespaced ones.
|
|
711
|
+
// Here we return the flat path; callers that need namespaced resolution
|
|
712
|
+
// should use the classic loader.
|
|
571
713
|
if (sourceType === 'marketplace') {
|
|
572
|
-
const
|
|
573
|
-
return path.join(
|
|
714
|
+
const base = baseDir || process.env.PHOTON_DIR || path.join(os.homedir(), '.photon');
|
|
715
|
+
return path.join(base, `${source}.photon.ts`);
|
|
574
716
|
}
|
|
575
717
|
|
|
576
718
|
// npm and github sources — for now, throw a helpful error
|
|
@@ -583,14 +725,15 @@ function resolvePhotonDepPath(
|
|
|
583
725
|
|
|
584
726
|
/**
|
|
585
727
|
* Resolve a photon name to a path for cross-photon calls.
|
|
586
|
-
*
|
|
728
|
+
* Prefers a sibling file next to the caller; falls back to the resolved
|
|
729
|
+
* PHOTON_DIR. Returns the most likely path; the caller reports an
|
|
730
|
+
* actionable error if it doesn't exist.
|
|
587
731
|
*/
|
|
588
|
-
function resolvePhotonPath(photonName: string, callerPath: string): string {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return siblingPath;
|
|
732
|
+
function resolvePhotonPath(photonName: string, callerPath: string, baseDir?: string): string {
|
|
733
|
+
const siblingPath = path.join(path.dirname(callerPath), `${photonName}.photon.ts`);
|
|
734
|
+
if (fsSync.existsSync(siblingPath)) return siblingPath;
|
|
735
|
+
const base = baseDir || process.env.PHOTON_DIR || path.join(os.homedir(), '.photon');
|
|
736
|
+
const baseFlat = path.join(base, `${photonName}.photon.ts`);
|
|
737
|
+
if (fsSync.existsSync(baseFlat)) return baseFlat;
|
|
738
|
+
return siblingPath; // load will fail with a clear error if missing
|
|
596
739
|
}
|
package/src/schema-extractor.ts
CHANGED
|
@@ -14,6 +14,21 @@ import { ExtractedSchema, ConstructorParam, TemplateInfo, StaticInfo, OutputForm
|
|
|
14
14
|
import { parseDuration, parseRate } from './utils/duration.js';
|
|
15
15
|
import { builtinRegistry, type MiddlewareDeclaration } from './middleware.js';
|
|
16
16
|
|
|
17
|
+
// Track which `handle*` method names have already emitted a deprecation
|
|
18
|
+
// warning this process so large photons don't spam the console.
|
|
19
|
+
const handlePrefixWarned = new Set<string>();
|
|
20
|
+
|
|
21
|
+
function warnHandlePrefixOnce(methodName: string): void {
|
|
22
|
+
if (handlePrefixWarned.has(methodName)) return;
|
|
23
|
+
handlePrefixWarned.add(methodName);
|
|
24
|
+
// eslint-disable-next-line no-console
|
|
25
|
+
console.error(
|
|
26
|
+
`[photon] deprecation: method "${methodName}" is being auto-registered as a ` +
|
|
27
|
+
`webhook via the legacy handle* prefix. Add an explicit "@webhook" JSDoc ` +
|
|
28
|
+
`tag; the prefix convention will be removed in the next minor release.`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
export interface ExtractedMetadata {
|
|
18
33
|
tools: ExtractedSchema[];
|
|
19
34
|
templates: TemplateInfo[];
|
|
@@ -1180,6 +1195,18 @@ export class SchemaExtractor {
|
|
|
1180
1195
|
declarations.push({ name: 'circuitBreaker', config: circuitBreaker, phase: def?.phase ?? 8 });
|
|
1181
1196
|
}
|
|
1182
1197
|
|
|
1198
|
+
// @bulkhead
|
|
1199
|
+
const bulkheadMatch = jsdocContent.match(/@bulkhead(?:\s+(\d+))?/i);
|
|
1200
|
+
if (bulkheadMatch) {
|
|
1201
|
+
const maxConcurrent = Math.max(1, parseInt(bulkheadMatch[1] || '1', 10));
|
|
1202
|
+
const def = builtinRegistry.get('bulkhead');
|
|
1203
|
+
declarations.push({
|
|
1204
|
+
name: 'bulkhead',
|
|
1205
|
+
config: { maxConcurrent },
|
|
1206
|
+
phase: def?.phase ?? 15,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1183
1210
|
// @cached
|
|
1184
1211
|
const cached = this.extractCached(jsdocContent);
|
|
1185
1212
|
if (cached) {
|
|
@@ -2036,10 +2063,15 @@ export class SchemaExtractor {
|
|
|
2036
2063
|
// ═══════════════════════════════════════════════════════════════════════════════
|
|
2037
2064
|
|
|
2038
2065
|
/**
|
|
2039
|
-
* Extract webhook configuration from @webhook tag or handle* prefix
|
|
2066
|
+
* Extract webhook configuration from @webhook tag or handle* prefix.
|
|
2040
2067
|
* - @webhook → use method name as path
|
|
2041
2068
|
* - @webhook stripe → custom path "stripe"
|
|
2042
|
-
* - handle* prefix → auto-detected as webhook
|
|
2069
|
+
* - handle* prefix → auto-detected as webhook (DEPRECATED)
|
|
2070
|
+
*
|
|
2071
|
+
* The handle* prefix is a legacy convention being removed. It still
|
|
2072
|
+
* works for one release but emits a one-time stderr warning per
|
|
2073
|
+
* method so users can migrate to the explicit @webhook tag before
|
|
2074
|
+
* the convention is dropped.
|
|
2043
2075
|
*/
|
|
2044
2076
|
private extractWebhook(jsdocContent: string, methodName: string): boolean | string | undefined {
|
|
2045
2077
|
// Check for @webhook tag with optional path
|
|
@@ -2051,8 +2083,9 @@ export class SchemaExtractor {
|
|
|
2051
2083
|
return path || true;
|
|
2052
2084
|
}
|
|
2053
2085
|
|
|
2054
|
-
// Check for handle* prefix (convention)
|
|
2086
|
+
// Check for handle* prefix (legacy convention — deprecated).
|
|
2055
2087
|
if (methodName.startsWith('handle')) {
|
|
2088
|
+
warnHandlePrefixOnce(methodName);
|
|
2056
2089
|
return true;
|
|
2057
2090
|
}
|
|
2058
2091
|
|
package/src/stateful.ts
CHANGED
|
@@ -87,10 +87,21 @@ import {
|
|
|
87
87
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
90
|
+
* Resolve the default runs directory at call time so a long-lived daemon
|
|
91
|
+
* that serves multiple PHOTON_DIRs picks up each base's runs. Previously
|
|
92
|
+
* a module-level const frozen at import time.
|
|
93
|
+
* @deprecated Use getPhotonRunsDir(namespace, photonName, baseDir) for per-photon runs.
|
|
92
94
|
*/
|
|
93
|
-
|
|
95
|
+
function defaultRunsDir(): string {
|
|
96
|
+
return getLegacyRunsDir();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Back-compat export. Resolved at import time — kept for consumers that
|
|
101
|
+
* read `RUNS_DIR` as a constant. New code should call getPhotonRunsDir().
|
|
102
|
+
* @deprecated Use getPhotonRunsDir(namespace, photonName, baseDir).
|
|
103
|
+
*/
|
|
104
|
+
export const RUNS_DIR = defaultRunsDir();
|
|
94
105
|
|
|
95
106
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
96
107
|
// CHECKPOINT YIELD TYPE
|
|
@@ -136,7 +147,9 @@ export class StateLog {
|
|
|
136
147
|
private logPath: string;
|
|
137
148
|
|
|
138
149
|
constructor(runId: string, runsDir?: string) {
|
|
139
|
-
|
|
150
|
+
// Resolve runs dir at call time so the current PHOTON_DIR is honored
|
|
151
|
+
// even when a long-lived process has served earlier bases.
|
|
152
|
+
this.logPath = path.join(runsDir || defaultRunsDir(), `${runId}.jsonl`);
|
|
140
153
|
}
|
|
141
154
|
|
|
142
155
|
/**
|
|
@@ -573,7 +586,7 @@ export async function executeStatefulGenerator<T>(
|
|
|
573
586
|
* List all workflow runs
|
|
574
587
|
*/
|
|
575
588
|
export async function listRuns(runsDir?: string): Promise<WorkflowRun[]> {
|
|
576
|
-
const dir = runsDir ||
|
|
589
|
+
const dir = runsDir || defaultRunsDir();
|
|
577
590
|
const runs: WorkflowRun[] = [];
|
|
578
591
|
|
|
579
592
|
try {
|
|
@@ -639,7 +652,7 @@ export async function getRunInfo(runId: string, runsDir?: string): Promise<Workf
|
|
|
639
652
|
* Delete a workflow run
|
|
640
653
|
*/
|
|
641
654
|
export async function deleteRun(runId: string, runsDir?: string): Promise<void> {
|
|
642
|
-
const logPath = path.join(runsDir ||
|
|
655
|
+
const logPath = path.join(runsDir || defaultRunsDir(), `${runId}.jsonl`);
|
|
643
656
|
await fs.unlink(logPath);
|
|
644
657
|
}
|
|
645
658
|
|