@portel/photon-core 2.22.0 → 2.24.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 +56 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +100 -2
- 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/description-sanitizer.d.ts +34 -0
- package/dist/description-sanitizer.d.ts.map +1 -0
- package/dist/description-sanitizer.js +80 -0
- package/dist/description-sanitizer.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/memory.d.ts.map +1 -1
- package/dist/memory.js +109 -1
- package/dist/memory.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +96 -0
- package/dist/middleware.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +9 -2
- package/dist/mixins.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 +203 -26
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schedule.d.ts +10 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +20 -10
- package/dist/schedule.js.map +1 -1
- package/dist/schema-extractor.d.ts +9 -3
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +149 -17
- 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/dist/types.d.ts +9 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/base.ts +123 -2
- package/src/bases-registry.ts +141 -0
- package/src/data-paths.ts +43 -49
- package/src/description-sanitizer.ts +102 -0
- package/src/index.ts +20 -1
- package/src/memory.ts +109 -0
- package/src/middleware.ts +98 -0
- package/src/mixins.ts +14 -2
- package/src/path-resolver.ts +26 -1
- package/src/photon-loader-lite.ts +214 -33
- package/src/schedule.ts +26 -10
- package/src/schema-extractor.ts +164 -17
- package/src/stateful.ts +19 -6
- package/src/types.ts +9 -0
|
@@ -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,50 @@ 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);
|
|
272
|
+
instance._baseDir = options.baseDir;
|
|
273
|
+
instance._photonFilePath = absolutePath;
|
|
274
|
+
// Stat-gate baseline. When executeTool() sees the source file has
|
|
275
|
+
// changed, it fires _photonReloader (registered just below) to swap
|
|
276
|
+
// in the fresh compile before dispatching.
|
|
277
|
+
try {
|
|
278
|
+
const s = fsSync.statSync(absolutePath);
|
|
279
|
+
instance._photonSourceStat = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino };
|
|
280
|
+
} catch {
|
|
281
|
+
// No stat — skip baselining so the gate is a no-op.
|
|
282
|
+
}
|
|
283
|
+
instance._photonReloader = async () => {
|
|
284
|
+
// Invalidate the cache entry, then re-run the whole load pipeline
|
|
285
|
+
// and copy public surface from the fresh instance onto the existing
|
|
286
|
+
// one. Callers already holding a reference to the proxy see new
|
|
287
|
+
// method behavior on the very next dispatch.
|
|
288
|
+
instanceCache.delete(cacheKey);
|
|
289
|
+
const fresh = (await photon(absolutePath, options)) as Record<string, any>;
|
|
290
|
+
// Refresh stat baseline on success so we don't re-trigger.
|
|
291
|
+
try {
|
|
292
|
+
const s = fsSync.statSync(absolutePath);
|
|
293
|
+
instance._photonSourceStat = { mtimeMs: s.mtimeMs, size: s.size, ino: s.ino };
|
|
294
|
+
} catch {
|
|
295
|
+
// ignore — next call will re-evaluate
|
|
296
|
+
}
|
|
297
|
+
// Rewire every own property from the fresh instance onto the
|
|
298
|
+
// live one. Prototype-level methods are re-looked-up through the
|
|
299
|
+
// instance's class on each dispatch via the proxy's method lookup,
|
|
300
|
+
// so they pick up the new code automatically. Own-property state
|
|
301
|
+
// (collections, explicit fields) gets refreshed here.
|
|
302
|
+
for (const key of Object.keys(fresh)) {
|
|
303
|
+
try {
|
|
304
|
+
(instance as Record<string, any>)[key] = fresh[key];
|
|
305
|
+
} catch {
|
|
306
|
+
// Read-only own property — skip; the proxy path will handle it.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
198
310
|
if (options.instanceName) {
|
|
199
311
|
instance.instanceName = options.instanceName;
|
|
200
312
|
}
|
|
@@ -219,7 +331,7 @@ async function loadPhotonInternal(
|
|
|
219
331
|
method: string,
|
|
220
332
|
params: Record<string, any>,
|
|
221
333
|
) => {
|
|
222
|
-
const targetPath = resolvePhotonPath(targetPhotonName, absolutePath);
|
|
334
|
+
const targetPath = resolvePhotonPath(targetPhotonName, absolutePath, options.baseDir);
|
|
223
335
|
const target = await photon(targetPath, {
|
|
224
336
|
baseDir: options.baseDir,
|
|
225
337
|
mcpFactory: options.mcpFactory,
|
|
@@ -233,15 +345,35 @@ async function loadPhotonInternal(
|
|
|
233
345
|
await instance.onInitialize();
|
|
234
346
|
}
|
|
235
347
|
|
|
236
|
-
// 16. Build middleware proxy
|
|
348
|
+
// 16. Build middleware proxy (wraps dispatch with onError observability)
|
|
237
349
|
const proxy = buildMiddlewareProxy(instance, photonName, toolSchemas, options);
|
|
238
350
|
|
|
351
|
+
// 17. Register for shutdown on process exit so onShutdown fires for
|
|
352
|
+
// lite-loaded photons the caller never explicitly disposed.
|
|
353
|
+
registerExitHook();
|
|
354
|
+
|
|
239
355
|
return proxy;
|
|
240
356
|
} finally {
|
|
241
357
|
loadingPaths.delete(absolutePath);
|
|
242
358
|
}
|
|
243
359
|
}
|
|
244
360
|
|
|
361
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
362
|
+
// Namespace derivation (mirrors classic loader's resolveNamespace)
|
|
363
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
364
|
+
|
|
365
|
+
function deriveNamespace(absolutePath: string, baseDir?: string): string {
|
|
366
|
+
if (!baseDir) return 'local';
|
|
367
|
+
const resolvedBase = path.resolve(baseDir);
|
|
368
|
+
const rel = path.relative(resolvedBase, absolutePath);
|
|
369
|
+
// File outside baseDir — fall back to 'local'.
|
|
370
|
+
if (rel.startsWith('..')) return 'local';
|
|
371
|
+
const parts = rel.split(path.sep);
|
|
372
|
+
// Flat file at baseDir root — use 'local' (equivalent to '' in getPhotonDataDir).
|
|
373
|
+
if (parts.length < 2) return 'local';
|
|
374
|
+
return parts.slice(0, -1).join(path.sep);
|
|
375
|
+
}
|
|
376
|
+
|
|
245
377
|
// ═══════════════════════════════════════════════════════════════════
|
|
246
378
|
// Constructor injection
|
|
247
379
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -268,7 +400,12 @@ async function resolveConstructorArgs(
|
|
|
268
400
|
case 'photon': {
|
|
269
401
|
// Recursive photon loading
|
|
270
402
|
const dep = injection.photonDependency!;
|
|
271
|
-
const depPath = resolvePhotonDepPath(
|
|
403
|
+
const depPath = resolvePhotonDepPath(
|
|
404
|
+
dep.source,
|
|
405
|
+
dep.sourceType,
|
|
406
|
+
currentPath,
|
|
407
|
+
options.baseDir,
|
|
408
|
+
);
|
|
272
409
|
const depInstance = await photon(depPath, {
|
|
273
410
|
baseDir: options.baseDir,
|
|
274
411
|
mcpFactory: options.mcpFactory,
|
|
@@ -507,14 +644,18 @@ function buildMiddlewareProxy(
|
|
|
507
644
|
|
|
508
645
|
const schema = toolMap.get(prop);
|
|
509
646
|
const declarations: MiddlewareDeclaration[] = schema?.middleware || [];
|
|
647
|
+
const hasErrorHook = typeof instance.onError === 'function';
|
|
510
648
|
|
|
511
|
-
// No middleware —
|
|
512
|
-
|
|
649
|
+
// No middleware AND no onError — preserve the bound-method fast path
|
|
650
|
+
// (keeps sync methods sync for callers that don't need the hook).
|
|
651
|
+
if (declarations.length === 0 && !hasErrorHook) {
|
|
513
652
|
return value.bind(target);
|
|
514
653
|
}
|
|
515
654
|
|
|
516
|
-
// Return a function that
|
|
517
|
-
|
|
655
|
+
// Return a function that runs through the middleware chain (if any)
|
|
656
|
+
// and routes any thrown error through the onError observability hook
|
|
657
|
+
// before re-throwing. Hook cannot suppress or transform the error.
|
|
658
|
+
return async (...args: any[]) => {
|
|
518
659
|
const ctx: MiddlewareContext = {
|
|
519
660
|
photon: photonName,
|
|
520
661
|
tool: prop,
|
|
@@ -523,20 +664,52 @@ function buildMiddlewareProxy(
|
|
|
523
664
|
};
|
|
524
665
|
|
|
525
666
|
const execute = () => value.apply(target, args);
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
667
|
+
const dispatch =
|
|
668
|
+
declarations.length === 0
|
|
669
|
+
? execute
|
|
670
|
+
: buildMiddlewareChain(execute, declarations, registry, stateStores, ctx);
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
return await dispatch();
|
|
674
|
+
} catch (err) {
|
|
675
|
+
await invokeErrorHookLite(instance, err, { tool: prop, params: args[0] ?? {} });
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
535
678
|
};
|
|
536
679
|
},
|
|
537
680
|
});
|
|
538
681
|
}
|
|
539
682
|
|
|
683
|
+
/** Fire onError with a bounded 5s timeout. Never throws, never suppresses. */
|
|
684
|
+
async function invokeErrorHookLite(
|
|
685
|
+
instance: Record<string, any>,
|
|
686
|
+
error: unknown,
|
|
687
|
+
ctx: { tool: string; params: any },
|
|
688
|
+
): Promise<void> {
|
|
689
|
+
const hook = instance.onError;
|
|
690
|
+
if (typeof hook !== 'function') return;
|
|
691
|
+
const TIMEOUT_MS = 5_000;
|
|
692
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
693
|
+
try {
|
|
694
|
+
await Promise.race([
|
|
695
|
+
Promise.resolve(hook.call(instance, error, ctx)),
|
|
696
|
+
new Promise<never>((_, reject) => {
|
|
697
|
+
timer = setTimeout(
|
|
698
|
+
() => reject(new Error(`onError hook exceeded ${TIMEOUT_MS}ms`)),
|
|
699
|
+
TIMEOUT_MS,
|
|
700
|
+
);
|
|
701
|
+
}),
|
|
702
|
+
]);
|
|
703
|
+
} catch (hookErr: any) {
|
|
704
|
+
// eslint-disable-next-line no-console
|
|
705
|
+
console.error(
|
|
706
|
+
`onError hook failed for ${ctx.tool}: ${hookErr?.message ?? String(hookErr)}`,
|
|
707
|
+
);
|
|
708
|
+
} finally {
|
|
709
|
+
if (timer) clearTimeout(timer);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
540
713
|
// ═══════════════════════════════════════════════════════════════════
|
|
541
714
|
// Path utilities
|
|
542
715
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -553,12 +726,15 @@ function derivePhotonName(filePath: string): string {
|
|
|
553
726
|
}
|
|
554
727
|
|
|
555
728
|
/**
|
|
556
|
-
* Resolve a photon dependency source to an absolute path.
|
|
729
|
+
* Resolve a photon dependency source to an absolute path. Marketplace
|
|
730
|
+
* lookups respect the resolved PHOTON_DIR so lite-loaded photons find
|
|
731
|
+
* their dependencies under the same base the caller configured.
|
|
557
732
|
*/
|
|
558
733
|
function resolvePhotonDepPath(
|
|
559
734
|
source: string,
|
|
560
735
|
sourceType: string,
|
|
561
736
|
currentPhotonPath: string,
|
|
737
|
+
baseDir?: string,
|
|
562
738
|
): string {
|
|
563
739
|
if (sourceType === 'local') {
|
|
564
740
|
if (source.startsWith('./') || source.startsWith('../')) {
|
|
@@ -567,10 +743,14 @@ function resolvePhotonDepPath(
|
|
|
567
743
|
return source;
|
|
568
744
|
}
|
|
569
745
|
|
|
570
|
-
//
|
|
746
|
+
// Marketplace photons live under the resolved PHOTON_DIR (not hardcoded
|
|
747
|
+
// to ~/.photon as before). Canonical layout is `{base}/{source}.photon.ts`
|
|
748
|
+
// for flat installs, `{base}/<ns>/{source}.photon.ts` for namespaced ones.
|
|
749
|
+
// Here we return the flat path; callers that need namespaced resolution
|
|
750
|
+
// should use the classic loader.
|
|
571
751
|
if (sourceType === 'marketplace') {
|
|
572
|
-
const
|
|
573
|
-
return path.join(
|
|
752
|
+
const base = baseDir || process.env.PHOTON_DIR || path.join(os.homedir(), '.photon');
|
|
753
|
+
return path.join(base, `${source}.photon.ts`);
|
|
574
754
|
}
|
|
575
755
|
|
|
576
756
|
// npm and github sources — for now, throw a helpful error
|
|
@@ -583,14 +763,15 @@ function resolvePhotonDepPath(
|
|
|
583
763
|
|
|
584
764
|
/**
|
|
585
765
|
* Resolve a photon name to a path for cross-photon calls.
|
|
586
|
-
*
|
|
766
|
+
* Prefers a sibling file next to the caller; falls back to the resolved
|
|
767
|
+
* PHOTON_DIR. Returns the most likely path; the caller reports an
|
|
768
|
+
* actionable error if it doesn't exist.
|
|
587
769
|
*/
|
|
588
|
-
function resolvePhotonPath(photonName: string, callerPath: string): string {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
const
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
return siblingPath;
|
|
770
|
+
function resolvePhotonPath(photonName: string, callerPath: string, baseDir?: string): string {
|
|
771
|
+
const siblingPath = path.join(path.dirname(callerPath), `${photonName}.photon.ts`);
|
|
772
|
+
if (fsSync.existsSync(siblingPath)) return siblingPath;
|
|
773
|
+
const base = baseDir || process.env.PHOTON_DIR || path.join(os.homedir(), '.photon');
|
|
774
|
+
const baseFlat = path.join(base, `${photonName}.photon.ts`);
|
|
775
|
+
if (fsSync.existsSync(baseFlat)) return baseFlat;
|
|
776
|
+
return siblingPath; // load will fail with a clear error if missing
|
|
596
777
|
}
|
package/src/schedule.ts
CHANGED
|
@@ -139,9 +139,9 @@ function resolveCron(schedule: string): string {
|
|
|
139
139
|
|
|
140
140
|
// ── Storage Helpers ────────────────────────────────────────────────────
|
|
141
141
|
|
|
142
|
-
function photonScheduleDir(photonId: string, namespace?: string): string {
|
|
142
|
+
function photonScheduleDir(photonId: string, namespace?: string, baseDir?: string): string {
|
|
143
143
|
const ns = namespace || 'local';
|
|
144
|
-
const newDir = getPhotonSchedulesDir(ns, photonId);
|
|
144
|
+
const newDir = getPhotonSchedulesDir(ns, photonId, baseDir);
|
|
145
145
|
if (!fsSync.existsSync(newDir)) {
|
|
146
146
|
const legacyDir = getLegacySchedulesDir(photonId);
|
|
147
147
|
if (fsSync.existsSync(legacyDir)) return legacyDir;
|
|
@@ -149,8 +149,8 @@ function photonScheduleDir(photonId: string, namespace?: string): string {
|
|
|
149
149
|
return newDir;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
function taskPath(photonId: string, taskId: string): string {
|
|
153
|
-
return path.join(photonScheduleDir(photonId), `${taskId}.json`);
|
|
152
|
+
function taskPath(photonId: string, taskId: string, baseDir?: string): string {
|
|
153
|
+
return path.join(photonScheduleDir(photonId, undefined, baseDir), `${taskId}.json`);
|
|
154
154
|
}
|
|
155
155
|
|
|
156
156
|
async function ensureDir(dir: string): Promise<void> {
|
|
@@ -171,9 +171,19 @@ async function ensureDir(dir: string): Promise<void> {
|
|
|
171
171
|
*/
|
|
172
172
|
export class ScheduleProvider {
|
|
173
173
|
private _photonId: string;
|
|
174
|
+
private _baseDir?: string;
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
/**
|
|
177
|
+
* @param photonId Photon identifier used as the bucket under .data/
|
|
178
|
+
* @param baseDir PHOTON_DIR the photon was loaded from. Pinned so
|
|
179
|
+
* schedule files stay under this base regardless of which process
|
|
180
|
+
* reads back later — mirrors the fix applied to MemoryProvider.
|
|
181
|
+
* Without it, photonScheduleDir falls through to PHOTON_DIR env or
|
|
182
|
+
* ~/.photon and schedule files drift across daemon restarts.
|
|
183
|
+
*/
|
|
184
|
+
constructor(photonId: string, baseDir?: string) {
|
|
176
185
|
this._photonId = photonId;
|
|
186
|
+
this._baseDir = baseDir;
|
|
177
187
|
}
|
|
178
188
|
|
|
179
189
|
/**
|
|
@@ -222,7 +232,10 @@ export class ScheduleProvider {
|
|
|
222
232
|
*/
|
|
223
233
|
async get(taskId: string): Promise<ScheduledTask | null> {
|
|
224
234
|
try {
|
|
225
|
-
const content = await fs.readFile(
|
|
235
|
+
const content = await fs.readFile(
|
|
236
|
+
taskPath(this._photonId, taskId, this._baseDir),
|
|
237
|
+
'utf-8'
|
|
238
|
+
);
|
|
226
239
|
return JSON.parse(content) as ScheduledTask;
|
|
227
240
|
} catch (err: any) {
|
|
228
241
|
if (err.code === 'ENOENT') return null;
|
|
@@ -242,7 +255,7 @@ export class ScheduleProvider {
|
|
|
242
255
|
* List all scheduled tasks, optionally filtered by status
|
|
243
256
|
*/
|
|
244
257
|
async list(status?: ScheduleStatus): Promise<ScheduledTask[]> {
|
|
245
|
-
const dir = photonScheduleDir(this._photonId);
|
|
258
|
+
const dir = photonScheduleDir(this._photonId, undefined, this._baseDir);
|
|
246
259
|
let files: string[];
|
|
247
260
|
try {
|
|
248
261
|
files = await fs.readdir(dir);
|
|
@@ -323,7 +336,7 @@ export class ScheduleProvider {
|
|
|
323
336
|
*/
|
|
324
337
|
async cancel(taskId: string): Promise<boolean> {
|
|
325
338
|
try {
|
|
326
|
-
await fs.unlink(taskPath(this._photonId, taskId));
|
|
339
|
+
await fs.unlink(taskPath(this._photonId, taskId, this._baseDir));
|
|
327
340
|
return true;
|
|
328
341
|
} catch (err: any) {
|
|
329
342
|
if (err.code === 'ENOENT') return false;
|
|
@@ -362,8 +375,11 @@ export class ScheduleProvider {
|
|
|
362
375
|
|
|
363
376
|
/** @internal */
|
|
364
377
|
private async _save(task: ScheduledTask): Promise<void> {
|
|
365
|
-
const dir = photonScheduleDir(this._photonId);
|
|
378
|
+
const dir = photonScheduleDir(this._photonId, undefined, this._baseDir);
|
|
366
379
|
await ensureDir(dir);
|
|
367
|
-
await fs.writeFile(
|
|
380
|
+
await fs.writeFile(
|
|
381
|
+
taskPath(this._photonId, task.id, this._baseDir),
|
|
382
|
+
JSON.stringify(task, null, 2)
|
|
383
|
+
);
|
|
368
384
|
}
|
|
369
385
|
}
|