@portel/photon-core 2.9.3 → 2.10.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/src/mixins.ts ADDED
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Photon Capabilities Mixin
3
+ *
4
+ * Injects all Photon framework capabilities into any class without requiring inheritance.
5
+ * Works with classes that already extend other bases.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Plain class (no inheritance)
10
+ * export default class Calculator {
11
+ * async add(a: number, b: number) { return a + b; }
12
+ * }
13
+ * // Gets wrapped: withPhotonCapabilities(Calculator)
14
+ * // Now has: this.emit(), this.memory, this.mcp(), this.call()
15
+ *
16
+ * // Class with own parent
17
+ * export default class Todo extends TodoBase {
18
+ * async add(text: string) { ... }
19
+ * }
20
+ * // Gets wrapped: withPhotonCapabilities(Todo)
21
+ * // Still works: this.someBaseMethod() + this.emit() + this.memory
22
+ * ```
23
+ */
24
+
25
+ import { MCPClient, MCPClientFactory, createMCPProxy } from '@portel/mcp';
26
+ import { executionContext } from '@portel/cli';
27
+ import { getBroker } from './channels/index.js';
28
+ import { MemoryProvider } from './memory.js';
29
+
30
+ /**
31
+ * Type for a constructor that may or may not extend Photon base class
32
+ */
33
+ type Constructor<T = any> = new (...args: any[]) => T;
34
+
35
+ /**
36
+ * Injects Photon framework capabilities into any class via mixin composition.
37
+ *
38
+ * Works with:
39
+ * - Plain classes (no parent)
40
+ * - Classes extending user-defined bases
41
+ * - Classes extending imported library classes
42
+ * - Classes already extending Photon (no-op, already has capabilities)
43
+ *
44
+ * @param Base The class to enhance with Photon capabilities
45
+ * @returns Enhanced class with all Photon methods and properties
46
+ */
47
+ export function withPhotonCapabilities<T extends Constructor>(Base: T): T {
48
+ // If already has Photon capabilities, return as-is
49
+ if ((Base.prototype as any)._photonName !== undefined || (Base.prototype as any).emit) {
50
+ return Base;
51
+ }
52
+
53
+ return class PhotonEnhanced extends Base {
54
+ /**
55
+ * Photon name (MCP name) - set by runtime loader
56
+ * @internal
57
+ */
58
+ _photonName?: string;
59
+
60
+ /**
61
+ * Session ID for session-scoped memory - set by runtime
62
+ * @internal
63
+ */
64
+ _sessionId?: string;
65
+
66
+ /**
67
+ * Scoped memory provider - lazy-initialized on first access
68
+ * @internal
69
+ */
70
+ private _memory?: MemoryProvider;
71
+
72
+ /**
73
+ * Cross-photon call handler - injected by runtime
74
+ * @internal
75
+ */
76
+ _callHandler?: (photon: string, method: string, params: Record<string, any>, targetInstance?: string) => Promise<any>;
77
+
78
+ /**
79
+ * MCP client factory - injected by runtime
80
+ * @internal
81
+ */
82
+ protected _mcpFactory?: MCPClientFactory;
83
+
84
+ /**
85
+ * Cache of MCP client instances
86
+ * @internal
87
+ */
88
+ private _mcpClients: Map<string, MCPClient & Record<string, (params?: any) => Promise<any>>> = new Map();
89
+
90
+ /**
91
+ * Scoped key-value storage for photon data
92
+ */
93
+ get memory(): MemoryProvider {
94
+ if (!this._memory) {
95
+ const name = this._photonName || this.constructor.name
96
+ .replace(/MCP$/, '')
97
+ .replace(/([A-Z])/g, '-$1')
98
+ .toLowerCase()
99
+ .replace(/^-/, '');
100
+ this._memory = new MemoryProvider(name, this._sessionId);
101
+ }
102
+ return this._memory;
103
+ }
104
+
105
+ /**
106
+ * Emit an event/progress update
107
+ */
108
+ protected emit(data: any): void {
109
+ const store = executionContext.getStore();
110
+
111
+ const emitData = this._photonName && typeof data === 'object' && data !== null
112
+ ? { ...data, _source: this._photonName }
113
+ : data;
114
+
115
+ if (store?.outputHandler) {
116
+ store.outputHandler(emitData);
117
+ }
118
+
119
+ if (data && typeof data.channel === 'string') {
120
+ const broker = getBroker();
121
+ broker.publish({
122
+ channel: data.channel,
123
+ event: data.event || 'message',
124
+ data: data.data !== undefined ? data.data : data,
125
+ timestamp: Date.now(),
126
+ source: this.constructor.name,
127
+ }).catch((err) => {
128
+ if (process.env.PHOTON_DEBUG) {
129
+ console.error('Channel publish error:', err);
130
+ }
131
+ });
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Call another photon's method through the daemon
137
+ */
138
+ protected async call(target: string, params: Record<string, any> = {}, options?: { instance?: string }): Promise<any> {
139
+ const dotIndex = target.indexOf('.');
140
+ if (dotIndex === -1) {
141
+ throw new Error(
142
+ `Invalid call target: '${target}'. Expected format: 'photonName.methodName'`
143
+ );
144
+ }
145
+
146
+ const photonName = target.slice(0, dotIndex);
147
+ const methodName = target.slice(dotIndex + 1);
148
+
149
+ if (!this._callHandler) {
150
+ throw new Error(
151
+ `Cross-photon calls not available. To use this.call('${target}'), the Photon must be run in a runtime with a daemon.`
152
+ );
153
+ }
154
+
155
+ return this._callHandler(photonName, methodName, params, options?.instance);
156
+ }
157
+
158
+ /**
159
+ * Get an MCP client for calling external MCP servers
160
+ */
161
+ mcp(mcpName: string): MCPClient & Record<string, (params?: any) => Promise<any>> {
162
+ if (!this._mcpFactory) {
163
+ throw new Error(
164
+ `MCP access not available. To use this.mcp('${mcpName}'), the Photon must be run in a runtime that supports MCP access.`
165
+ );
166
+ }
167
+
168
+ let client = this._mcpClients.get(mcpName);
169
+ if (client) {
170
+ return client;
171
+ }
172
+
173
+ const rawClient = this._mcpFactory.create(mcpName);
174
+ client = createMCPProxy(rawClient);
175
+ this._mcpClients.set(mcpName, client);
176
+ return client;
177
+ }
178
+
179
+ /**
180
+ * Set the MCP client factory
181
+ * @internal
182
+ */
183
+ setMCPFactory(factory: MCPClientFactory): void {
184
+ this._mcpFactory = factory;
185
+ }
186
+ } as T;
187
+ }