@portel/photon-core 2.1.2 → 2.3.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 +61 -0
- package/dist/base.d.ts +42 -2
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +75 -7
- package/dist/base.js.map +1 -1
- package/dist/channels/daemon-broker.d.ts +35 -0
- package/dist/channels/daemon-broker.d.ts.map +1 -0
- package/dist/channels/daemon-broker.js +229 -0
- package/dist/channels/daemon-broker.js.map +1 -0
- package/dist/channels/http-broker.d.ts +45 -0
- package/dist/channels/http-broker.d.ts.map +1 -0
- package/dist/channels/http-broker.js +182 -0
- package/dist/channels/http-broker.js.map +1 -0
- package/dist/channels/index.d.ts +53 -0
- package/dist/channels/index.d.ts.map +1 -0
- package/dist/channels/index.js +67 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/noop-broker.d.ts +21 -0
- package/dist/channels/noop-broker.d.ts.map +1 -0
- package/dist/channels/noop-broker.js +38 -0
- package/dist/channels/noop-broker.js.map +1 -0
- package/dist/channels/redis-broker.d.ts +45 -0
- package/dist/channels/redis-broker.d.ts.map +1 -0
- package/dist/channels/redis-broker.js +214 -0
- package/dist/channels/redis-broker.js.map +1 -0
- package/dist/channels/registry.d.ts +49 -0
- package/dist/channels/registry.d.ts.map +1 -0
- package/dist/channels/registry.js +150 -0
- package/dist/channels/registry.js.map +1 -0
- package/dist/channels/types.d.ts +85 -0
- package/dist/channels/types.d.ts.map +1 -0
- package/dist/channels/types.js +8 -0
- package/dist/channels/types.js.map +1 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +117 -0
- package/dist/config.js.map +1 -0
- package/dist/decorators.d.ts +48 -0
- package/dist/decorators.d.ts.map +1 -0
- package/dist/decorators.js +64 -0
- package/dist/decorators.js.map +1 -0
- package/dist/design-system/tokens.d.ts +66 -0
- package/dist/design-system/tokens.d.ts.map +1 -1
- package/dist/design-system/tokens.js +324 -44
- package/dist/design-system/tokens.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-apps.d.ts +130 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +87 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/schema-extractor.d.ts +41 -3
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +166 -14
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +91 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +15 -3
- package/src/base.ts +82 -6
- package/src/channels/daemon-broker.ts +271 -0
- package/src/channels/http-broker.ts +221 -0
- package/src/channels/index.ts +96 -0
- package/src/channels/noop-broker.ts +47 -0
- package/src/channels/redis-broker.ts +252 -0
- package/src/channels/registry.ts +170 -0
- package/src/channels/types.ts +95 -0
- package/src/config.ts +134 -0
- package/src/decorators.ts +87 -0
- package/src/design-system/tokens.ts +381 -57
- package/src/index.ts +39 -0
- package/src/mcp-apps.ts +204 -0
- package/src/schema-extractor.ts +191 -15
- package/src/types.ts +103 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Channel Broker
|
|
3
|
+
*
|
|
4
|
+
* Uses Redis pub/sub for cross-process and cross-server messaging.
|
|
5
|
+
*
|
|
6
|
+
* Best for:
|
|
7
|
+
* - Multi-server deployments
|
|
8
|
+
* - Cloud environments (AWS, GCP, etc.)
|
|
9
|
+
* - High-throughput scenarios
|
|
10
|
+
*
|
|
11
|
+
* Requires: ioredis package (optional dependency)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ChannelBroker, ChannelMessage, ChannelHandler, Subscription } from './types.js';
|
|
15
|
+
import { registerBroker } from './registry.js';
|
|
16
|
+
|
|
17
|
+
export interface RedisBrokerOptions {
|
|
18
|
+
/** Redis connection URL (redis://host:port) */
|
|
19
|
+
url?: string;
|
|
20
|
+
/** Channel prefix for namespacing */
|
|
21
|
+
prefix?: string;
|
|
22
|
+
/** Redis client options (passed to ioredis) */
|
|
23
|
+
clientOptions?: Record<string, unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Lazy-load Redis to make it optional
|
|
27
|
+
let Redis: any = null;
|
|
28
|
+
|
|
29
|
+
async function getRedis(): Promise<any> {
|
|
30
|
+
if (!Redis) {
|
|
31
|
+
try {
|
|
32
|
+
// Dynamic import to avoid bundling redis if not used
|
|
33
|
+
// @ts-ignore - ioredis is an optional peer dependency
|
|
34
|
+
const module = await import('ioredis');
|
|
35
|
+
Redis = module.default || module;
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(
|
|
38
|
+
'Redis broker requires ioredis package. Install it with: npm install ioredis'
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return Redis;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class RedisBroker implements ChannelBroker {
|
|
46
|
+
readonly type = 'redis';
|
|
47
|
+
|
|
48
|
+
private url: string;
|
|
49
|
+
private prefix: string;
|
|
50
|
+
private clientOptions: Record<string, unknown>;
|
|
51
|
+
private pubClient: any = null;
|
|
52
|
+
private subClient: any = null;
|
|
53
|
+
private connected = false;
|
|
54
|
+
private subscriptions = new Map<string, Set<ChannelHandler>>();
|
|
55
|
+
|
|
56
|
+
constructor(options: RedisBrokerOptions = {}) {
|
|
57
|
+
this.url = options.url || process.env.PHOTON_REDIS_URL || process.env.REDIS_URL || 'redis://localhost:6379';
|
|
58
|
+
this.prefix = options.prefix || process.env.PHOTON_REDIS_PREFIX || 'photon:channel:';
|
|
59
|
+
this.clientOptions = options.clientOptions || {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private getChannelKey(channel: string): string {
|
|
63
|
+
return `${this.prefix}${channel}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async connect(): Promise<void> {
|
|
67
|
+
if (this.connected) return;
|
|
68
|
+
|
|
69
|
+
const RedisClient = await getRedis();
|
|
70
|
+
|
|
71
|
+
// Create publish client
|
|
72
|
+
this.pubClient = new RedisClient(this.url, {
|
|
73
|
+
...this.clientOptions,
|
|
74
|
+
lazyConnect: true,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Create subscribe client (Redis requires separate connections for pub/sub)
|
|
78
|
+
this.subClient = new RedisClient(this.url, {
|
|
79
|
+
...this.clientOptions,
|
|
80
|
+
lazyConnect: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Connect both clients
|
|
84
|
+
await Promise.all([
|
|
85
|
+
this.pubClient.connect(),
|
|
86
|
+
this.subClient.connect(),
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// Handle incoming messages
|
|
90
|
+
this.subClient.on('message', (redisChannel: string, messageStr: string) => {
|
|
91
|
+
// Strip prefix to get original channel
|
|
92
|
+
const channel = redisChannel.startsWith(this.prefix)
|
|
93
|
+
? redisChannel.slice(this.prefix.length)
|
|
94
|
+
: redisChannel;
|
|
95
|
+
|
|
96
|
+
const handlers = this.subscriptions.get(channel);
|
|
97
|
+
if (!handlers || handlers.size === 0) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const message: ChannelMessage = JSON.parse(messageStr);
|
|
101
|
+
handlers.forEach((handler) => {
|
|
102
|
+
try {
|
|
103
|
+
handler(message);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error('Error in channel handler:', err);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('Error parsing channel message:', err);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Handle pattern messages (for wildcard subscriptions)
|
|
114
|
+
this.subClient.on('pmessage', (pattern: string, redisChannel: string, messageStr: string) => {
|
|
115
|
+
const channel = redisChannel.startsWith(this.prefix)
|
|
116
|
+
? redisChannel.slice(this.prefix.length)
|
|
117
|
+
: redisChannel;
|
|
118
|
+
|
|
119
|
+
// Find matching pattern handlers
|
|
120
|
+
for (const [subPattern, handlers] of this.subscriptions) {
|
|
121
|
+
if (this.matchPattern(subPattern, channel)) {
|
|
122
|
+
try {
|
|
123
|
+
const message: ChannelMessage = JSON.parse(messageStr);
|
|
124
|
+
handlers.forEach((handler) => {
|
|
125
|
+
try {
|
|
126
|
+
handler(message);
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('Error in channel handler:', err);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('Error parsing channel message:', err);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.connected = true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Simple wildcard pattern matching
|
|
143
|
+
* Supports * for single-segment wildcard
|
|
144
|
+
*/
|
|
145
|
+
private matchPattern(pattern: string, channel: string): boolean {
|
|
146
|
+
if (!pattern.includes('*')) {
|
|
147
|
+
return pattern === channel;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const patternParts = pattern.split(':');
|
|
151
|
+
const channelParts = channel.split(':');
|
|
152
|
+
|
|
153
|
+
if (patternParts.length !== channelParts.length) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return patternParts.every((part, i) => part === '*' || part === channelParts[i]);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async disconnect(): Promise<void> {
|
|
161
|
+
if (!this.connected) return;
|
|
162
|
+
|
|
163
|
+
// Unsubscribe from all channels
|
|
164
|
+
for (const channel of this.subscriptions.keys()) {
|
|
165
|
+
const redisChannel = this.getChannelKey(channel);
|
|
166
|
+
if (channel.includes('*')) {
|
|
167
|
+
await this.subClient.punsubscribe(redisChannel);
|
|
168
|
+
} else {
|
|
169
|
+
await this.subClient.unsubscribe(redisChannel);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
this.subscriptions.clear();
|
|
173
|
+
|
|
174
|
+
// Disconnect clients
|
|
175
|
+
await Promise.all([
|
|
176
|
+
this.pubClient?.quit(),
|
|
177
|
+
this.subClient?.quit(),
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
this.pubClient = null;
|
|
181
|
+
this.subClient = null;
|
|
182
|
+
this.connected = false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async publish(message: ChannelMessage): Promise<void> {
|
|
186
|
+
if (!this.connected) {
|
|
187
|
+
await this.connect();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const redisChannel = this.getChannelKey(message.channel);
|
|
191
|
+
const payload: ChannelMessage = {
|
|
192
|
+
...message,
|
|
193
|
+
timestamp: message.timestamp || Date.now(),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
await this.pubClient.publish(redisChannel, JSON.stringify(payload));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async subscribe(channel: string, handler: ChannelHandler): Promise<Subscription> {
|
|
200
|
+
if (!this.connected) {
|
|
201
|
+
await this.connect();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const redisChannel = this.getChannelKey(channel);
|
|
205
|
+
const isPattern = channel.includes('*');
|
|
206
|
+
|
|
207
|
+
// Track handler
|
|
208
|
+
if (!this.subscriptions.has(channel)) {
|
|
209
|
+
this.subscriptions.set(channel, new Set());
|
|
210
|
+
|
|
211
|
+
// Subscribe to Redis channel
|
|
212
|
+
if (isPattern) {
|
|
213
|
+
await this.subClient.psubscribe(redisChannel);
|
|
214
|
+
} else {
|
|
215
|
+
await this.subClient.subscribe(redisChannel);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
this.subscriptions.get(channel)!.add(handler);
|
|
219
|
+
|
|
220
|
+
const subscription: Subscription = {
|
|
221
|
+
channel,
|
|
222
|
+
active: true,
|
|
223
|
+
unsubscribe: async () => {
|
|
224
|
+
subscription.active = false;
|
|
225
|
+
const handlers = this.subscriptions.get(channel);
|
|
226
|
+
if (handlers) {
|
|
227
|
+
handlers.delete(handler);
|
|
228
|
+
if (handlers.size === 0) {
|
|
229
|
+
this.subscriptions.delete(channel);
|
|
230
|
+
// Unsubscribe from Redis
|
|
231
|
+
if (isPattern) {
|
|
232
|
+
await this.subClient.punsubscribe(redisChannel);
|
|
233
|
+
} else {
|
|
234
|
+
await this.subClient.unsubscribe(redisChannel);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return subscription;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
isConnected(): boolean {
|
|
245
|
+
return this.connected;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Register the broker
|
|
250
|
+
registerBroker('redis', (options) => new RedisBroker(options as RedisBrokerOptions));
|
|
251
|
+
|
|
252
|
+
export default RedisBroker;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Broker Registry
|
|
3
|
+
*
|
|
4
|
+
* Manages registration and selection of channel brokers.
|
|
5
|
+
* Auto-detects the appropriate broker based on environment configuration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ChannelBroker, BrokerConfig, BrokerFactory } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Registry of available broker factories
|
|
12
|
+
*/
|
|
13
|
+
const brokerFactories = new Map<string, BrokerFactory>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Current active broker instance
|
|
17
|
+
*/
|
|
18
|
+
let activeBroker: ChannelBroker | null = null;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register a broker factory
|
|
22
|
+
* @param type Broker type identifier (e.g., 'redis', 'daemon', 'http')
|
|
23
|
+
* @param factory Factory function that creates the broker
|
|
24
|
+
*/
|
|
25
|
+
export function registerBroker(type: string, factory: BrokerFactory): void {
|
|
26
|
+
brokerFactories.set(type, factory);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a list of registered broker types
|
|
31
|
+
*/
|
|
32
|
+
export function getRegisteredBrokers(): string[] {
|
|
33
|
+
return Array.from(brokerFactories.keys());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a broker instance by type
|
|
38
|
+
* @param type Broker type
|
|
39
|
+
* @param options Type-specific options
|
|
40
|
+
*/
|
|
41
|
+
export function createBroker(type: string, options?: Record<string, unknown>): ChannelBroker {
|
|
42
|
+
const factory = brokerFactories.get(type);
|
|
43
|
+
if (!factory) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Unknown broker type: ${type}. Available types: ${getRegisteredBrokers().join(', ')}`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return factory(options);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Auto-detect and create the appropriate broker based on environment
|
|
53
|
+
*
|
|
54
|
+
* Detection order:
|
|
55
|
+
* 1. PHOTON_CHANNEL_BROKER env var (explicit override)
|
|
56
|
+
* 2. PHOTON_REDIS_URL → Redis broker
|
|
57
|
+
* 3. PHOTON_CHANNEL_HTTP_URL → HTTP broker
|
|
58
|
+
* 4. PHOTON_CLOUDFLARE_* → Cloudflare broker
|
|
59
|
+
* 5. Default → NoOp broker (silent, no-op)
|
|
60
|
+
*/
|
|
61
|
+
export function detectBroker(): ChannelBroker {
|
|
62
|
+
// Explicit broker type override
|
|
63
|
+
const explicitType = process.env.PHOTON_CHANNEL_BROKER;
|
|
64
|
+
if (explicitType) {
|
|
65
|
+
return createBrokerFromEnv(explicitType);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Redis detection
|
|
69
|
+
if (process.env.PHOTON_REDIS_URL || process.env.REDIS_URL) {
|
|
70
|
+
try {
|
|
71
|
+
return createBrokerFromEnv('redis');
|
|
72
|
+
} catch {
|
|
73
|
+
// Redis broker not available, continue
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// HTTP webhook detection
|
|
78
|
+
if (process.env.PHOTON_CHANNEL_HTTP_URL) {
|
|
79
|
+
try {
|
|
80
|
+
return createBrokerFromEnv('http');
|
|
81
|
+
} catch {
|
|
82
|
+
// HTTP broker not available, continue
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Cloudflare detection
|
|
87
|
+
if (process.env.PHOTON_CLOUDFLARE_ACCOUNT_ID || process.env.CF_ACCOUNT_ID) {
|
|
88
|
+
try {
|
|
89
|
+
return createBrokerFromEnv('cloudflare');
|
|
90
|
+
} catch {
|
|
91
|
+
// Cloudflare broker not available, continue
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Local daemon detection (for local development)
|
|
96
|
+
if (process.env.PHOTON_DAEMON_ENABLED !== 'false') {
|
|
97
|
+
try {
|
|
98
|
+
return createBrokerFromEnv('daemon');
|
|
99
|
+
} catch {
|
|
100
|
+
// Daemon broker not available, continue
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fallback to NoOp
|
|
105
|
+
return createBrokerFromEnv('noop');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Create a broker with configuration from environment variables
|
|
110
|
+
*/
|
|
111
|
+
function createBrokerFromEnv(type: string): ChannelBroker {
|
|
112
|
+
const options: Record<string, unknown> = {};
|
|
113
|
+
|
|
114
|
+
switch (type) {
|
|
115
|
+
case 'redis':
|
|
116
|
+
options.url = process.env.PHOTON_REDIS_URL || process.env.REDIS_URL;
|
|
117
|
+
options.prefix = process.env.PHOTON_REDIS_PREFIX || 'photon:channel:';
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'http':
|
|
121
|
+
options.publishUrl = process.env.PHOTON_CHANNEL_HTTP_URL;
|
|
122
|
+
options.subscribeUrl = process.env.PHOTON_CHANNEL_SSE_URL;
|
|
123
|
+
options.authToken = process.env.PHOTON_CHANNEL_AUTH_TOKEN;
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'cloudflare':
|
|
127
|
+
options.accountId = process.env.PHOTON_CLOUDFLARE_ACCOUNT_ID || process.env.CF_ACCOUNT_ID;
|
|
128
|
+
options.namespaceId = process.env.PHOTON_CLOUDFLARE_DO_NAMESPACE;
|
|
129
|
+
options.apiToken = process.env.PHOTON_CLOUDFLARE_API_TOKEN || process.env.CF_API_TOKEN;
|
|
130
|
+
break;
|
|
131
|
+
|
|
132
|
+
case 'daemon':
|
|
133
|
+
options.photonName = process.env.PHOTON_NAME;
|
|
134
|
+
options.socketDir = process.env.PHOTON_SOCKET_DIR;
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case 'noop':
|
|
138
|
+
default:
|
|
139
|
+
// No configuration needed
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return createBroker(type, options);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get or create the active broker
|
|
148
|
+
* Uses auto-detection on first call, then caches the instance
|
|
149
|
+
*/
|
|
150
|
+
export function getBroker(): ChannelBroker {
|
|
151
|
+
if (!activeBroker) {
|
|
152
|
+
activeBroker = detectBroker();
|
|
153
|
+
}
|
|
154
|
+
return activeBroker;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Set the active broker explicitly
|
|
159
|
+
* @param broker Broker instance to use
|
|
160
|
+
*/
|
|
161
|
+
export function setBroker(broker: ChannelBroker): void {
|
|
162
|
+
activeBroker = broker;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Clear the active broker (for testing)
|
|
167
|
+
*/
|
|
168
|
+
export function clearBroker(): void {
|
|
169
|
+
activeBroker = null;
|
|
170
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel Broker Types
|
|
3
|
+
*
|
|
4
|
+
* Defines the interface for channel-based pub/sub messaging.
|
|
5
|
+
* Implementations can use different backends (local daemon, Redis, HTTP, etc.)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Message published to a channel
|
|
10
|
+
*/
|
|
11
|
+
export interface ChannelMessage {
|
|
12
|
+
/** Channel identifier (e.g., 'board:my-board', 'user:123') */
|
|
13
|
+
channel: string;
|
|
14
|
+
/** Event type (e.g., 'update', 'created', 'deleted') */
|
|
15
|
+
event: string;
|
|
16
|
+
/** Message payload */
|
|
17
|
+
data?: unknown;
|
|
18
|
+
/** Timestamp when message was created */
|
|
19
|
+
timestamp?: number;
|
|
20
|
+
/** Source identifier (photon name, instance id, etc.) */
|
|
21
|
+
source?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Handler function for channel messages
|
|
26
|
+
*/
|
|
27
|
+
export type ChannelHandler = (message: ChannelMessage) => void;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Subscription handle returned by subscribe()
|
|
31
|
+
*/
|
|
32
|
+
export interface Subscription {
|
|
33
|
+
/** Unsubscribe from the channel */
|
|
34
|
+
unsubscribe(): void;
|
|
35
|
+
/** Channel name */
|
|
36
|
+
channel: string;
|
|
37
|
+
/** Whether subscription is active */
|
|
38
|
+
active: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Channel broker interface
|
|
43
|
+
*
|
|
44
|
+
* Implementations provide the actual pub/sub transport mechanism.
|
|
45
|
+
* Examples: Unix socket daemon, Redis, HTTP webhooks, Cloudflare Durable Objects, etc.
|
|
46
|
+
*/
|
|
47
|
+
export interface ChannelBroker {
|
|
48
|
+
/** Broker type identifier */
|
|
49
|
+
readonly type: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Publish a message to a channel
|
|
53
|
+
* @param message The message to publish
|
|
54
|
+
* @returns Promise that resolves when message is sent (not necessarily delivered)
|
|
55
|
+
*/
|
|
56
|
+
publish(message: ChannelMessage): Promise<void>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Subscribe to messages on a channel
|
|
60
|
+
* @param channel Channel to subscribe to (supports wildcards in some implementations)
|
|
61
|
+
* @param handler Function called when messages arrive
|
|
62
|
+
* @returns Subscription handle for unsubscribing
|
|
63
|
+
*/
|
|
64
|
+
subscribe(channel: string, handler: ChannelHandler): Promise<Subscription>;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Check if broker is connected/ready
|
|
68
|
+
*/
|
|
69
|
+
isConnected(): boolean;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Connect to the broker (if applicable)
|
|
73
|
+
*/
|
|
74
|
+
connect?(): Promise<void>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Disconnect from the broker and cleanup resources
|
|
78
|
+
*/
|
|
79
|
+
disconnect?(): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Configuration for broker initialization
|
|
84
|
+
*/
|
|
85
|
+
export interface BrokerConfig {
|
|
86
|
+
/** Broker type (e.g., 'daemon', 'redis', 'http', 'cloudflare') */
|
|
87
|
+
type: string;
|
|
88
|
+
/** Type-specific options */
|
|
89
|
+
options?: Record<string, unknown>;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Factory function type for creating brokers
|
|
94
|
+
*/
|
|
95
|
+
export type BrokerFactory = (config?: BrokerConfig['options']) => ChannelBroker;
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photon Configuration Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides standard config storage for photons that implement the configure() convention.
|
|
5
|
+
* Config is stored at ~/.photon/{photonName}/config.json
|
|
6
|
+
*
|
|
7
|
+
* Usage in a Photon:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { loadPhotonConfig, savePhotonConfig, getPhotonConfigPath } from '@portel/photon-core';
|
|
10
|
+
*
|
|
11
|
+
* export default class MyPhoton extends PhotonMCP {
|
|
12
|
+
* async configure(params: { apiKey: string }) {
|
|
13
|
+
* savePhotonConfig('my-photon', params);
|
|
14
|
+
* return { success: true, config: params };
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* async getConfig() {
|
|
18
|
+
* return loadPhotonConfig('my-photon');
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from 'fs';
|
|
25
|
+
import * as path from 'path';
|
|
26
|
+
import * as os from 'os';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the config directory for photons
|
|
30
|
+
* Default: ~/.photon/
|
|
31
|
+
*/
|
|
32
|
+
export function getPhotonConfigDir(): string {
|
|
33
|
+
return process.env.PHOTON_CONFIG_DIR || path.join(os.homedir(), '.photon');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the config file path for a specific photon
|
|
38
|
+
* @param photonName The photon name (kebab-case)
|
|
39
|
+
* @returns Path to config.json for this photon
|
|
40
|
+
*/
|
|
41
|
+
export function getPhotonConfigPath(photonName: string): string {
|
|
42
|
+
const safeName = photonName.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
43
|
+
return path.join(getPhotonConfigDir(), safeName, 'config.json');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Load configuration for a photon
|
|
48
|
+
* @param photonName The photon name (kebab-case)
|
|
49
|
+
* @param defaults Default values if config doesn't exist
|
|
50
|
+
* @returns The config object or defaults
|
|
51
|
+
*/
|
|
52
|
+
export function loadPhotonConfig<T extends Record<string, any>>(
|
|
53
|
+
photonName: string,
|
|
54
|
+
defaults?: T
|
|
55
|
+
): T {
|
|
56
|
+
const configPath = getPhotonConfigPath(photonName);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
if (fs.existsSync(configPath)) {
|
|
60
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
61
|
+
const config = JSON.parse(content);
|
|
62
|
+
// Merge with defaults
|
|
63
|
+
return defaults ? { ...defaults, ...config } : config;
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Log but don't throw - return defaults
|
|
67
|
+
if (process.env.PHOTON_DEBUG) {
|
|
68
|
+
console.error(`Failed to load config for ${photonName}:`, error);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return defaults || ({} as T);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Save configuration for a photon
|
|
77
|
+
* @param photonName The photon name (kebab-case)
|
|
78
|
+
* @param config The configuration object to save
|
|
79
|
+
*/
|
|
80
|
+
export function savePhotonConfig<T extends Record<string, any>>(
|
|
81
|
+
photonName: string,
|
|
82
|
+
config: T
|
|
83
|
+
): void {
|
|
84
|
+
const configPath = getPhotonConfigPath(photonName);
|
|
85
|
+
const configDir = path.dirname(configPath);
|
|
86
|
+
|
|
87
|
+
// Ensure directory exists
|
|
88
|
+
if (!fs.existsSync(configDir)) {
|
|
89
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a photon has been configured
|
|
97
|
+
* @param photonName The photon name (kebab-case)
|
|
98
|
+
* @returns true if config file exists
|
|
99
|
+
*/
|
|
100
|
+
export function hasPhotonConfig(photonName: string): boolean {
|
|
101
|
+
return fs.existsSync(getPhotonConfigPath(photonName));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Delete configuration for a photon
|
|
106
|
+
* @param photonName The photon name (kebab-case)
|
|
107
|
+
*/
|
|
108
|
+
export function deletePhotonConfig(photonName: string): void {
|
|
109
|
+
const configPath = getPhotonConfigPath(photonName);
|
|
110
|
+
if (fs.existsSync(configPath)) {
|
|
111
|
+
fs.unlinkSync(configPath);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* List all configured photons
|
|
117
|
+
* @returns Array of photon names that have config
|
|
118
|
+
*/
|
|
119
|
+
export function listConfiguredPhotons(): string[] {
|
|
120
|
+
const configDir = getPhotonConfigDir();
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(configDir)) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
return fs.readdirSync(configDir, { withFileTypes: true })
|
|
128
|
+
.filter(entry => entry.isDirectory())
|
|
129
|
+
.filter(entry => fs.existsSync(path.join(configDir, entry.name, 'config.json')))
|
|
130
|
+
.map(entry => entry.name);
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|