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