@portel/photon-core 2.1.2 → 2.2.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 +41 -1
- package/dist/base.d.ts.map +1 -1
- package/dist/base.js +63 -1
- 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/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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/schema-extractor.d.ts +23 -2
- package/dist/schema-extractor.d.ts.map +1 -1
- package/dist/schema-extractor.js +88 -3
- package/dist/schema-extractor.js.map +1 -1
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +5 -3
- package/src/base.ts +70 -1
- 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/decorators.ts +87 -0
- package/src/index.ts +13 -0
- package/src/schema-extractor.ts +100 -3
- package/src/types.ts +23 -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;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Photon Lock Helpers
|
|
3
|
+
*
|
|
4
|
+
* Runtime support for distributed locking via daemon.
|
|
5
|
+
* The @locked docblock tag and this.withLock() helper both use this.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Lock Manager Interface
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Interface for lock management
|
|
14
|
+
* Implemented by daemon client or other providers
|
|
15
|
+
*/
|
|
16
|
+
export interface LockManager {
|
|
17
|
+
acquire(lockName: string, timeout?: number): Promise<boolean>;
|
|
18
|
+
release(lockName: string): Promise<boolean>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let _lockManager: LockManager | null = null;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Set the global lock manager (called by runtime)
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export function setLockManager(manager: LockManager | null): void {
|
|
28
|
+
_lockManager = manager;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get the current lock manager
|
|
33
|
+
* @internal
|
|
34
|
+
*/
|
|
35
|
+
export function getLockManager(): LockManager | null {
|
|
36
|
+
return _lockManager;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// withLock Helper
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Execute a function with a distributed lock
|
|
45
|
+
*
|
|
46
|
+
* Use this for fine-grained locking within a method, or when you
|
|
47
|
+
* need dynamic lock names.
|
|
48
|
+
*
|
|
49
|
+
* @param lockName Name of the lock to acquire
|
|
50
|
+
* @param fn Function to execute while holding the lock
|
|
51
|
+
* @param timeout Optional lock timeout in ms (default 30000)
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* async moveTask(params: { taskId: string; column: string }) {
|
|
56
|
+
* return this.withLock(`task:${params.taskId}`, async () => {
|
|
57
|
+
* const task = await this.loadTask(params.taskId);
|
|
58
|
+
* task.column = params.column;
|
|
59
|
+
* await this.saveTask(task);
|
|
60
|
+
* return task;
|
|
61
|
+
* });
|
|
62
|
+
* }
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export async function withLock<T>(
|
|
66
|
+
lockName: string,
|
|
67
|
+
fn: () => Promise<T>,
|
|
68
|
+
timeout?: number
|
|
69
|
+
): Promise<T> {
|
|
70
|
+
const lockManager = getLockManager();
|
|
71
|
+
|
|
72
|
+
if (!lockManager) {
|
|
73
|
+
// No lock manager, run without lock
|
|
74
|
+
return fn();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const acquired = await lockManager.acquire(lockName, timeout);
|
|
78
|
+
if (!acquired) {
|
|
79
|
+
throw new Error(`Could not acquire lock: ${lockName}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return await fn();
|
|
84
|
+
} finally {
|
|
85
|
+
await lockManager.release(lockName);
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -321,3 +321,16 @@ export * from './rendering/index.js';
|
|
|
321
321
|
// ===== UCP (Universal Commerce Protocol) =====
|
|
322
322
|
// Agentic commerce with checkout, identity, orders, and AP2 payments
|
|
323
323
|
export * from './ucp/index.js';
|
|
324
|
+
|
|
325
|
+
// ===== CHANNEL-BASED PUB/SUB =====
|
|
326
|
+
// Cross-process messaging with pluggable brokers (daemon, Redis, HTTP, etc.)
|
|
327
|
+
export * from './channels/index.js';
|
|
328
|
+
|
|
329
|
+
// ===== LOCK HELPERS =====
|
|
330
|
+
// Runtime support for distributed locking via daemon
|
|
331
|
+
export {
|
|
332
|
+
withLock,
|
|
333
|
+
setLockManager,
|
|
334
|
+
getLockManager,
|
|
335
|
+
type LockManager,
|
|
336
|
+
} from './decorators.js';
|