@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.
Files changed (61) hide show
  1. package/README.md +61 -0
  2. package/dist/base.d.ts +41 -1
  3. package/dist/base.d.ts.map +1 -1
  4. package/dist/base.js +63 -1
  5. package/dist/base.js.map +1 -1
  6. package/dist/channels/daemon-broker.d.ts +35 -0
  7. package/dist/channels/daemon-broker.d.ts.map +1 -0
  8. package/dist/channels/daemon-broker.js +229 -0
  9. package/dist/channels/daemon-broker.js.map +1 -0
  10. package/dist/channels/http-broker.d.ts +45 -0
  11. package/dist/channels/http-broker.d.ts.map +1 -0
  12. package/dist/channels/http-broker.js +182 -0
  13. package/dist/channels/http-broker.js.map +1 -0
  14. package/dist/channels/index.d.ts +53 -0
  15. package/dist/channels/index.d.ts.map +1 -0
  16. package/dist/channels/index.js +67 -0
  17. package/dist/channels/index.js.map +1 -0
  18. package/dist/channels/noop-broker.d.ts +21 -0
  19. package/dist/channels/noop-broker.d.ts.map +1 -0
  20. package/dist/channels/noop-broker.js +38 -0
  21. package/dist/channels/noop-broker.js.map +1 -0
  22. package/dist/channels/redis-broker.d.ts +45 -0
  23. package/dist/channels/redis-broker.d.ts.map +1 -0
  24. package/dist/channels/redis-broker.js +214 -0
  25. package/dist/channels/redis-broker.js.map +1 -0
  26. package/dist/channels/registry.d.ts +49 -0
  27. package/dist/channels/registry.d.ts.map +1 -0
  28. package/dist/channels/registry.js +150 -0
  29. package/dist/channels/registry.js.map +1 -0
  30. package/dist/channels/types.d.ts +85 -0
  31. package/dist/channels/types.d.ts.map +1 -0
  32. package/dist/channels/types.js +8 -0
  33. package/dist/channels/types.js.map +1 -0
  34. package/dist/decorators.d.ts +48 -0
  35. package/dist/decorators.d.ts.map +1 -0
  36. package/dist/decorators.js +64 -0
  37. package/dist/decorators.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +6 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/schema-extractor.d.ts +23 -2
  43. package/dist/schema-extractor.d.ts.map +1 -1
  44. package/dist/schema-extractor.js +88 -3
  45. package/dist/schema-extractor.js.map +1 -1
  46. package/dist/types.d.ts +18 -0
  47. package/dist/types.d.ts.map +1 -1
  48. package/dist/types.js.map +1 -1
  49. package/package.json +5 -3
  50. package/src/base.ts +70 -1
  51. package/src/channels/daemon-broker.ts +271 -0
  52. package/src/channels/http-broker.ts +221 -0
  53. package/src/channels/index.ts +96 -0
  54. package/src/channels/noop-broker.ts +47 -0
  55. package/src/channels/redis-broker.ts +252 -0
  56. package/src/channels/registry.ts +170 -0
  57. package/src/channels/types.ts +95 -0
  58. package/src/decorators.ts +87 -0
  59. package/src/index.ts +13 -0
  60. package/src/schema-extractor.ts +100 -3
  61. 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';