@w3streamdev/plugin-core 1.0.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/dist/index.d.mts +217 -0
- package/dist/index.d.ts +217 -0
- package/dist/index.js +613 -0
- package/dist/index.mjs +571 -0
- package/package.json +38 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
// src/plugin-object.ts
|
|
2
|
+
import { EventEmitter } from "eventemitter3";
|
|
3
|
+
|
|
4
|
+
// src/postmessage-bridge.ts
|
|
5
|
+
var PROTOCOL_NAMESPACE = "_w3stream";
|
|
6
|
+
var W3PostMessageBridge = class {
|
|
7
|
+
constructor(plugin, iframe, viewId, meeting, storeSync) {
|
|
8
|
+
this.messageHandler = null;
|
|
9
|
+
this.connected = false;
|
|
10
|
+
this.plugin = plugin;
|
|
11
|
+
this.iframe = iframe;
|
|
12
|
+
this.viewId = viewId;
|
|
13
|
+
this.pluginId = plugin.id;
|
|
14
|
+
this.meeting = meeting;
|
|
15
|
+
this.storeSync = storeSync;
|
|
16
|
+
}
|
|
17
|
+
connect() {
|
|
18
|
+
if (this.connected) return;
|
|
19
|
+
this.connected = true;
|
|
20
|
+
this.messageHandler = this.handleMessage.bind(this);
|
|
21
|
+
window.addEventListener("message", this.messageHandler);
|
|
22
|
+
}
|
|
23
|
+
destroy() {
|
|
24
|
+
if (this.messageHandler) {
|
|
25
|
+
window.removeEventListener("message", this.messageHandler);
|
|
26
|
+
this.messageHandler = null;
|
|
27
|
+
}
|
|
28
|
+
this.connected = false;
|
|
29
|
+
}
|
|
30
|
+
/** Send PLUGIN_DATA to the iframe */
|
|
31
|
+
send(eventName, data) {
|
|
32
|
+
this.iframe.contentWindow?.postMessage(
|
|
33
|
+
{
|
|
34
|
+
type: "PLUGIN_DATA",
|
|
35
|
+
pluginId: this.pluginId,
|
|
36
|
+
viewId: this.viewId,
|
|
37
|
+
eventName,
|
|
38
|
+
data,
|
|
39
|
+
[PROTOCOL_NAMESPACE]: true
|
|
40
|
+
},
|
|
41
|
+
"*"
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
/** Send PLUGIN_CLOSE to the iframe */
|
|
45
|
+
sendClose() {
|
|
46
|
+
this.iframe.contentWindow?.postMessage(
|
|
47
|
+
{
|
|
48
|
+
type: "PLUGIN_CLOSE",
|
|
49
|
+
pluginId: this.pluginId,
|
|
50
|
+
viewId: this.viewId,
|
|
51
|
+
[PROTOCOL_NAMESPACE]: true
|
|
52
|
+
},
|
|
53
|
+
"*"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
/** Send a STORE_UPDATE to this iframe */
|
|
57
|
+
sendStoreUpdate(storeName, key, value, fromPeerId) {
|
|
58
|
+
this.iframe.contentWindow?.postMessage(
|
|
59
|
+
{
|
|
60
|
+
type: "STORE_UPDATE",
|
|
61
|
+
pluginId: this.pluginId,
|
|
62
|
+
storeName,
|
|
63
|
+
key,
|
|
64
|
+
value,
|
|
65
|
+
fromPeerId,
|
|
66
|
+
[PROTOCOL_NAMESPACE]: true
|
|
67
|
+
},
|
|
68
|
+
"*"
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
handleMessage(event) {
|
|
72
|
+
const msg = event.data;
|
|
73
|
+
if (!msg || msg[PROTOCOL_NAMESPACE] !== true) return;
|
|
74
|
+
if (msg["pluginId"] !== this.pluginId) return;
|
|
75
|
+
if (event.source !== this.iframe.contentWindow) return;
|
|
76
|
+
switch (msg["type"]) {
|
|
77
|
+
case "PLUGIN_READY":
|
|
78
|
+
this.sendInit();
|
|
79
|
+
break;
|
|
80
|
+
case "PLUGIN_EVENT": {
|
|
81
|
+
const eventName = msg["eventName"];
|
|
82
|
+
const data = msg["data"];
|
|
83
|
+
this.plugin.emit(eventName, data);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "STORE_POPULATE": {
|
|
87
|
+
const storeName = msg["storeName"];
|
|
88
|
+
if (this.storeSync) {
|
|
89
|
+
const snapshot = this.storeSync.getStoreSnapshot(
|
|
90
|
+
this.pluginId,
|
|
91
|
+
storeName
|
|
92
|
+
);
|
|
93
|
+
this.iframe.contentWindow?.postMessage(
|
|
94
|
+
{
|
|
95
|
+
type: "STORE_DATA",
|
|
96
|
+
pluginId: this.pluginId,
|
|
97
|
+
storeName,
|
|
98
|
+
data: snapshot,
|
|
99
|
+
[PROTOCOL_NAMESPACE]: true
|
|
100
|
+
},
|
|
101
|
+
"*"
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
case "STORE_UPDATE": {
|
|
107
|
+
const storeName = msg["storeName"];
|
|
108
|
+
const key = msg["key"];
|
|
109
|
+
const value = msg["value"];
|
|
110
|
+
const selfPeerId = this.meeting.self?.id ?? "";
|
|
111
|
+
if (this.storeSync) {
|
|
112
|
+
this.storeSync.applyUpdate(
|
|
113
|
+
this.pluginId,
|
|
114
|
+
storeName,
|
|
115
|
+
key,
|
|
116
|
+
value,
|
|
117
|
+
selfPeerId,
|
|
118
|
+
this.viewId
|
|
119
|
+
// origin viewId to avoid echo
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
sendInit() {
|
|
127
|
+
const m = this.meeting;
|
|
128
|
+
const participants = m.participants?.joined?.toArray?.() ?? m.participants?.active?.toArray?.() ?? [];
|
|
129
|
+
const peers = participants.map((p) => ({
|
|
130
|
+
id: p.id ?? "",
|
|
131
|
+
name: p.name ?? "",
|
|
132
|
+
picture: p.picture,
|
|
133
|
+
isHost: false,
|
|
134
|
+
customParticipantId: p.customParticipantId
|
|
135
|
+
}));
|
|
136
|
+
const initMsg = {
|
|
137
|
+
type: "PLUGIN_INIT",
|
|
138
|
+
pluginId: this.pluginId,
|
|
139
|
+
viewId: this.viewId,
|
|
140
|
+
authToken: m.__authToken ?? "",
|
|
141
|
+
meetingId: m.meta?.meetingId ?? "",
|
|
142
|
+
selfPeerId: m.self?.id ?? "",
|
|
143
|
+
selfName: m.self?.name ?? "",
|
|
144
|
+
enabledBy: this.plugin.enabledBy,
|
|
145
|
+
roomName: m.meta?.roomName ?? m.meta?.meetingTitle ?? "",
|
|
146
|
+
peers,
|
|
147
|
+
[PROTOCOL_NAMESPACE]: true
|
|
148
|
+
};
|
|
149
|
+
this.iframe.contentWindow?.postMessage(initMsg, "*");
|
|
150
|
+
this.plugin.emit("ready", { viewId: this.viewId });
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/plugin-object.ts
|
|
155
|
+
var W3_PLUGIN_ACTIVATE = "W3_PLUGIN_ACTIVATE";
|
|
156
|
+
var W3_PLUGIN_DEACTIVATE = "W3_PLUGIN_DEACTIVATE";
|
|
157
|
+
function createW3PluginObject(serverPlugin, meeting, _config, storeSync) {
|
|
158
|
+
const emitter = new EventEmitter();
|
|
159
|
+
const m = meeting;
|
|
160
|
+
const bridges = /* @__PURE__ */ new Map();
|
|
161
|
+
const iframes = /* @__PURE__ */ new Map();
|
|
162
|
+
const plugin = {
|
|
163
|
+
// Static fields from server
|
|
164
|
+
id: serverPlugin.id,
|
|
165
|
+
name: serverPlugin.name,
|
|
166
|
+
description: serverPlugin.description,
|
|
167
|
+
picture: serverPlugin.picture,
|
|
168
|
+
baseURL: serverPlugin.baseURL,
|
|
169
|
+
organizationId: serverPlugin.organizationId,
|
|
170
|
+
published: serverPlugin.published,
|
|
171
|
+
private: serverPlugin.private,
|
|
172
|
+
staggered: serverPlugin.staggered,
|
|
173
|
+
tags: serverPlugin.tags,
|
|
174
|
+
type: serverPlugin.type,
|
|
175
|
+
createdAt: new Date(serverPlugin.createdAt),
|
|
176
|
+
updatedAt: new Date(serverPlugin.updatedAt),
|
|
177
|
+
config: void 0,
|
|
178
|
+
// Runtime state
|
|
179
|
+
active: false,
|
|
180
|
+
enabledBy: "",
|
|
181
|
+
iframes,
|
|
182
|
+
_bridges: bridges,
|
|
183
|
+
// ── activate() — broadcast to all peers via RTK data channel ──
|
|
184
|
+
activate: async () => {
|
|
185
|
+
plugin.active = true;
|
|
186
|
+
plugin.enabledBy = m.self?.id ?? "";
|
|
187
|
+
m.participants?.broadcastMessage?.(W3_PLUGIN_ACTIVATE, {
|
|
188
|
+
pluginId: serverPlugin.id,
|
|
189
|
+
enabledBy: plugin.enabledBy
|
|
190
|
+
});
|
|
191
|
+
emitter.emit("enabled");
|
|
192
|
+
},
|
|
193
|
+
// ── deactivate() — broadcast to all peers ──
|
|
194
|
+
deactivate: async () => {
|
|
195
|
+
plugin.active = false;
|
|
196
|
+
for (const [viewId, bridge] of bridges) {
|
|
197
|
+
bridge.sendClose();
|
|
198
|
+
bridge.destroy();
|
|
199
|
+
storeSync?.unregisterBridge(serverPlugin.id, viewId);
|
|
200
|
+
}
|
|
201
|
+
bridges.clear();
|
|
202
|
+
iframes.clear();
|
|
203
|
+
m.participants?.broadcastMessage?.(W3_PLUGIN_DEACTIVATE, {
|
|
204
|
+
pluginId: serverPlugin.id
|
|
205
|
+
});
|
|
206
|
+
emitter.emit("closed");
|
|
207
|
+
},
|
|
208
|
+
// ── activateForSelf() — local only (staggered plugins) ──
|
|
209
|
+
activateForSelf: async () => {
|
|
210
|
+
plugin.active = true;
|
|
211
|
+
plugin.enabledBy = m.self?.id ?? "";
|
|
212
|
+
emitter.emit("enabled");
|
|
213
|
+
},
|
|
214
|
+
deactivateForSelf: () => {
|
|
215
|
+
plugin.active = false;
|
|
216
|
+
for (const [viewId, bridge] of bridges) {
|
|
217
|
+
bridge.sendClose();
|
|
218
|
+
bridge.destroy();
|
|
219
|
+
storeSync?.unregisterBridge(serverPlugin.id, viewId);
|
|
220
|
+
}
|
|
221
|
+
bridges.clear();
|
|
222
|
+
iframes.clear();
|
|
223
|
+
emitter.emit("closed");
|
|
224
|
+
},
|
|
225
|
+
// Deprecated aliases
|
|
226
|
+
enable: async () => plugin.activateForSelf(),
|
|
227
|
+
disable: () => plugin.deactivateForSelf(),
|
|
228
|
+
// ── addPluginView() — creates a W3PostMessageBridge ──
|
|
229
|
+
addPluginView: (iframe, viewId = "plugin-main") => {
|
|
230
|
+
if (bridges.has(viewId)) {
|
|
231
|
+
bridges.get(viewId).destroy();
|
|
232
|
+
storeSync?.unregisterBridge(serverPlugin.id, viewId);
|
|
233
|
+
}
|
|
234
|
+
iframes.set(viewId, iframe);
|
|
235
|
+
const bridge = new W3PostMessageBridge(
|
|
236
|
+
plugin,
|
|
237
|
+
iframe,
|
|
238
|
+
viewId,
|
|
239
|
+
meeting,
|
|
240
|
+
storeSync
|
|
241
|
+
);
|
|
242
|
+
bridges.set(viewId, bridge);
|
|
243
|
+
if (storeSync) {
|
|
244
|
+
storeSync.registerBridge(serverPlugin.id, viewId, bridge);
|
|
245
|
+
}
|
|
246
|
+
bridge.connect();
|
|
247
|
+
},
|
|
248
|
+
// ── removePluginView() ──
|
|
249
|
+
removePluginView: (viewId = "plugin-main") => {
|
|
250
|
+
const bridge = bridges.get(viewId);
|
|
251
|
+
if (bridge) {
|
|
252
|
+
bridge.destroy();
|
|
253
|
+
bridges.delete(viewId);
|
|
254
|
+
storeSync?.unregisterBridge(serverPlugin.id, viewId);
|
|
255
|
+
}
|
|
256
|
+
iframes.delete(viewId);
|
|
257
|
+
},
|
|
258
|
+
// ── sendData() — sends PLUGIN_DATA to all registered iframes ──
|
|
259
|
+
sendData: (payload) => {
|
|
260
|
+
for (const bridge of bridges.values()) {
|
|
261
|
+
bridge.send(payload.eventName, payload.data);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
sendIframeEvent: (message) => {
|
|
265
|
+
const msg = message;
|
|
266
|
+
if (msg.eventName) {
|
|
267
|
+
plugin.sendData({ eventName: msg.eventName, data: msg.data });
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
// ── EventEmitter delegation ──
|
|
271
|
+
on: (event, cb) => {
|
|
272
|
+
emitter.on(event, cb);
|
|
273
|
+
return plugin;
|
|
274
|
+
},
|
|
275
|
+
off: (event, cb) => {
|
|
276
|
+
emitter.off(event, cb);
|
|
277
|
+
return plugin;
|
|
278
|
+
},
|
|
279
|
+
once: (event, cb) => {
|
|
280
|
+
emitter.once(event, cb);
|
|
281
|
+
return plugin;
|
|
282
|
+
},
|
|
283
|
+
emit: (event, ...args) => emitter.emit(event, ...args),
|
|
284
|
+
addListener: (event, cb) => {
|
|
285
|
+
emitter.on(event, cb);
|
|
286
|
+
return plugin;
|
|
287
|
+
},
|
|
288
|
+
removeListener: (event, cb) => {
|
|
289
|
+
emitter.off(event, cb);
|
|
290
|
+
return plugin;
|
|
291
|
+
},
|
|
292
|
+
removeAllListeners: (event) => {
|
|
293
|
+
if (event) emitter.removeAllListeners(event);
|
|
294
|
+
else emitter.removeAllListeners();
|
|
295
|
+
return plugin;
|
|
296
|
+
},
|
|
297
|
+
listeners: (event) => emitter.listeners(event),
|
|
298
|
+
listenerCount: (event) => emitter.listenerCount(event),
|
|
299
|
+
eventNames: () => emitter.eventNames(),
|
|
300
|
+
setMaxListeners: (_n) => plugin,
|
|
301
|
+
getMaxListeners: () => 10,
|
|
302
|
+
rawListeners: (event) => emitter.listeners(event),
|
|
303
|
+
prependListener: (event, cb) => {
|
|
304
|
+
emitter.on(event, cb);
|
|
305
|
+
return plugin;
|
|
306
|
+
},
|
|
307
|
+
prependOnceListener: (event, cb) => {
|
|
308
|
+
emitter.once(event, cb);
|
|
309
|
+
return plugin;
|
|
310
|
+
},
|
|
311
|
+
// No-op telemetry
|
|
312
|
+
telemetry: {},
|
|
313
|
+
logger: console
|
|
314
|
+
};
|
|
315
|
+
return plugin;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/store-sync.ts
|
|
319
|
+
var W3_STORE_UPDATE = "W3_STORE_UPDATE";
|
|
320
|
+
var StoreSyncManager = class {
|
|
321
|
+
constructor(meeting) {
|
|
322
|
+
/** pluginId → storeName → key → value */
|
|
323
|
+
this.stores = /* @__PURE__ */ new Map();
|
|
324
|
+
/** pluginId → viewId → W3PostMessageBridge */
|
|
325
|
+
this.bridges = /* @__PURE__ */ new Map();
|
|
326
|
+
this.meeting = meeting;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Register a bridge so store updates can be forwarded to its iframe.
|
|
330
|
+
*/
|
|
331
|
+
registerBridge(pluginId, viewId, bridge) {
|
|
332
|
+
if (!this.bridges.has(pluginId)) this.bridges.set(pluginId, /* @__PURE__ */ new Map());
|
|
333
|
+
this.bridges.get(pluginId).set(viewId, bridge);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Unregister a bridge when the plugin view is removed.
|
|
337
|
+
*/
|
|
338
|
+
unregisterBridge(pluginId, viewId) {
|
|
339
|
+
this.bridges.get(pluginId)?.delete(viewId);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Returns the current snapshot of a named store for a plugin.
|
|
343
|
+
*/
|
|
344
|
+
getStoreSnapshot(pluginId, storeName) {
|
|
345
|
+
const pluginStores = this.stores.get(pluginId);
|
|
346
|
+
if (!pluginStores) return {};
|
|
347
|
+
const store = pluginStores.get(storeName);
|
|
348
|
+
if (!store) return {};
|
|
349
|
+
return Object.fromEntries(store.entries());
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Apply a store update from a local iframe:
|
|
353
|
+
* 1. Update the in-memory store
|
|
354
|
+
* 2. Forward to all other local iframes for this plugin (excluding origin)
|
|
355
|
+
* 3. Broadcast to all remote peers via RTK data channel
|
|
356
|
+
*/
|
|
357
|
+
applyUpdate(pluginId, storeName, key, value, fromPeerId, originViewId) {
|
|
358
|
+
if (!this.stores.has(pluginId)) this.stores.set(pluginId, /* @__PURE__ */ new Map());
|
|
359
|
+
const pluginStores = this.stores.get(pluginId);
|
|
360
|
+
if (!pluginStores.has(storeName)) pluginStores.set(storeName, /* @__PURE__ */ new Map());
|
|
361
|
+
pluginStores.get(storeName).set(key, value);
|
|
362
|
+
const pluginBridges = this.bridges.get(pluginId);
|
|
363
|
+
if (pluginBridges) {
|
|
364
|
+
for (const [viewId, bridge] of pluginBridges) {
|
|
365
|
+
if (viewId === originViewId) continue;
|
|
366
|
+
bridge.sendStoreUpdate(storeName, key, value, fromPeerId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
this.meeting.participants?.broadcastMessage?.(W3_STORE_UPDATE, {
|
|
370
|
+
pluginId,
|
|
371
|
+
storeName,
|
|
372
|
+
key,
|
|
373
|
+
value,
|
|
374
|
+
fromPeerId
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Apply a store update received from a remote peer:
|
|
379
|
+
* 1. Update the in-memory store
|
|
380
|
+
* 2. Forward to all local iframes for this plugin
|
|
381
|
+
* (No re-broadcast — the sender already broadcast to all peers)
|
|
382
|
+
*/
|
|
383
|
+
applyRemoteUpdate(pluginId, storeName, key, value, fromPeerId) {
|
|
384
|
+
if (!this.stores.has(pluginId)) this.stores.set(pluginId, /* @__PURE__ */ new Map());
|
|
385
|
+
const pluginStores = this.stores.get(pluginId);
|
|
386
|
+
if (!pluginStores.has(storeName)) pluginStores.set(storeName, /* @__PURE__ */ new Map());
|
|
387
|
+
pluginStores.get(storeName).set(key, value);
|
|
388
|
+
const pluginBridges = this.bridges.get(pluginId);
|
|
389
|
+
if (pluginBridges) {
|
|
390
|
+
for (const bridge of pluginBridges.values()) {
|
|
391
|
+
bridge.sendStoreUpdate(storeName, key, value, fromPeerId);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Listen for incoming store updates from remote peers via RTK data channel.
|
|
397
|
+
* Call this once after constructing the StoreSyncManager.
|
|
398
|
+
*/
|
|
399
|
+
listenForRemoteUpdates() {
|
|
400
|
+
const participants = this.meeting.participants;
|
|
401
|
+
if (!participants?.on) return;
|
|
402
|
+
const handler = (...args) => {
|
|
403
|
+
let data;
|
|
404
|
+
if (args.length >= 2) {
|
|
405
|
+
data = args[1];
|
|
406
|
+
} else if (args.length === 1) {
|
|
407
|
+
data = args[0];
|
|
408
|
+
}
|
|
409
|
+
if (!data) return;
|
|
410
|
+
const payload = data["payload"] ?? data;
|
|
411
|
+
const type = data["type"] ?? payload["type"];
|
|
412
|
+
if (type !== W3_STORE_UPDATE) return;
|
|
413
|
+
const update = payload;
|
|
414
|
+
this.applyRemoteUpdate(
|
|
415
|
+
update.pluginId,
|
|
416
|
+
update.storeName,
|
|
417
|
+
update.key,
|
|
418
|
+
update.value,
|
|
419
|
+
update.fromPeerId
|
|
420
|
+
);
|
|
421
|
+
};
|
|
422
|
+
participants.on("broadcastedMessage", handler);
|
|
423
|
+
participants.on("dataMessage", handler);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/plugin-manager.ts
|
|
428
|
+
var PluginList = class {
|
|
429
|
+
constructor() {
|
|
430
|
+
this.items = [];
|
|
431
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
432
|
+
}
|
|
433
|
+
toArray() {
|
|
434
|
+
return [...this.items];
|
|
435
|
+
}
|
|
436
|
+
addListener(event, callback) {
|
|
437
|
+
if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
438
|
+
this.listeners.get(event).add(callback);
|
|
439
|
+
}
|
|
440
|
+
removeListener(event, callback) {
|
|
441
|
+
this.listeners.get(event)?.delete(callback);
|
|
442
|
+
}
|
|
443
|
+
_setItems(items) {
|
|
444
|
+
this.items = items;
|
|
445
|
+
this.listeners.get("stateUpdate")?.forEach((cb) => cb());
|
|
446
|
+
}
|
|
447
|
+
_addItem(item) {
|
|
448
|
+
this.items.push(item);
|
|
449
|
+
this.listeners.get("stateUpdate")?.forEach((cb) => cb());
|
|
450
|
+
}
|
|
451
|
+
_removeItem(id) {
|
|
452
|
+
this.items = this.items.filter((p) => p.id !== id);
|
|
453
|
+
this.listeners.get("stateUpdate")?.forEach((cb) => cb());
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
async function fetchPluginsFromServer(config) {
|
|
457
|
+
const res = await fetch(`${config.serverURL}/v2/plugins/user`, {
|
|
458
|
+
headers: {
|
|
459
|
+
Authorization: `Bearer ${config.apiKey}`,
|
|
460
|
+
"Content-Type": "application/json",
|
|
461
|
+
...config.devMode ? { "X-Dev-Mode": "true" } : {}
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
if (!res.ok) {
|
|
465
|
+
throw new Error(
|
|
466
|
+
`[plugin-core] Failed to fetch plugins: ${res.status} ${res.statusText}`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const json = await res.json();
|
|
470
|
+
return json.data;
|
|
471
|
+
}
|
|
472
|
+
async function patchMeeting(meeting, config) {
|
|
473
|
+
const m = meeting;
|
|
474
|
+
const serverPlugins = await fetchPluginsFromServer(config);
|
|
475
|
+
const storeSync = new StoreSyncManager(meeting);
|
|
476
|
+
storeSync.listenForRemoteUpdates();
|
|
477
|
+
const pluginObjects = serverPlugins.map(
|
|
478
|
+
(record) => createW3PluginObject(record, meeting, config, storeSync)
|
|
479
|
+
);
|
|
480
|
+
const all = new PluginList();
|
|
481
|
+
all._setItems(pluginObjects);
|
|
482
|
+
const active = new PluginList();
|
|
483
|
+
const pluginsProxy = {
|
|
484
|
+
all,
|
|
485
|
+
active
|
|
486
|
+
};
|
|
487
|
+
Object.defineProperty(m, "plugins", {
|
|
488
|
+
value: pluginsProxy,
|
|
489
|
+
writable: true,
|
|
490
|
+
configurable: true
|
|
491
|
+
});
|
|
492
|
+
if (m.participants?.on) {
|
|
493
|
+
const handleBroadcast = (...args) => {
|
|
494
|
+
let data;
|
|
495
|
+
if (args.length >= 2) {
|
|
496
|
+
data = args[1];
|
|
497
|
+
} else if (args.length === 1) {
|
|
498
|
+
data = args[0];
|
|
499
|
+
}
|
|
500
|
+
if (!data) return;
|
|
501
|
+
const payload = data["payload"] ?? data;
|
|
502
|
+
const type = data["type"] ?? payload["type"];
|
|
503
|
+
const pluginId = payload["pluginId"];
|
|
504
|
+
if (!pluginId) return;
|
|
505
|
+
const plugin = pluginObjects.find((p) => p.id === pluginId);
|
|
506
|
+
if (!plugin) return;
|
|
507
|
+
if (type === W3_PLUGIN_ACTIVATE) {
|
|
508
|
+
plugin.active = true;
|
|
509
|
+
plugin.enabledBy = payload["enabledBy"] ?? "";
|
|
510
|
+
plugin.emit("enabled");
|
|
511
|
+
if (!active.toArray().find((p) => p.id === pluginId)) {
|
|
512
|
+
active._addItem(plugin);
|
|
513
|
+
}
|
|
514
|
+
} else if (type === W3_PLUGIN_DEACTIVATE) {
|
|
515
|
+
plugin.active = false;
|
|
516
|
+
plugin.emit("closed");
|
|
517
|
+
active._removeItem(pluginId);
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
m.participants.on("broadcastedMessage", handleBroadcast);
|
|
521
|
+
m.participants.on("dataMessage", handleBroadcast);
|
|
522
|
+
}
|
|
523
|
+
for (const plugin of pluginObjects) {
|
|
524
|
+
const originalActivate = plugin.activate;
|
|
525
|
+
plugin.activate = async () => {
|
|
526
|
+
await originalActivate();
|
|
527
|
+
if (!active.toArray().find((p) => p.id === plugin.id)) {
|
|
528
|
+
active._addItem(plugin);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
const originalDeactivate = plugin.deactivate;
|
|
532
|
+
plugin.deactivate = async () => {
|
|
533
|
+
await originalDeactivate();
|
|
534
|
+
active._removeItem(plugin.id);
|
|
535
|
+
};
|
|
536
|
+
const originalActivateForSelf = plugin.activateForSelf;
|
|
537
|
+
plugin.activateForSelf = async () => {
|
|
538
|
+
await originalActivateForSelf();
|
|
539
|
+
if (!active.toArray().find((p) => p.id === plugin.id)) {
|
|
540
|
+
active._addItem(plugin);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const originalDeactivateForSelf = plugin.deactivateForSelf;
|
|
544
|
+
plugin.deactivateForSelf = () => {
|
|
545
|
+
originalDeactivateForSelf();
|
|
546
|
+
active._removeItem(plugin.id);
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
async function initWithPlugins(rtkInitOptions, pluginConfig) {
|
|
551
|
+
let RealtimeKitClient;
|
|
552
|
+
try {
|
|
553
|
+
const rtk = await import("@cloudflare/realtimekit");
|
|
554
|
+
RealtimeKitClient = rtk.default ?? rtk;
|
|
555
|
+
} catch {
|
|
556
|
+
throw new Error(
|
|
557
|
+
"[plugin-core] @cloudflare/realtimekit must be installed to use initWithPlugins(). Install it or use patchMeeting() directly after initializing the meeting."
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
const meeting = await RealtimeKitClient.init(rtkInitOptions);
|
|
561
|
+
await patchMeeting(meeting, pluginConfig);
|
|
562
|
+
return meeting;
|
|
563
|
+
}
|
|
564
|
+
export {
|
|
565
|
+
PluginList,
|
|
566
|
+
StoreSyncManager,
|
|
567
|
+
W3PostMessageBridge,
|
|
568
|
+
createW3PluginObject,
|
|
569
|
+
initWithPlugins,
|
|
570
|
+
patchMeeting
|
|
571
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@w3streamdev/plugin-core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Host-side patch layer — wires @cloudflare/realtimekit into your plugin infrastructure",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
18
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"lint": "eslint src",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@cloudflare/realtimekit": ">=0"
|
|
25
|
+
},
|
|
26
|
+
"peerDependenciesMeta": {
|
|
27
|
+
"@cloudflare/realtimekit": { "optional": false }
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"eventemitter3": "^5.0.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@cloudflare/realtimekit": "latest",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.4.0"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|