@final-commerce/command-frame 0.0.7 → 0.1.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 +336 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pubsub/index.d.ts +36 -0
- package/dist/pubsub/index.js +52 -0
- package/dist/pubsub/subscriber.d.ts +56 -0
- package/dist/pubsub/subscriber.js +226 -0
- package/dist/pubsub/types.d.ts +42 -0
- package/dist/pubsub/types.js +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -146,6 +146,342 @@ console.log('Current build:', context.buildName);
|
|
|
146
146
|
|
|
147
147
|
For complete usage examples and detailed parameter descriptions, see the documentation for each action in the [Actions Documentation](#actions-documentation) section.
|
|
148
148
|
|
|
149
|
+
## Pub/Sub System
|
|
150
|
+
|
|
151
|
+
The library includes a pub/sub system that allows iframe apps to subscribe to topics and receive events published by the host application (Render).
|
|
152
|
+
|
|
153
|
+
### Quick Start - Pub/Sub
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { topics } from '@final-commerce/command-frame';
|
|
157
|
+
|
|
158
|
+
// Get available topics
|
|
159
|
+
const availableTopics = await topics.getTopics();
|
|
160
|
+
console.log('Available topics:', availableTopics);
|
|
161
|
+
|
|
162
|
+
// Subscribe to a topic with a callback
|
|
163
|
+
const subscriptionId = topics.subscribe('customers', (event) => {
|
|
164
|
+
if (event.type === 'customer-created') {
|
|
165
|
+
console.log('New customer created:', event.data);
|
|
166
|
+
// Handle the event
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Later, unsubscribe when done
|
|
171
|
+
topics.unsubscribe('customers', subscriptionId);
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Pub/Sub API
|
|
175
|
+
|
|
176
|
+
#### `topics.getTopics()`
|
|
177
|
+
|
|
178
|
+
Retrieves the list of available topics from the host application.
|
|
179
|
+
|
|
180
|
+
**Returns:** `Promise<TopicDefinition[]>`
|
|
181
|
+
|
|
182
|
+
**Example:**
|
|
183
|
+
```typescript
|
|
184
|
+
const topics = await topics.getTopics();
|
|
185
|
+
topics.forEach(topic => {
|
|
186
|
+
console.log(`Topic: ${topic.name} (${topic.id})`);
|
|
187
|
+
console.log(`Event types: ${topic.eventTypes.map(et => et.id).join(', ')}`);
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### `topics.subscribe(topic, callback)`
|
|
192
|
+
|
|
193
|
+
Subscribes to a topic and receives events via the callback function.
|
|
194
|
+
|
|
195
|
+
**Parameters:**
|
|
196
|
+
- `topic: string` - The topic ID to subscribe to
|
|
197
|
+
- `callback: (event: TopicEvent) => void` - Function called when an event is received
|
|
198
|
+
|
|
199
|
+
**Returns:** `string` - Subscription ID (use this to unsubscribe)
|
|
200
|
+
|
|
201
|
+
**Example:**
|
|
202
|
+
```typescript
|
|
203
|
+
const subscriptionId = topics.subscribe('customers', (event) => {
|
|
204
|
+
console.log('Received event:', event.type);
|
|
205
|
+
console.log('Event data:', event.data);
|
|
206
|
+
console.log('Timestamp:', event.timestamp);
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### `topics.unsubscribe(topic, subscriptionId)`
|
|
211
|
+
|
|
212
|
+
Unsubscribes from a topic using the subscription ID returned from `subscribe()`.
|
|
213
|
+
|
|
214
|
+
**Parameters:**
|
|
215
|
+
- `topic: string` - The topic ID
|
|
216
|
+
- `subscriptionId: string` - The subscription ID returned from `subscribe()`
|
|
217
|
+
|
|
218
|
+
**Returns:** `boolean` - `true` if successfully unsubscribed
|
|
219
|
+
|
|
220
|
+
**Example:**
|
|
221
|
+
```typescript
|
|
222
|
+
const success = topics.unsubscribe('customers', subscriptionId);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
#### `topics.unsubscribeAll(topic)`
|
|
226
|
+
|
|
227
|
+
Unsubscribes all callbacks for a specific topic.
|
|
228
|
+
|
|
229
|
+
**Parameters:**
|
|
230
|
+
- `topic: string` - The topic ID
|
|
231
|
+
|
|
232
|
+
**Returns:** `number` - Number of subscriptions removed
|
|
233
|
+
|
|
234
|
+
**Example:**
|
|
235
|
+
```typescript
|
|
236
|
+
const removed = topics.unsubscribeAll('customers');
|
|
237
|
+
console.log(`Removed ${removed} subscriptions`);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Topic and Event Types
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
interface TopicDefinition {
|
|
244
|
+
id: string;
|
|
245
|
+
name: string;
|
|
246
|
+
description?: string;
|
|
247
|
+
eventTypes: TopicEventType[];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
interface TopicEvent<T = any> {
|
|
251
|
+
topic: string;
|
|
252
|
+
type: string;
|
|
253
|
+
data: T;
|
|
254
|
+
timestamp: string;
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Example: React Component with Pub/Sub
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
import { useEffect, useState } from 'react';
|
|
262
|
+
import { topics, type TopicEvent } from '@final-commerce/command-frame';
|
|
263
|
+
|
|
264
|
+
function CustomerEvents() {
|
|
265
|
+
const [events, setEvents] = useState<TopicEvent[]>([]);
|
|
266
|
+
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
// Subscribe on mount
|
|
269
|
+
const subscriptionId = topics.subscribe('customers', (event) => {
|
|
270
|
+
if (event.type === 'customer-created') {
|
|
271
|
+
setEvents(prev => [event, ...prev]);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Unsubscribe on unmount
|
|
276
|
+
return () => {
|
|
277
|
+
topics.unsubscribe('customers', subscriptionId);
|
|
278
|
+
};
|
|
279
|
+
}, []);
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div>
|
|
283
|
+
<h2>Customer Events ({events.length})</h2>
|
|
284
|
+
{events.map((event, index) => (
|
|
285
|
+
<div key={index}>
|
|
286
|
+
<p>Type: {event.type}</p>
|
|
287
|
+
<pre>{JSON.stringify(event.data, null, 2)}</pre>
|
|
288
|
+
</div>
|
|
289
|
+
))}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Available Topics
|
|
296
|
+
|
|
297
|
+
#### Customers Topic (`customers`)
|
|
298
|
+
|
|
299
|
+
The customers topic provides events related to customer lifecycle and cart assignment.
|
|
300
|
+
|
|
301
|
+
**Event Types:**
|
|
302
|
+
|
|
303
|
+
1. **`customer-created`** - Fired when a new customer is created
|
|
304
|
+
- **Event Data:**
|
|
305
|
+
```typescript
|
|
306
|
+
{
|
|
307
|
+
customer: {
|
|
308
|
+
_id: string;
|
|
309
|
+
companyId: string;
|
|
310
|
+
email: string;
|
|
311
|
+
firstName: string;
|
|
312
|
+
lastName: string;
|
|
313
|
+
phone?: string;
|
|
314
|
+
tags?: string[];
|
|
315
|
+
metadata?: Record<string, string>[];
|
|
316
|
+
notes?: CustomerNote[];
|
|
317
|
+
billing: Address | null;
|
|
318
|
+
shipping: Address | null;
|
|
319
|
+
createdAt: string;
|
|
320
|
+
updatedAt: string;
|
|
321
|
+
// ... other customer fields
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
2. **`customer-updated`** - Fired when a customer's information is updated
|
|
327
|
+
- **Event Data:**
|
|
328
|
+
```typescript
|
|
329
|
+
{
|
|
330
|
+
customer: {
|
|
331
|
+
// Updated customer object with all fields
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
3. **`customer-note-added`** - Fired when a note is added to a customer
|
|
337
|
+
- **Event Data:**
|
|
338
|
+
```typescript
|
|
339
|
+
{
|
|
340
|
+
customer: {
|
|
341
|
+
// Customer object with updated notes array
|
|
342
|
+
},
|
|
343
|
+
note: {
|
|
344
|
+
createdAt: string;
|
|
345
|
+
message: string;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
4. **`customer-note-deleted`** - Fired when a note is deleted from a customer
|
|
351
|
+
- **Event Data:**
|
|
352
|
+
```typescript
|
|
353
|
+
{
|
|
354
|
+
customer: {
|
|
355
|
+
// Customer object with updated notes array
|
|
356
|
+
},
|
|
357
|
+
note: {
|
|
358
|
+
createdAt: string;
|
|
359
|
+
message: string;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
5. **`customer-assigned`** - Fired when a customer is assigned to the cart
|
|
365
|
+
- **Event Data:**
|
|
366
|
+
```typescript
|
|
367
|
+
{
|
|
368
|
+
customer: {
|
|
369
|
+
// Full customer object
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
6. **`customer-unassigned`** - Fired when a customer is unassigned from the cart
|
|
375
|
+
- **Event Data:**
|
|
376
|
+
```typescript
|
|
377
|
+
{
|
|
378
|
+
customer: {
|
|
379
|
+
// Full customer object (before removal)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Example: Subscribing to Customer Events**
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { topics, type TopicEvent } from '@final-commerce/command-frame';
|
|
388
|
+
|
|
389
|
+
// Subscribe to all customer events
|
|
390
|
+
const subscriptionId = topics.subscribe('customers', (event: TopicEvent) => {
|
|
391
|
+
switch (event.type) {
|
|
392
|
+
case 'customer-created':
|
|
393
|
+
console.log('New customer created:', event.data.customer);
|
|
394
|
+
// Update your customer list, show notification, etc.
|
|
395
|
+
break;
|
|
396
|
+
|
|
397
|
+
case 'customer-updated':
|
|
398
|
+
console.log('Customer updated:', event.data.customer);
|
|
399
|
+
// Refresh customer details in your UI
|
|
400
|
+
break;
|
|
401
|
+
|
|
402
|
+
case 'customer-note-added':
|
|
403
|
+
console.log('Note added to customer:', event.data.customer._id);
|
|
404
|
+
console.log('Note:', event.data.note);
|
|
405
|
+
// Update customer notes display
|
|
406
|
+
break;
|
|
407
|
+
|
|
408
|
+
case 'customer-note-deleted':
|
|
409
|
+
console.log('Note deleted from customer:', event.data.customer._id);
|
|
410
|
+
// Update customer notes display
|
|
411
|
+
break;
|
|
412
|
+
|
|
413
|
+
case 'customer-assigned':
|
|
414
|
+
console.log('Customer assigned to cart:', event.data.customer);
|
|
415
|
+
// Update cart UI to show customer info
|
|
416
|
+
break;
|
|
417
|
+
|
|
418
|
+
case 'customer-unassigned':
|
|
419
|
+
console.log('Customer unassigned from cart:', event.data.customer);
|
|
420
|
+
// Clear customer info from cart UI
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Later, unsubscribe
|
|
426
|
+
topics.unsubscribe('customers', subscriptionId);
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
**Example: Filtering Specific Event Types**
|
|
430
|
+
|
|
431
|
+
```typescript
|
|
432
|
+
import { topics, type TopicEvent } from '@final-commerce/command-frame';
|
|
433
|
+
|
|
434
|
+
// Only listen for customer assignment/unassignment
|
|
435
|
+
const subscriptionId = topics.subscribe('customers', (event: TopicEvent) => {
|
|
436
|
+
if (event.type === 'customer-assigned' || event.type === 'customer-unassigned') {
|
|
437
|
+
console.log(`Customer ${event.type}:`, event.data.customer);
|
|
438
|
+
// Update your cart UI accordingly
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### Host Application (Render) - Publishing Events
|
|
444
|
+
|
|
445
|
+
In the Render application, use the `topicPublisher` to publish events:
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
import { topicPublisher } from '@render/command-frame';
|
|
449
|
+
|
|
450
|
+
// When a customer is created
|
|
451
|
+
topicPublisher.publish('customers', 'customer-created', {
|
|
452
|
+
customer: newCustomer
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
// When a customer is updated
|
|
456
|
+
topicPublisher.publish('customers', 'customer-updated', {
|
|
457
|
+
customer: updatedCustomer
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// When a note is added to a customer
|
|
461
|
+
topicPublisher.publish('customers', 'customer-note-added', {
|
|
462
|
+
customer: updatedCustomer,
|
|
463
|
+
note: newNote
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// When a note is deleted from a customer
|
|
467
|
+
topicPublisher.publish('customers', 'customer-note-deleted', {
|
|
468
|
+
customer: updatedCustomer,
|
|
469
|
+
note: deletedNote
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
// When a customer is assigned to the cart
|
|
473
|
+
topicPublisher.publish('customers', 'customer-assigned', {
|
|
474
|
+
customer: customer
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// When a customer is unassigned from the cart
|
|
478
|
+
topicPublisher.publish('customers', 'customer-unassigned', {
|
|
479
|
+
customer: customer
|
|
480
|
+
});
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
The host application must register topics before they can be used. Topics are registered automatically when the `TopicPublisher` is initialized. See the Render application's pub/sub implementation for details on topic registration.
|
|
484
|
+
|
|
149
485
|
## Actions Documentation
|
|
150
486
|
|
|
151
487
|
Each action has detailed documentation with complete parameter descriptions, response structures, and multiple usage examples:
|
package/dist/index.d.ts
CHANGED
|
@@ -108,3 +108,5 @@ export type { TriggerZapierWebhook, TriggerZapierWebhookParams, TriggerZapierWeb
|
|
|
108
108
|
export * from "./CommonTypes";
|
|
109
109
|
export { commandFrameClient, CommandFrameClient } from "./client";
|
|
110
110
|
export type { PostMessageRequest, PostMessageResponse } from "./client";
|
|
111
|
+
export { topics } from "./pubsub";
|
|
112
|
+
export type { TopicDefinition, TopicEvent, TopicEventType, TopicSubscriptionCallback, TopicSubscription } from "./pubsub/types";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pub/Sub module for Command Frame
|
|
3
|
+
* Provides topic subscription functionality for iframe apps
|
|
4
|
+
*/
|
|
5
|
+
export * from "./types";
|
|
6
|
+
export { TopicSubscriber } from "./subscriber";
|
|
7
|
+
import { TopicSubscriber } from "./subscriber";
|
|
8
|
+
/**
|
|
9
|
+
* Get or create the singleton TopicSubscriber instance
|
|
10
|
+
*/
|
|
11
|
+
export declare function getTopicSubscriber(options?: {
|
|
12
|
+
origin?: string;
|
|
13
|
+
debug?: boolean;
|
|
14
|
+
}): TopicSubscriber;
|
|
15
|
+
/**
|
|
16
|
+
* Topics API for iframe apps
|
|
17
|
+
*/
|
|
18
|
+
declare const topicsApi: {
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe to a topic
|
|
21
|
+
*/
|
|
22
|
+
subscribe: <T = any>(topic: string, callback: (event: import("./types").TopicEvent<T>) => void) => string;
|
|
23
|
+
/**
|
|
24
|
+
* Unsubscribe from a topic
|
|
25
|
+
*/
|
|
26
|
+
unsubscribe: (topic: string, subscriptionId: string) => boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Unsubscribe all callbacks for a topic
|
|
29
|
+
*/
|
|
30
|
+
unsubscribeAll: (topic: string) => number;
|
|
31
|
+
/**
|
|
32
|
+
* Get available topics
|
|
33
|
+
*/
|
|
34
|
+
getTopics: () => Promise<import("./types").TopicDefinition[]>;
|
|
35
|
+
};
|
|
36
|
+
export { topicsApi as topics };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pub/Sub module for Command Frame
|
|
3
|
+
* Provides topic subscription functionality for iframe apps
|
|
4
|
+
*/
|
|
5
|
+
export * from "./types";
|
|
6
|
+
export { TopicSubscriber } from "./subscriber";
|
|
7
|
+
// Singleton instance
|
|
8
|
+
import { TopicSubscriber } from "./subscriber";
|
|
9
|
+
let subscriberInstance = null;
|
|
10
|
+
/**
|
|
11
|
+
* Get or create the singleton TopicSubscriber instance
|
|
12
|
+
*/
|
|
13
|
+
export function getTopicSubscriber(options) {
|
|
14
|
+
if (!subscriberInstance) {
|
|
15
|
+
subscriberInstance = new TopicSubscriber(options);
|
|
16
|
+
}
|
|
17
|
+
return subscriberInstance;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Topics API for iframe apps
|
|
21
|
+
*/
|
|
22
|
+
const topicsApi = {
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to a topic
|
|
25
|
+
*/
|
|
26
|
+
subscribe: (topic, callback) => {
|
|
27
|
+
const subscriber = getTopicSubscriber();
|
|
28
|
+
return subscriber.subscribe(topic, callback);
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Unsubscribe from a topic
|
|
32
|
+
*/
|
|
33
|
+
unsubscribe: (topic, subscriptionId) => {
|
|
34
|
+
const subscriber = getTopicSubscriber();
|
|
35
|
+
return subscriber.unsubscribe(topic, subscriptionId);
|
|
36
|
+
},
|
|
37
|
+
/**
|
|
38
|
+
* Unsubscribe all callbacks for a topic
|
|
39
|
+
*/
|
|
40
|
+
unsubscribeAll: (topic) => {
|
|
41
|
+
const subscriber = getTopicSubscriber();
|
|
42
|
+
return subscriber.unsubscribeAll(topic);
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* Get available topics
|
|
46
|
+
*/
|
|
47
|
+
getTopics: async () => {
|
|
48
|
+
const subscriber = getTopicSubscriber();
|
|
49
|
+
return await subscriber.getTopics();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export { topicsApi as topics };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic Subscriber for iframe communication
|
|
3
|
+
* Manages subscriptions to topics and receives events from the host window
|
|
4
|
+
*/
|
|
5
|
+
import type { TopicDefinition, TopicSubscriptionCallback } from "./types";
|
|
6
|
+
export declare class TopicSubscriber {
|
|
7
|
+
private subscriptions;
|
|
8
|
+
private topics;
|
|
9
|
+
private origin;
|
|
10
|
+
private debug;
|
|
11
|
+
private useGlobalDebug;
|
|
12
|
+
private boundHandleMessage;
|
|
13
|
+
private subscriptionIdCounter;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
origin?: string;
|
|
16
|
+
debug?: boolean;
|
|
17
|
+
});
|
|
18
|
+
private isDebugEnabled;
|
|
19
|
+
/**
|
|
20
|
+
* Request the list of available topics from the host
|
|
21
|
+
*/
|
|
22
|
+
private requestTopics;
|
|
23
|
+
/**
|
|
24
|
+
* Subscribe to a topic with a callback
|
|
25
|
+
* Returns a subscription ID that can be used to unsubscribe
|
|
26
|
+
*/
|
|
27
|
+
subscribe<T = any>(topic: string, callback: TopicSubscriptionCallback<T>): string;
|
|
28
|
+
/**
|
|
29
|
+
* Unsubscribe from a topic using subscription ID
|
|
30
|
+
*/
|
|
31
|
+
unsubscribe(topic: string, subscriptionId: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Unsubscribe all callbacks for a topic
|
|
34
|
+
*/
|
|
35
|
+
unsubscribeAll(topic: string): number;
|
|
36
|
+
/**
|
|
37
|
+
* Get list of available topics
|
|
38
|
+
*/
|
|
39
|
+
getTopics(): Promise<TopicDefinition[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Notify host about subscription changes
|
|
42
|
+
*/
|
|
43
|
+
private notifySubscription;
|
|
44
|
+
/**
|
|
45
|
+
* Handle incoming messages from host
|
|
46
|
+
*/
|
|
47
|
+
private handleMessage;
|
|
48
|
+
/**
|
|
49
|
+
* Handle incoming topic event and dispatch to callbacks
|
|
50
|
+
*/
|
|
51
|
+
private handleTopicEvent;
|
|
52
|
+
/**
|
|
53
|
+
* Cleanup and destroy the subscriber
|
|
54
|
+
*/
|
|
55
|
+
destroy(): void;
|
|
56
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic Subscriber for iframe communication
|
|
3
|
+
* Manages subscriptions to topics and receives events from the host window
|
|
4
|
+
*/
|
|
5
|
+
export class TopicSubscriber {
|
|
6
|
+
constructor(options = {}) {
|
|
7
|
+
this.subscriptions = new Map();
|
|
8
|
+
this.topics = [];
|
|
9
|
+
this.subscriptionIdCounter = 0;
|
|
10
|
+
this.origin = options.origin || "*";
|
|
11
|
+
this.debug = options.debug ?? false;
|
|
12
|
+
this.useGlobalDebug = options.debug === undefined;
|
|
13
|
+
// Store bound handler for cleanup
|
|
14
|
+
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
15
|
+
if (typeof window !== "undefined") {
|
|
16
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
17
|
+
}
|
|
18
|
+
// Request topics list on initialization
|
|
19
|
+
this.requestTopics();
|
|
20
|
+
if (this.isDebugEnabled()) {
|
|
21
|
+
console.log("[TopicSubscriber] Initialized", {
|
|
22
|
+
origin: this.origin,
|
|
23
|
+
debug: this.isDebugEnabled()
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
isDebugEnabled() {
|
|
28
|
+
if (!this.useGlobalDebug) {
|
|
29
|
+
return this.debug;
|
|
30
|
+
}
|
|
31
|
+
return typeof window !== "undefined" && window.__POSTMESSAGE_DEBUG__ === true;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Request the list of available topics from the host
|
|
35
|
+
*/
|
|
36
|
+
requestTopics() {
|
|
37
|
+
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
|
|
38
|
+
const message = {
|
|
39
|
+
type: "pubsub-request-topics",
|
|
40
|
+
requestId: `topics_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
41
|
+
};
|
|
42
|
+
if (this.isDebugEnabled()) {
|
|
43
|
+
console.log("[TopicSubscriber] Requesting topics list", message);
|
|
44
|
+
}
|
|
45
|
+
window.parent.postMessage(message, this.origin);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Subscribe to a topic with a callback
|
|
50
|
+
* Returns a subscription ID that can be used to unsubscribe
|
|
51
|
+
*/
|
|
52
|
+
subscribe(topic, callback) {
|
|
53
|
+
const subscriptionId = `sub_${++this.subscriptionIdCounter}_${Date.now()}`;
|
|
54
|
+
if (!this.subscriptions.has(topic)) {
|
|
55
|
+
this.subscriptions.set(topic, []);
|
|
56
|
+
}
|
|
57
|
+
const subscription = {
|
|
58
|
+
id: subscriptionId,
|
|
59
|
+
topic,
|
|
60
|
+
callback: callback
|
|
61
|
+
};
|
|
62
|
+
this.subscriptions.get(topic).push(subscription);
|
|
63
|
+
// Notify host about the subscription
|
|
64
|
+
this.notifySubscription(topic, true);
|
|
65
|
+
if (this.isDebugEnabled()) {
|
|
66
|
+
console.log("[TopicSubscriber] Subscribed to topic", {
|
|
67
|
+
topic,
|
|
68
|
+
subscriptionId,
|
|
69
|
+
totalSubscriptions: this.subscriptions.get(topic).length
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return subscriptionId;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Unsubscribe from a topic using subscription ID
|
|
76
|
+
*/
|
|
77
|
+
unsubscribe(topic, subscriptionId) {
|
|
78
|
+
const topicSubscriptions = this.subscriptions.get(topic);
|
|
79
|
+
if (!topicSubscriptions) {
|
|
80
|
+
if (this.isDebugEnabled()) {
|
|
81
|
+
console.warn("[TopicSubscriber] Topic not found for unsubscribe", { topic });
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const index = topicSubscriptions.findIndex(sub => sub.id === subscriptionId);
|
|
86
|
+
if (index === -1) {
|
|
87
|
+
if (this.isDebugEnabled()) {
|
|
88
|
+
console.warn("[TopicSubscriber] Subscription ID not found", { topic, subscriptionId });
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
topicSubscriptions.splice(index, 1);
|
|
93
|
+
// If no more subscriptions for this topic, remove it
|
|
94
|
+
if (topicSubscriptions.length === 0) {
|
|
95
|
+
this.subscriptions.delete(topic);
|
|
96
|
+
// Notify host about unsubscription
|
|
97
|
+
this.notifySubscription(topic, false);
|
|
98
|
+
}
|
|
99
|
+
if (this.isDebugEnabled()) {
|
|
100
|
+
console.log("[TopicSubscriber] Unsubscribed from topic", {
|
|
101
|
+
topic,
|
|
102
|
+
subscriptionId,
|
|
103
|
+
remainingSubscriptions: topicSubscriptions.length
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Unsubscribe all callbacks for a topic
|
|
110
|
+
*/
|
|
111
|
+
unsubscribeAll(topic) {
|
|
112
|
+
const topicSubscriptions = this.subscriptions.get(topic);
|
|
113
|
+
if (!topicSubscriptions) {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
const count = topicSubscriptions.length;
|
|
117
|
+
this.subscriptions.delete(topic);
|
|
118
|
+
this.notifySubscription(topic, false);
|
|
119
|
+
if (this.isDebugEnabled()) {
|
|
120
|
+
console.log("[TopicSubscriber] Unsubscribed all from topic", { topic, count });
|
|
121
|
+
}
|
|
122
|
+
return count;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get list of available topics
|
|
126
|
+
*/
|
|
127
|
+
async getTopics() {
|
|
128
|
+
// Request fresh topics list
|
|
129
|
+
this.requestTopics();
|
|
130
|
+
// Return cached topics (host will send updated list via message)
|
|
131
|
+
return [...this.topics];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Notify host about subscription changes
|
|
135
|
+
*/
|
|
136
|
+
notifySubscription(topic, isSubscribed) {
|
|
137
|
+
if (typeof window !== "undefined" && window.parent && window.parent !== window) {
|
|
138
|
+
const message = {
|
|
139
|
+
type: isSubscribed ? "pubsub-subscribe" : "pubsub-unsubscribe",
|
|
140
|
+
topic,
|
|
141
|
+
requestId: `sub_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
142
|
+
};
|
|
143
|
+
if (this.isDebugEnabled()) {
|
|
144
|
+
console.log("[TopicSubscriber] Notifying subscription change", message);
|
|
145
|
+
}
|
|
146
|
+
window.parent.postMessage(message, this.origin);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Handle incoming messages from host
|
|
151
|
+
*/
|
|
152
|
+
handleMessage(event) {
|
|
153
|
+
if (this.origin !== "*" && event.origin !== this.origin) {
|
|
154
|
+
if (this.isDebugEnabled()) {
|
|
155
|
+
console.warn("[TopicSubscriber] Origin mismatch", {
|
|
156
|
+
expected: this.origin,
|
|
157
|
+
received: event.origin
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const data = event.data;
|
|
163
|
+
// Handle topic event
|
|
164
|
+
if (data && data.type === "pubsub-event") {
|
|
165
|
+
const eventMessage = data;
|
|
166
|
+
this.handleTopicEvent(eventMessage.payload);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// Handle topics list
|
|
170
|
+
if (data && data.type === "pubsub-topics-list") {
|
|
171
|
+
const topicsMessage = data;
|
|
172
|
+
this.topics = topicsMessage.payload || [];
|
|
173
|
+
if (this.isDebugEnabled()) {
|
|
174
|
+
console.log("[TopicSubscriber] Received topics list", this.topics);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Handle incoming topic event and dispatch to callbacks
|
|
181
|
+
*/
|
|
182
|
+
handleTopicEvent(event) {
|
|
183
|
+
const topicSubscriptions = this.subscriptions.get(event.topic);
|
|
184
|
+
if (!topicSubscriptions || topicSubscriptions.length === 0) {
|
|
185
|
+
if (this.isDebugEnabled()) {
|
|
186
|
+
console.warn("[TopicSubscriber] Received event for topic with no subscriptions", {
|
|
187
|
+
topic: event.topic
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (this.isDebugEnabled()) {
|
|
193
|
+
console.log("[TopicSubscriber] Dispatching event to callbacks", {
|
|
194
|
+
topic: event.topic,
|
|
195
|
+
type: event.type,
|
|
196
|
+
subscriptionCount: topicSubscriptions.length
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Call all callbacks for this topic
|
|
200
|
+
topicSubscriptions.forEach(subscription => {
|
|
201
|
+
try {
|
|
202
|
+
subscription.callback(event);
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error("[TopicSubscriber] Error in subscription callback", {
|
|
206
|
+
topic: event.topic,
|
|
207
|
+
subscriptionId: subscription.id,
|
|
208
|
+
error
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Cleanup and destroy the subscriber
|
|
215
|
+
*/
|
|
216
|
+
destroy() {
|
|
217
|
+
this.subscriptions.clear();
|
|
218
|
+
this.topics = [];
|
|
219
|
+
if (typeof window !== "undefined") {
|
|
220
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
221
|
+
}
|
|
222
|
+
if (this.isDebugEnabled()) {
|
|
223
|
+
console.log("[TopicSubscriber] Destroyed");
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pub/Sub Types for Command Frame
|
|
3
|
+
* Defines topic and event structures for pub/sub communication
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Event type definition for a topic
|
|
7
|
+
*/
|
|
8
|
+
export interface TopicEventType {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Topic definition with metadata
|
|
15
|
+
*/
|
|
16
|
+
export interface TopicDefinition {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description?: string;
|
|
20
|
+
eventTypes: TopicEventType[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Event payload structure
|
|
24
|
+
*/
|
|
25
|
+
export interface TopicEvent<T = any> {
|
|
26
|
+
topic: string;
|
|
27
|
+
type: string;
|
|
28
|
+
data: T;
|
|
29
|
+
timestamp: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Subscription callback function type
|
|
33
|
+
*/
|
|
34
|
+
export type TopicSubscriptionCallback<T = any> = (event: TopicEvent<T>) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Subscription information
|
|
37
|
+
*/
|
|
38
|
+
export interface TopicSubscription {
|
|
39
|
+
id: string;
|
|
40
|
+
topic: string;
|
|
41
|
+
callback: TopicSubscriptionCallback;
|
|
42
|
+
}
|