@portel/photon-core 2.26.0 → 2.27.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/README.md +121 -0
- package/dist/base.d.ts +27 -0
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +27 -0
- package/dist/base.js.map +1 -1
- package/dist/cf.d.ts +121 -0
- package/dist/cf.d.ts.map +1 -0
- package/dist/cf.js +65 -0
- package/dist/cf.js.map +1 -0
- package/dist/cloudflare.d.ts +110 -0
- package/dist/cloudflare.d.ts.map +1 -0
- package/dist/cloudflare.js +138 -0
- package/dist/cloudflare.js.map +1 -0
- package/dist/collections/Collection.d.ts +12 -0
- package/dist/collections/Collection.d.ts.map +1 -1
- package/dist/collections/Collection.js +19 -0
- package/dist/collections/Collection.js.map +1 -1
- package/dist/env.d.ts +29 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/mixins.d.ts.map +1 -1
- package/dist/mixins.js +10 -0
- package/dist/mixins.js.map +1 -1
- package/dist/photon-loader-lite.d.ts +17 -0
- package/dist/photon-loader-lite.d.ts.map +1 -1
- package/dist/photon-loader-lite.js +150 -1
- package/dist/photon-loader-lite.js.map +1 -1
- package/dist/schema-extractor.d.ts +4 -1
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +54 -0
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
- package/src/base.ts +29 -0
- package/src/cf.ts +165 -0
- package/src/cloudflare.ts +198 -0
- package/src/collections/Collection.ts +19 -0
- package/src/env.ts +28 -0
- package/src/index.ts +27 -0
- package/src/mixins.ts +12 -0
- package/src/photon-loader-lite.ts +185 -0
- package/src/schema-extractor.ts +74 -1
- package/src/types.ts +16 -1
|
@@ -38,6 +38,12 @@ import { withPhotonCapabilities } from './mixins.js';
|
|
|
38
38
|
import { MemoryProvider } from './memory.js';
|
|
39
39
|
import { ScheduleProvider } from './schedule.js';
|
|
40
40
|
import { toEnvVarName, parseEnvValue, type MissingParamInfo } from './env-utils.js';
|
|
41
|
+
import { Photon as PhotonBase } from './base.js';
|
|
42
|
+
import {
|
|
43
|
+
type Cloudflare,
|
|
44
|
+
notConfiguredCloudflare,
|
|
45
|
+
createCloudflareFromEnv,
|
|
46
|
+
} from './cloudflare.js';
|
|
41
47
|
import type { ExtractedSchema } from './types.js';
|
|
42
48
|
import type { MCPClientFactory } from '@portel/mcp';
|
|
43
49
|
import { getCacheDir } from './data-paths.js';
|
|
@@ -59,6 +65,22 @@ export interface PhotonOptions {
|
|
|
59
65
|
sessionId?: string;
|
|
60
66
|
/** Namespace for data path resolution (marketplace owner). Defaults to 'local'. */
|
|
61
67
|
namespace?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Build a `Cloudflare` runtime for a given photon. Hosts that bring CF
|
|
70
|
+
* support (local miniflare, deployed Worker) supply a factory; the
|
|
71
|
+
* loader calls it whenever a photon's constructor needs `Cloudflare`
|
|
72
|
+
* (or whenever `this.cf.*` is detected on a plain class). When the
|
|
73
|
+
* factory is absent the loader injects a throwing-Proxy fallback so
|
|
74
|
+
* the diagnostic remains clear.
|
|
75
|
+
*/
|
|
76
|
+
cloudflareFactory?: (photonName: string) => Cloudflare;
|
|
77
|
+
/**
|
|
78
|
+
* Raw Cloudflare Worker `env`. Wired through to constructor params
|
|
79
|
+
* typed `CloudflareEnv` / `CloudflareEnv<T>`. Only present when the
|
|
80
|
+
* loader is being driven by the deployed Worker template (or by a
|
|
81
|
+
* test that wants to simulate it).
|
|
82
|
+
*/
|
|
83
|
+
cloudflareEnv?: Record<string, unknown>;
|
|
62
84
|
}
|
|
63
85
|
|
|
64
86
|
export interface PhotonEvent {
|
|
@@ -263,6 +285,24 @@ async function loadPhotonInternal(
|
|
|
263
285
|
// 9. Instantiate
|
|
264
286
|
const instance = new EnhancedClass(...constructorArgs) as Record<string, any>;
|
|
265
287
|
|
|
288
|
+
// 9a. Forgiving auto-inject: classes that reference `this.cf.*` or
|
|
289
|
+
// `this.cfEnv.*` without declaring the matching constructor param
|
|
290
|
+
// still get the field populated, so authoring stays loose. The
|
|
291
|
+
// explicit-injection path (a typed constructor param) takes
|
|
292
|
+
// precedence; this only fills gaps. Mirrors how the classic loader
|
|
293
|
+
// injects `_callHandler` etc. for plain classes.
|
|
294
|
+
const detected = detectCapabilities(source);
|
|
295
|
+
const hasExplicitCloudflare = injections.some(i => i.injectionType === 'cloudflare');
|
|
296
|
+
const hasExplicitCloudflareEnv = injections.some(i => i.injectionType === 'cloudflareEnv');
|
|
297
|
+
if (detected.has('cloudflare') && !hasExplicitCloudflare && instance.cf === undefined) {
|
|
298
|
+
instance.cf = options.cloudflareFactory
|
|
299
|
+
? options.cloudflareFactory(photonName)
|
|
300
|
+
: notConfiguredCloudflare();
|
|
301
|
+
}
|
|
302
|
+
if (detected.has('cloudflareEnv') && !hasExplicitCloudflareEnv && instance.cfEnv === undefined) {
|
|
303
|
+
instance.cfEnv = options.cloudflareEnv ?? makeThrowingCloudflareEnv();
|
|
304
|
+
}
|
|
305
|
+
|
|
266
306
|
// 10. Set photon identity. Namespace mirrors the file's position under
|
|
267
307
|
// baseDir (same rule the classic loader applies). Falls back to 'local'
|
|
268
308
|
// when no baseDir context is available. See
|
|
@@ -450,6 +490,38 @@ async function resolveConstructorArgs(
|
|
|
450
490
|
break;
|
|
451
491
|
}
|
|
452
492
|
|
|
493
|
+
case 'photonRuntime': {
|
|
494
|
+
// `private photon: Photon` — inject a Photon instance configured
|
|
495
|
+
// identically to the host so `this.photon.memory.set(...)`,
|
|
496
|
+
// `this.photon.emit(...)`, etc. resolve to the same scope as
|
|
497
|
+
// the equivalent `extends Photon` calls would. Identity-stable
|
|
498
|
+
// per host load.
|
|
499
|
+
const runtime = createPhotonRuntimeForHost(photonName, currentPath, options);
|
|
500
|
+
values.push(runtime);
|
|
501
|
+
break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
case 'cloudflare': {
|
|
505
|
+
// `private cf: Cloudflare` — wrapped, auto-named CF surface.
|
|
506
|
+
const cf = options.cloudflareFactory
|
|
507
|
+
? options.cloudflareFactory(photonName)
|
|
508
|
+
: notConfiguredCloudflare();
|
|
509
|
+
values.push(cf);
|
|
510
|
+
break;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
case 'cloudflareEnv': {
|
|
514
|
+
// `private cfEnv: CloudflareEnv` — raw Worker env. On hosts
|
|
515
|
+
// without a CF runtime, hand back a throwing Proxy so the
|
|
516
|
+
// diagnostic names the imported symbol clearly.
|
|
517
|
+
if (options.cloudflareEnv) {
|
|
518
|
+
values.push(options.cloudflareEnv);
|
|
519
|
+
} else {
|
|
520
|
+
values.push(makeThrowingCloudflareEnv());
|
|
521
|
+
}
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
453
525
|
default:
|
|
454
526
|
values.push(undefined);
|
|
455
527
|
}
|
|
@@ -466,6 +538,119 @@ async function resolveConstructorArgs(
|
|
|
466
538
|
return values;
|
|
467
539
|
}
|
|
468
540
|
|
|
541
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
542
|
+
// Photon-as-injection helpers
|
|
543
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Build the `Photon` instance handed to `private photon: Photon` ctor
|
|
547
|
+
* params. We instantiate the real base class and configure it with the
|
|
548
|
+
* host photon's identity so every capability (`memory`, `schedule`,
|
|
549
|
+
* `emit`, `call`, `mcp`, `caller`, `confirm`, `elicit`, `sample`,
|
|
550
|
+
* `photon.use`, ...) resolves to the same scope an equivalent
|
|
551
|
+
* `extends Photon` consumer would see. The result is shape-equivalent
|
|
552
|
+
* to a normal Photon, so authors can swap between `extends Photon` and
|
|
553
|
+
* constructor injection without touching any call site beyond the
|
|
554
|
+
* access path (`this.memory` vs `this.photon.memory`).
|
|
555
|
+
*
|
|
556
|
+
* The `_callHandler` and `setMCPFactory` wiring is deferred to the
|
|
557
|
+
* caller (see step 14 of `loadPhotonInternal`), which assigns the
|
|
558
|
+
* resolver after constructor args are built. Constructor-time access
|
|
559
|
+
* to `photon.memory` works because MemoryProvider is lazy-resolved on
|
|
560
|
+
* first use and the photon name / baseDir are set up here directly.
|
|
561
|
+
*/
|
|
562
|
+
function createPhotonRuntimeForHost(
|
|
563
|
+
photonName: string,
|
|
564
|
+
currentPath: string,
|
|
565
|
+
options: PhotonOptions,
|
|
566
|
+
): InstanceType<typeof PhotonBase> {
|
|
567
|
+
const runtime = new PhotonBase() as PhotonBase & Record<string, unknown>;
|
|
568
|
+
runtime._photonName = photonName;
|
|
569
|
+
runtime._photonNamespace =
|
|
570
|
+
options.namespace ?? deriveNamespace(currentPath, options.baseDir);
|
|
571
|
+
runtime._baseDir = options.baseDir;
|
|
572
|
+
runtime._photonFilePath = currentPath;
|
|
573
|
+
if (options.sessionId) {
|
|
574
|
+
runtime._sessionId = options.sessionId;
|
|
575
|
+
}
|
|
576
|
+
const setMCPFactory = (runtime as { setMCPFactory?: (f: MCPClientFactory) => void }).setMCPFactory;
|
|
577
|
+
if (options.mcpFactory && typeof setMCPFactory === 'function') {
|
|
578
|
+
setMCPFactory.call(runtime, options.mcpFactory);
|
|
579
|
+
}
|
|
580
|
+
// Cross-photon calls flow through the same in-process resolver the
|
|
581
|
+
// host instance uses. `loadPhotonInternal` re-assigns `_callHandler`
|
|
582
|
+
// on the host instance below; the runtime facade gets its own copy
|
|
583
|
+
// pointed at the same target resolver so injected callers and
|
|
584
|
+
// extends-Photon callers see identical behaviour.
|
|
585
|
+
runtime._callHandler = async (
|
|
586
|
+
targetPhotonName: string,
|
|
587
|
+
method: string,
|
|
588
|
+
params: Record<string, unknown>,
|
|
589
|
+
) => {
|
|
590
|
+
const targetPath = resolvePhotonPath(targetPhotonName, currentPath, options.baseDir);
|
|
591
|
+
const target = await photon(targetPath, {
|
|
592
|
+
baseDir: options.baseDir,
|
|
593
|
+
mcpFactory: options.mcpFactory,
|
|
594
|
+
sessionId: options.sessionId,
|
|
595
|
+
});
|
|
596
|
+
return (target as Record<string, (p: Record<string, unknown>) => Promise<unknown>>)[method](params);
|
|
597
|
+
};
|
|
598
|
+
// `this.photon.photon.use()` parity: `extends Photon` instances get
|
|
599
|
+
// `_photonResolver` set by the host loader; the injected runtime
|
|
600
|
+
// needs the same resolver so dynamic photon access works identically
|
|
601
|
+
// through both consumption modes.
|
|
602
|
+
runtime._photonResolver = async (name: string, instanceName?: string) => {
|
|
603
|
+
const targetPath = resolvePhotonPath(name, currentPath, options.baseDir);
|
|
604
|
+
return photon(targetPath, {
|
|
605
|
+
baseDir: options.baseDir,
|
|
606
|
+
mcpFactory: options.mcpFactory,
|
|
607
|
+
sessionId: options.sessionId,
|
|
608
|
+
instanceName,
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
return runtime;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const CFENV_HINT =
|
|
615
|
+
'This photon imports `CloudflareEnv` from "@portel/photon" but no CF ' +
|
|
616
|
+
'env was attached. Run via `photon host run` (miniflare-backed) or ' +
|
|
617
|
+
'deploy with `photon host deploy cloudflare`. Outside CF, this ' +
|
|
618
|
+
'photon\'s CF-dependent methods cannot execute.';
|
|
619
|
+
|
|
620
|
+
function makeThrowingCloudflareEnv(): Record<string, unknown> {
|
|
621
|
+
// Engine-internal property accesses (constructor lookup, async-iterator
|
|
622
|
+
// probing, `await` thenable check, JSON.stringify) must not throw —
|
|
623
|
+
// otherwise, harmless reflection from runtime utilities like
|
|
624
|
+
// `wireReactiveCollections` blows up the photon load before any user
|
|
625
|
+
// code runs. We satisfy those safely and only throw when actual
|
|
626
|
+
// user code reads a binding name.
|
|
627
|
+
const safePassthrough = new Set<string | symbol>([
|
|
628
|
+
'constructor',
|
|
629
|
+
'then',
|
|
630
|
+
'toJSON',
|
|
631
|
+
'toString',
|
|
632
|
+
'valueOf',
|
|
633
|
+
Symbol.toPrimitive,
|
|
634
|
+
Symbol.toStringTag,
|
|
635
|
+
Symbol.iterator,
|
|
636
|
+
Symbol.asyncIterator,
|
|
637
|
+
]);
|
|
638
|
+
return new Proxy(Object.create(null), {
|
|
639
|
+
get(_target, prop) {
|
|
640
|
+
if (safePassthrough.has(prop)) {
|
|
641
|
+
if (prop === 'toString' || prop === Symbol.toPrimitive) {
|
|
642
|
+
return () => '[unconfigured CloudflareEnv]';
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
throw new Error(`CloudflareEnv.${String(prop)} accessed but ${CFENV_HINT}`);
|
|
647
|
+
},
|
|
648
|
+
has(_target, prop) {
|
|
649
|
+
return !safePassthrough.has(prop);
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
469
654
|
// ═══════════════════════════════════════════════════════════════════
|
|
470
655
|
// Reactive collection wiring
|
|
471
656
|
// ═══════════════════════════════════════════════════════════════════
|
package/src/schema-extractor.ts
CHANGED
|
@@ -2888,6 +2888,30 @@ export class SchemaExtractor {
|
|
|
2888
2888
|
const photonMap = new Map(photonDeps.map(d => [d.name, d]));
|
|
2889
2889
|
|
|
2890
2890
|
return params.map(param => {
|
|
2891
|
+
// Framework-recognized typed injections (matched on the parameter
|
|
2892
|
+
// type, not the parameter name). Win ahead of every other rule
|
|
2893
|
+
// because they are unambiguous: a user importing `Photon` /
|
|
2894
|
+
// `Cloudflare` / `CloudflareEnv` and typing a constructor param
|
|
2895
|
+
// with one of those names is asking the loader to wire it.
|
|
2896
|
+
if (isPhotonRuntimeType(param.type)) {
|
|
2897
|
+
return {
|
|
2898
|
+
param,
|
|
2899
|
+
injectionType: 'photonRuntime' as const,
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
if (isCloudflareType(param.type)) {
|
|
2903
|
+
return {
|
|
2904
|
+
param,
|
|
2905
|
+
injectionType: 'cloudflare' as const,
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
if (isCloudflareEnvType(param.type)) {
|
|
2909
|
+
return {
|
|
2910
|
+
param,
|
|
2911
|
+
injectionType: 'cloudflareEnv' as const,
|
|
2912
|
+
};
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2891
2915
|
// Primitives → env var
|
|
2892
2916
|
if (param.isPrimitive) {
|
|
2893
2917
|
const envVarName = this.toEnvVarName(mcpName, param.name);
|
|
@@ -3164,7 +3188,23 @@ export class SchemaExtractor {
|
|
|
3164
3188
|
/**
|
|
3165
3189
|
* Capability types that can be auto-detected from source code
|
|
3166
3190
|
*/
|
|
3167
|
-
export type PhotonCapability =
|
|
3191
|
+
export type PhotonCapability =
|
|
3192
|
+
| 'emit'
|
|
3193
|
+
| 'memory'
|
|
3194
|
+
| 'call'
|
|
3195
|
+
| 'mcp'
|
|
3196
|
+
| 'lock'
|
|
3197
|
+
| 'instanceMeta'
|
|
3198
|
+
| 'allInstances'
|
|
3199
|
+
| 'caller'
|
|
3200
|
+
// Forgiving auto-inject: when these are detected on a class that
|
|
3201
|
+
// didn't declare a constructor param of the matching type, the loader
|
|
3202
|
+
// assigns the matching field on the instance after construction so
|
|
3203
|
+
// `this.cf.kv()` / `this.cfEnv.MY_KV.put(...)` work without the user
|
|
3204
|
+
// having to wire the import + ctor param themselves. The diagnostic
|
|
3205
|
+
// remains clear when the matching CF runtime isn't actually attached.
|
|
3206
|
+
| 'cloudflare'
|
|
3207
|
+
| 'cloudflareEnv';
|
|
3168
3208
|
|
|
3169
3209
|
/**
|
|
3170
3210
|
* Match a `this`-like base in source code. Covers:
|
|
@@ -3194,6 +3234,34 @@ function memberAccess(name: string, trailing: '\\(' | '\\b'): RegExp {
|
|
|
3194
3234
|
return new RegExp(`${THIS_BASE}\\s*\\.\\s*${name}\\s*${trailing}`);
|
|
3195
3235
|
}
|
|
3196
3236
|
|
|
3237
|
+
/**
|
|
3238
|
+
* Constructor-param type matchers for the framework-injected types.
|
|
3239
|
+
* The schema extractor sees parameter types as raw text (e.g. `"Photon"`,
|
|
3240
|
+
* `"Cloudflare"`, `"CloudflareEnv<MyBindings>"`), so we match by string.
|
|
3241
|
+
*
|
|
3242
|
+
* Optional modifiers / unions like `Photon | undefined` are accepted so
|
|
3243
|
+
* authors who write `private photon?: Photon` get the same injection.
|
|
3244
|
+
*/
|
|
3245
|
+
function stripNullish(type: string): string {
|
|
3246
|
+
return type
|
|
3247
|
+
.trim()
|
|
3248
|
+
.replace(/\|\s*undefined\b/g, '')
|
|
3249
|
+
.replace(/\|\s*null\b/g, '')
|
|
3250
|
+
.trim();
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
export function isPhotonRuntimeType(type: string): boolean {
|
|
3254
|
+
return /^Photon$/.test(stripNullish(type));
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
export function isCloudflareType(type: string): boolean {
|
|
3258
|
+
return /^Cloudflare$/.test(stripNullish(type));
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
export function isCloudflareEnvType(type: string): boolean {
|
|
3262
|
+
return /^CloudflareEnv(\s*<[\s\S]+>)?$/.test(stripNullish(type));
|
|
3263
|
+
}
|
|
3264
|
+
|
|
3197
3265
|
/**
|
|
3198
3266
|
* Detect capabilities used by a Photon from its source code.
|
|
3199
3267
|
*
|
|
@@ -3215,5 +3283,10 @@ export function detectCapabilities(source: string): Set<PhotonCapability> {
|
|
|
3215
3283
|
if (memberAccess('instanceMeta', '\\b').test(source)) caps.add('instanceMeta');
|
|
3216
3284
|
if (memberAccess('allInstances', '\\(').test(source)) caps.add('allInstances');
|
|
3217
3285
|
if (memberAccess('caller', '\\b').test(source)) caps.add('caller');
|
|
3286
|
+
// Forgiving auto-inject signals — surface as separate capabilities so
|
|
3287
|
+
// the loader can populate `instance.cf` / `instance.cfEnv` for plain
|
|
3288
|
+
// classes that reference them without the explicit constructor param.
|
|
3289
|
+
if (memberAccess('cf', '\\b').test(source)) caps.add('cloudflare');
|
|
3290
|
+
if (memberAccess('cfEnv', '\\b').test(source)) caps.add('cloudflareEnv');
|
|
3218
3291
|
return caps;
|
|
3219
3292
|
}
|
package/src/types.ts
CHANGED
|
@@ -313,7 +313,22 @@ export interface ConstructorParam {
|
|
|
313
313
|
/**
|
|
314
314
|
* Injection type for constructor parameters
|
|
315
315
|
*/
|
|
316
|
-
export type InjectionType =
|
|
316
|
+
export type InjectionType =
|
|
317
|
+
| 'env'
|
|
318
|
+
| 'mcp'
|
|
319
|
+
| 'photon'
|
|
320
|
+
| 'state'
|
|
321
|
+
// Constructor param typed `Photon` from @portel/photon — loader injects
|
|
322
|
+
// a Photon instance configured identically to the host photon.
|
|
323
|
+
| 'photonRuntime'
|
|
324
|
+
// Constructor param typed `Cloudflare` — loader injects a wrapped CF
|
|
325
|
+
// surface (auto-named bindings) backed by miniflare locally or by the
|
|
326
|
+
// deployed Worker `env`.
|
|
327
|
+
| 'cloudflare'
|
|
328
|
+
// Constructor param typed `CloudflareEnv` / `CloudflareEnv<T>` — loader
|
|
329
|
+
// injects the raw Worker `env`. Escape hatch for service bindings
|
|
330
|
+
// and exotic features the wrapped surface doesn't cover.
|
|
331
|
+
| 'cloudflareEnv';
|
|
317
332
|
|
|
318
333
|
/**
|
|
319
334
|
* Resolved injection info for a constructor parameter
|