versacall-core-library-react 2.0.75 → 2.0.76-dev

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.
@@ -11,15 +11,31 @@ var _withCore = _interopRequireDefault(require("../Core/withCore"));
11
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
12
12
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
13
  const namespace = 'ChannelsProvider';
14
- const emitter = require('emitter-io');
14
+
15
+ // ─── Module-level singletons (one set per iframe / JS module scope) ──────────
16
+ //
17
+ // These are intentionally module-scoped so that multiple ChannelsProvider
18
+ // instances within the same iframe bundle share a single set of state — the
19
+ // same as the original design.
20
+ //
21
+ // Cross-iframe deduplication is handled by the SharedWorker: because all
22
+ // iframes load the same bundle, `new URL(...)` resolves to the same hashed
23
+ // URL and the browser routes every new SharedWorker(url) call to the same
24
+ // running worker thread.
25
+
15
26
  const channelTimeoutHandlers = [];
16
27
  const channelInformation = [];
17
28
  const channelFunctions = [];
18
29
  const handlers = [];
19
30
  const connectionChangedHandlers = [];
20
31
  const subscribingHandlers = [];
21
- let client;
22
- let clientInitialized = false; // NEW: Track if client is initialized
32
+
33
+ /** The MessagePort connected to the SharedWorker. Null until first init. */
34
+ let workerPort = null;
35
+ /** Guard: set to true once the worker port has been opened for this iframe. */
36
+ let clientInitialized = false;
37
+
38
+ // ─── Shared utility functions (identical to original) ────────────────────────
23
39
 
24
40
  function compareChannelInfo(ci1, ci2) {
25
41
  if (ci1 !== null) {
@@ -50,9 +66,9 @@ const registerHandlers = (id, handler, connectionChanged, subscribing) => {
50
66
  });
51
67
  }
52
68
  if (connectionChanged) {
53
- const existingConnectionChanged = connectionChangedHandlers.find(x => x.id === id);
54
- if (existingConnectionChanged !== undefined) {
55
- existingConnectionChanged.handler = connectionChanged;
69
+ const existingCC = connectionChangedHandlers.find(x => x.id === id);
70
+ if (existingCC !== undefined) {
71
+ existingCC.handler = connectionChanged;
56
72
  } else {
57
73
  connectionChangedHandlers.push({
58
74
  id: id,
@@ -61,9 +77,9 @@ const registerHandlers = (id, handler, connectionChanged, subscribing) => {
61
77
  }
62
78
  }
63
79
  if (subscribing) {
64
- const existingSubscribing = subscribingHandlers.find(x => x.id === id);
65
- if (existingSubscribing !== undefined) {
66
- existingSubscribing.handler = subscribing;
80
+ const existingSub = subscribingHandlers.find(x => x.id === id);
81
+ if (existingSub !== undefined) {
82
+ existingSub.handler = subscribing;
67
83
  } else {
68
84
  subscribingHandlers.push({
69
85
  id: id,
@@ -77,13 +93,13 @@ const unregisterHandlers = id => {
77
93
  if (existing !== undefined) {
78
94
  handlers.splice(handlers.indexOf(existing), 1);
79
95
  }
80
- const existingConnectionChanged = connectionChangedHandlers.find(x => x.id === id);
81
- if (existingConnectionChanged !== undefined) {
82
- connectionChangedHandlers.splice(connectionChangedHandlers.indexOf(existingConnectionChanged), 1);
96
+ const existingCC = connectionChangedHandlers.find(x => x.id === id);
97
+ if (existingCC !== undefined) {
98
+ connectionChangedHandlers.splice(connectionChangedHandlers.indexOf(existingCC), 1);
83
99
  }
84
- const existingSubscribing = subscribingHandlers.find(x => x.id === id);
85
- if (existingSubscribing !== undefined) {
86
- subscribingHandlers.splice(subscribingHandlers.indexOf(existingSubscribing), 1);
100
+ const existingSub = subscribingHandlers.find(x => x.id === id);
101
+ if (existingSub !== undefined) {
102
+ subscribingHandlers.splice(subscribingHandlers.indexOf(existingSub), 1);
87
103
  }
88
104
  };
89
105
  const registerChannel = (id, channelFunction) => {
@@ -97,10 +113,7 @@ const registerChannel = (id, channelFunction) => {
97
113
  });
98
114
  }
99
115
  };
100
- const isChannelRegistered = id => {
101
- const existing = channelFunctions.find(x => x.id === id);
102
- return existing !== undefined;
103
- };
116
+ const isChannelRegistered = id => channelFunctions.find(x => x.id === id) !== undefined;
104
117
  const clearTimeoutHandler = id => {
105
118
  const existing = channelTimeoutHandlers.find(x => x.id === id);
106
119
  if (existing !== undefined) {
@@ -123,47 +136,99 @@ const notifySubscribing = channel => {
123
136
  x.handler(channel);
124
137
  });
125
138
  };
139
+
140
+ // ─── Worker initialisation ───────────────────────────────────────────────────
141
+
142
+ /**
143
+ * Open a MessagePort to the SharedWorker and wire up the inbound message
144
+ * handler. Safe to call multiple times — a module-level guard ensures the
145
+ * work only happens once per iframe.
146
+ *
147
+ * The SharedWorker URL is resolved by webpack 5 at build time via
148
+ * `new URL(relPath, import.meta.url)`, which produces a stable content-hashed
149
+ * URL. Because every iframe loads the same bundle, they all resolve to the
150
+ * identical URL → the browser routes them all to the same running worker.
151
+ *
152
+ * @param {string} host Hostname / IP (no protocol, no port).
153
+ * @param {boolean} secure true → wss, false → ws.
154
+ * @param {Function} log props.core.log bound to the core instance.
155
+ */
156
+ function initWorkerPort(host, secure, log) {
157
+ if (clientInitialized) return;
158
+
159
+ // `new URL('./versacall-channels.worker.js', import.meta.url)` requires
160
+ // webpack 5 (react-scripts ≥ 5) or Vite. If your build toolchain is older,
161
+ // replace this with a hardcoded absolute path, e.g.:
162
+ // new SharedWorker('/versacall-channels.worker.js', { name: 'versacall-channels' })
163
+ // and serve the worker file from the app's public folder.
164
+ const worker = new SharedWorker('/ChannelsWorker.js', {
165
+ name: 'versacall-channels'
166
+ });
167
+ workerPort = worker.port;
168
+
169
+ // Inbound messages from the worker
170
+ workerPort.onmessage = ev => {
171
+ const {
172
+ type
173
+ } = ev.data;
174
+ if (type === 'MESSAGE') {
175
+ // ev.data.channel is the raw MQTT topic "{channelKey}/{channelName}/"
176
+ // — identical to what emitter-io exposed as msg.channel.
177
+ processMessage(ev.data.channel, ev.data.data);
178
+ } else if (type === 'CONNECTION_CHANGED') {
179
+ log('system', namespace, 'workerPort.onmessage', ev.data.connected ? 'Emitter Connect' : 'Emitter Disconnect');
180
+ notifyConnectionChanged(ev.data.connected);
181
+ }
182
+ };
183
+ workerPort.onmessageerror = err => {
184
+ log('error', namespace, 'workerPort.onmessageerror', err);
185
+ };
186
+
187
+ // start() is required for MessagePort when the event listener is assigned
188
+ // via the property (not addEventListener).
189
+ workerPort.start();
190
+
191
+ // Tell the worker our connection parameters. If another iframe already
192
+ // sent INIT, the worker will skip reconnecting and reply with the current
193
+ // connection state instead.
194
+ workerPort.postMessage({
195
+ type: 'INIT',
196
+ host: host,
197
+ port: 9090,
198
+ secure: secure
199
+ });
200
+ clientInitialized = true;
201
+ log('system', namespace, 'initWorkerPort()', 'SharedWorker port opened (Singleton)');
202
+ }
203
+
204
+ // ─── Component ───────────────────────────────────────────────────────────────
205
+
126
206
  class ChannelsProvider extends _react.Component {
127
207
  constructor(props) {
128
208
  const methodName = 'constructor()';
129
209
  super(props);
130
-
131
- // ✅ FIXED: Check if props are ready before initializing
132
210
  if (!clientInitialized) {
133
211
  if (!props.core || !props.core.baseUrl) {
134
- // Props not ready yet, will retry on next instance
135
- console.log('ChannelsProvider: Core props not ready, deferring client initialization');
212
+ // Core context not ready yet another ChannelsProvider instance will
213
+ // complete the init once props are available.
214
+ console.log('ChannelsProvider: Core props not ready, deferring SharedWorker init');
136
215
  return;
137
216
  }
138
217
  const urlArray = props.core.baseUrl.split('://');
139
218
  const protocol = urlArray[0];
140
- const modifiedUrl = urlArray[urlArray.length - 1];
219
+ const host = urlArray[urlArray.length - 1];
141
220
  const isSecure = protocol === 'https';
142
- client = emitter.connect({
143
- host: modifiedUrl,
144
- port: 9090,
145
- secure: isSecure
146
- });
147
- client.on('disconnect', () => {
148
- props.core.log('system', namespace, methodName, 'Emitter Disconnect');
149
- notifyConnectionChanged(false);
150
- });
151
- client.on('offline', () => {
152
- props.core.log('system', namespace, methodName, 'Emitter Offline');
153
- notifyConnectionChanged(false);
154
- });
155
- client.on('message', msg => {
156
- processMessage(msg.channel, JSON.parse(msg.asString()));
157
- });
158
- client.on('error', error => {
159
- props.core.log('error', namespace, methodName, error);
160
- });
161
- clientInitialized = true;
162
- props.core.log('system', namespace, methodName, 'Emitter Connect (Singleton)');
163
- } else if (props.core && props.core.baseUrl) {
164
- props.core.log('system', namespace, methodName, 'Reusing existing Emitter connection');
221
+
222
+ // Bind log so it can be called without a `this` context inside the
223
+ // module-level initWorkerPort function.
224
+ initWorkerPort(host, isSecure, props.core.log.bind(props.core));
225
+ } else if (props.core && props.core.log) {
226
+ props.core.log('system', namespace, methodName, 'Reusing existing SharedWorker connection');
165
227
  }
166
228
  }
229
+
230
+ // ── Channel update / subscription lifecycle (unchanged) ──────────────────
231
+
167
232
  updateChannel(id, data) {
168
233
  const existing = channelTimeoutHandlers.find(x => x.id === id);
169
234
  if (existing !== undefined) {
@@ -211,25 +276,48 @@ class ChannelsProvider extends _react.Component {
211
276
  });
212
277
  }
213
278
  }
279
+
280
+ /**
281
+ * Tell the SharedWorker to subscribe to an emitter channel.
282
+ * The HTTP call that fetches {channelKey, channelName} still happens here in
283
+ * ChannelsProvider (via callChannelFunction / registerChannel) — only the
284
+ * actual WebSocket transport has moved to the worker.
285
+ */
214
286
  subscribe(channelInfo) {
215
287
  const methodName = 'subscribe()';
216
288
  this.props.core.log('system', namespace, methodName, "Subscribing to ".concat(channelInfo.channelName));
217
- client.subscribe({
218
- key: channelInfo.channelKey,
219
- channel: channelInfo.channelName
220
- });
289
+ if (workerPort) {
290
+ workerPort.postMessage({
291
+ type: 'SUBSCRIBE',
292
+ channelKey: channelInfo.channelKey,
293
+ channelName: channelInfo.channelName
294
+ });
295
+ }
221
296
  notifySubscribing(channelInfo.channelName);
222
297
  }
298
+
299
+ /** Tell the SharedWorker to unsubscribe from an emitter channel. */
223
300
  unsubscribe(channelInfo) {
224
301
  const methodName = 'unsubscribe()';
225
302
  this.props.core.log('system', namespace, methodName, "Unsubscribing from ".concat(channelInfo.channelName));
226
- client.unsubscribe({
227
- key: channelInfo.channelKey,
228
- channel: channelInfo.channelName
229
- });
303
+ if (workerPort) {
304
+ workerPort.postMessage({
305
+ type: 'UNSUBSCRIBE',
306
+ channelKey: channelInfo.channelKey,
307
+ channelName: channelInfo.channelName
308
+ });
309
+ }
230
310
  }
311
+
312
+ /**
313
+ * Re-issue subscribe calls for every channel currently tracked in
314
+ * channelInformation. The SharedWorker already replays all active
315
+ * subscriptions after a reconnect (fixing the latent bug in the original
316
+ * code), so this method is kept for API compatibility but is no longer
317
+ * needed for reconnect recovery.
318
+ */
231
319
  resubscribeToChannels() {
232
- channelInformation.forEach((item, i) => {
320
+ channelInformation.forEach(item => {
233
321
  this.subscribe(item.channelInfo);
234
322
  });
235
323
  }
@@ -0,0 +1,419 @@
1
+ /* eslint-disable */
2
+ 'use strict';
3
+
4
+ /**
5
+ * versacall-channels.worker.js
6
+ * SharedWorker — owns a single MQTT-over-WebSocket connection to the emitter
7
+ * server. All iframes in the same origin connect here via MessagePort and
8
+ * share one WebSocket instead of opening one each.
9
+ *
10
+ * Wire protocol (ChannelsProvider → Worker):
11
+ * { type: 'INIT', host, port, secure }
12
+ * { type: 'SUBSCRIBE', channelKey, channelName }
13
+ * { type: 'UNSUBSCRIBE', channelKey, channelName }
14
+ *
15
+ * Wire protocol (Worker → ChannelsProvider port):
16
+ * { type: 'MESSAGE', channel, data }
17
+ * { type: 'CONNECTION_CHANGED', connected }
18
+ *
19
+ * Topic format matches emitter-io: "{channelKey}/{channelName}/"
20
+ *
21
+ * No external dependencies — implements the minimal MQTT 3.1.1 subset needed:
22
+ * TX: CONNECT, SUBSCRIBE, UNSUBSCRIBE, PINGREQ
23
+ * RX: CONNACK, PUBLISH, SUBACK, UNSUBACK, PINGRESP
24
+ */
25
+
26
+ // ─── Global state ────────────────────────────────────────────────────────────
27
+
28
+ /** @type {Set<MessagePort>} */
29
+ const ports = new Set();
30
+ let ws = null; // current WebSocket
31
+ let wsConfig = null; // { host, port, secure } — set once on first INIT
32
+ let mqttConnected = false; // true after CONNACK(0) received
33
+ let pingIntervalId = null;
34
+ let reconnectTimerId = null;
35
+ let receiveBuffer = new Uint8Array(0);
36
+ let packetIdCounter = 0;
37
+
38
+ /**
39
+ * topic string → { channelKey, channelName }
40
+ * Maintained across reconnects so we can resubscribe automatically.
41
+ */
42
+ const activeSubscriptions = new Map();
43
+
44
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
45
+
46
+ const textEncoder = new TextEncoder();
47
+ const textDecoder = new TextDecoder();
48
+ function nextPacketId() {
49
+ if (++packetIdCounter > 65535) packetIdCounter = 1;
50
+ return packetIdCounter;
51
+ }
52
+
53
+ /** Concatenate two Uint8Arrays */
54
+ function concat(a, b) {
55
+ const out = new Uint8Array(a.length + b.length);
56
+ out.set(a, 0);
57
+ out.set(b, a.length);
58
+ return out;
59
+ }
60
+
61
+ // ─── MQTT encoding ───────────────────────────────────────────────────────────
62
+
63
+ /**
64
+ * Encode a UTF-8 string as MQTT length-prefixed bytes (2-byte big-endian len).
65
+ * @param {string} str
66
+ * @returns {Uint8Array}
67
+ */
68
+ function mqttStr(str) {
69
+ const bytes = textEncoder.encode(str);
70
+ const out = new Uint8Array(2 + bytes.length);
71
+ out[0] = bytes.length >> 8 & 0xff;
72
+ out[1] = bytes.length & 0xff;
73
+ out.set(bytes, 2);
74
+ return out;
75
+ }
76
+
77
+ /**
78
+ * Encode n as an MQTT variable-length integer (1–4 bytes).
79
+ * @param {number} n
80
+ * @returns {Uint8Array}
81
+ */
82
+ function mqttVarInt(n) {
83
+ const bytes = [];
84
+ do {
85
+ let b = n & 0x7f;
86
+ n >>>= 7;
87
+ if (n > 0) b |= 0x80;
88
+ bytes.push(b);
89
+ } while (n > 0);
90
+ return new Uint8Array(bytes);
91
+ }
92
+
93
+ /**
94
+ * Build a complete MQTT packet.
95
+ * @param {number} firstByte Fixed-header first byte (type + flags).
96
+ * @param {...Uint8Array} parts Variable-header + payload parts in order.
97
+ * @returns {Uint8Array}
98
+ */
99
+ function mqttPacket(firstByte) {
100
+ let remaining = 0;
101
+ for (var _len = arguments.length, parts = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
102
+ parts[_key - 1] = arguments[_key];
103
+ }
104
+ for (const p of parts) remaining += p.length;
105
+ const varLen = mqttVarInt(remaining);
106
+ const out = new Uint8Array(1 + varLen.length + remaining);
107
+ let off = 0;
108
+ out[off++] = firstByte;
109
+ out.set(varLen, off);
110
+ off += varLen.length;
111
+ for (const p of parts) {
112
+ out.set(p, off);
113
+ off += p.length;
114
+ }
115
+ return out;
116
+ }
117
+
118
+ /** MQTT CONNECT packet (clean session, no credentials). */
119
+ function buildConnect(clientId) {
120
+ // Protocol name "MQTT" as length-prefixed bytes
121
+ const protoName = new Uint8Array([0x00, 0x04, 0x4d, 0x51, 0x54, 0x54]);
122
+ const protoLevel = new Uint8Array([0x04]); // 3.1.1
123
+ const connFlags = new Uint8Array([0x02]); // clean session
124
+ const keepAlive = new Uint8Array([0x00, 0x1e]); // 30 seconds
125
+ return mqttPacket(0x10, protoName, protoLevel, connFlags, keepAlive, mqttStr(clientId));
126
+ }
127
+
128
+ /** MQTT SUBSCRIBE packet, QoS 0. */
129
+ function buildSubscribe(packetId, topic) {
130
+ const id = new Uint8Array([packetId >> 8 & 0xff, packetId & 0xff]);
131
+ return mqttPacket(0x82, id, mqttStr(topic), new Uint8Array([0x00]));
132
+ }
133
+
134
+ /** MQTT UNSUBSCRIBE packet. */
135
+ function buildUnsubscribe(packetId, topic) {
136
+ const id = new Uint8Array([packetId >> 8 & 0xff, packetId & 0xff]);
137
+ return mqttPacket(0xa2, id, mqttStr(topic));
138
+ }
139
+ const PINGREQ = new Uint8Array([0xc0, 0x00]);
140
+
141
+ // ─── MQTT decoding ───────────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Decode an MQTT variable-length integer starting at buffer[offset+1]
145
+ * (i.e. the byte after the fixed-header first byte).
146
+ * @returns {{ value: number, bytes: number } | null} null if incomplete data
147
+ */
148
+ function decodeVarInt(buf, offset) {
149
+ let value = 0,
150
+ mult = 1;
151
+ for (let i = 0; i < 4; i++) {
152
+ if (offset + i >= buf.length) return null; // need more data
153
+ const b = buf[offset + i];
154
+ value += (b & 0x7f) * mult;
155
+ mult *= 128;
156
+ if ((b & 0x80) === 0) return {
157
+ value,
158
+ bytes: i + 1
159
+ };
160
+ }
161
+ return null; // malformed
162
+ }
163
+
164
+ /**
165
+ * Drain receiveBuffer, dispatch any complete MQTT packets found.
166
+ * Leaves any trailing incomplete bytes in receiveBuffer.
167
+ */
168
+ function drainBuffer() {
169
+ let offset = 0;
170
+ while (offset < receiveBuffer.length) {
171
+ const firstByte = receiveBuffer[offset];
172
+ const varInt = decodeVarInt(receiveBuffer, offset + 1);
173
+ if (!varInt) break; // need more data
174
+
175
+ const totalLen = 1 + varInt.bytes + varInt.value;
176
+ if (offset + totalLen > receiveBuffer.length) break; // packet incomplete
177
+
178
+ const packetType = firstByte >> 4 & 0x0f;
179
+ const headerFlags = firstByte & 0x0f;
180
+ const body = receiveBuffer.slice(offset + 1 + varInt.bytes, offset + totalLen);
181
+ dispatchMqttPacket(packetType, headerFlags, body);
182
+ offset += totalLen;
183
+ }
184
+ receiveBuffer = receiveBuffer.slice(offset);
185
+ }
186
+
187
+ /** Route a decoded MQTT packet to the appropriate handler. */
188
+ function dispatchMqttPacket(type, flags, body) {
189
+ switch (type) {
190
+ case 2:
191
+ return onConnack(body);
192
+ // CONNACK
193
+ case 3:
194
+ return onPublish(flags, body);
195
+ // PUBLISH
196
+ // SUBACK(9), UNSUBACK(11), PINGRESP(13) — acknowledged, nothing to do
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Handle CONNACK. On success, mark connected and replay all active
202
+ * subscriptions — this is the fix for the latent resubscription-after-
203
+ * reconnect bug that existed in the original ChannelsProvider.
204
+ */
205
+ function onConnack(body) {
206
+ // body[0] = session-present flag, body[1] = return code
207
+ if (body.length < 2 || body[1] !== 0) {
208
+ console.warn('[vc-worker] CONNACK error code:', body[1]);
209
+ return;
210
+ }
211
+ mqttConnected = true;
212
+ broadcast({
213
+ type: 'CONNECTION_CHANGED',
214
+ connected: true
215
+ });
216
+
217
+ // Resubscribe every channel the app has asked for.
218
+ // This handles both initial subscription after connect AND
219
+ // re-subscription after an unintended disconnect / reconnect.
220
+ activeSubscriptions.forEach((_, topic) => {
221
+ wsSend(buildSubscribe(nextPacketId(), topic));
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Handle an incoming PUBLISH packet.
227
+ * The MQTT topic for emitter-io is: "{channelKey}/{channelName}/"
228
+ * That same string is what emitter-io exposes as msg.channel, so we
229
+ * forward it unchanged — ChannelsConsumer handlers will match as before.
230
+ */
231
+ function onPublish(flags, body) {
232
+ if (body.length < 2) return;
233
+ const topicLen = body[0] << 8 | body[1];
234
+ if (2 + topicLen > body.length) return;
235
+ const topic = textDecoder.decode(body.slice(2, 2 + topicLen));
236
+
237
+ // QoS > 0 means a 2-byte packet-id follows the topic
238
+ const qos = flags >> 1 & 0x03;
239
+ const payloadStart = 2 + topicLen + (qos > 0 ? 2 : 0);
240
+ const payloadStr = textDecoder.decode(body.slice(payloadStart));
241
+ let data;
242
+ try {
243
+ data = JSON.parse(payloadStr);
244
+ } catch (_) {
245
+ data = payloadStr;
246
+ }
247
+ broadcast({
248
+ type: 'MESSAGE',
249
+ channel: topic,
250
+ data
251
+ });
252
+ }
253
+
254
+ // ─── WebSocket management ────────────────────────────────────────────────────
255
+
256
+ /** Send a Uint8Array over the WebSocket if it's open. */
257
+ function wsSend(packet) {
258
+ if (ws && ws.readyState === WebSocket.OPEN) {
259
+ ws.send(packet);
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Open (or reopen) the WebSocket and perform an MQTT CONNECT.
265
+ * Any existing socket is closed first.
266
+ */
267
+ function openWebSocket(config) {
268
+ // Tear down old socket cleanly
269
+ if (ws) {
270
+ ws.onopen = ws.onclose = ws.onerror = ws.onmessage = null;
271
+ try {
272
+ ws.close();
273
+ } catch (_) {/* ignore */}
274
+ ws = null;
275
+ }
276
+ clearInterval(pingIntervalId);
277
+ clearTimeout(reconnectTimerId);
278
+ const proto = config.secure ? 'wss' : 'ws';
279
+ const url = "".concat(proto, "://").concat(config.host, ":").concat(config.port);
280
+ ws = new WebSocket(url, 'mqtt');
281
+ ws.binaryType = 'arraybuffer';
282
+
283
+ // Each connection gets a fresh random client ID (emitter doesn't care about
284
+ // persistence between reconnects).
285
+ const clientId = 'vc_' + Math.random().toString(36).slice(2, 10);
286
+ ws.onopen = () => {
287
+ receiveBuffer = new Uint8Array(0); // reset frame reassembly buffer
288
+ wsSend(buildConnect(clientId));
289
+
290
+ // Keepalive: PINGREQ every 25 s (server keepalive is set to 30 s)
291
+ clearInterval(pingIntervalId);
292
+ pingIntervalId = setInterval(() => wsSend(PINGREQ), 25000);
293
+ };
294
+ ws.onmessage = ev => {
295
+ receiveBuffer = concat(receiveBuffer, new Uint8Array(ev.data));
296
+ drainBuffer();
297
+ };
298
+ ws.onclose = () => {
299
+ clearInterval(pingIntervalId);
300
+ if (mqttConnected) {
301
+ mqttConnected = false;
302
+ broadcast({
303
+ type: 'CONNECTION_CHANGED',
304
+ connected: false
305
+ });
306
+ }
307
+ // Reconnect after 3 s — forever, since the app may come back at any time.
308
+ reconnectTimerId = setTimeout(() => {
309
+ if (wsConfig) openWebSocket(wsConfig);
310
+ }, 3000);
311
+ };
312
+
313
+ // onerror always precedes onclose; logging only.
314
+ ws.onerror = err => {
315
+ console.warn('[vc-worker] WebSocket error:', err.message || err);
316
+ };
317
+ }
318
+
319
+ // ─── Port broadcast ──────────────────────────────────────────────────────────
320
+
321
+ /**
322
+ * Send a message to every connected port.
323
+ * Dead ports (closed iframes) are removed on first failure.
324
+ */
325
+ function broadcast(msg) {
326
+ for (const port of ports) {
327
+ try {
328
+ port.postMessage(msg);
329
+ } catch (_) {
330
+ ports.delete(port);
331
+ }
332
+ }
333
+ }
334
+
335
+ // ─── Incoming message handler ────────────────────────────────────────────────
336
+
337
+ /**
338
+ * Handle a message arriving from one ChannelsProvider port.
339
+ * @param {MessagePort} port The originating port (used for INIT replies).
340
+ * @param {{ type: string }} msg
341
+ */
342
+ function handlePortMessage(port, msg) {
343
+ switch (msg.type) {
344
+ case 'INIT':
345
+ {
346
+ const config = {
347
+ host: msg.host,
348
+ port: msg.port,
349
+ secure: msg.secure
350
+ };
351
+ if (!wsConfig) {
352
+ // First INIT — record config and open the connection.
353
+ wsConfig = config;
354
+ openWebSocket(config);
355
+ port.postMessage({
356
+ type: 'LOG',
357
+ message: 'Worker: first INIT, opening WebSocket'
358
+ });
359
+ } else if (mqttConnected) {
360
+ // Subsequent INIT from a later iframe — we're already live.
361
+ // Immediately tell the new port so it can update connection-state UI.
362
+ port.postMessage({
363
+ type: 'CONNECTION_CHANGED',
364
+ connected: true
365
+ });
366
+ port.postMessage({
367
+ type: 'LOG',
368
+ message: 'Worker: subsequent INIT, reusing existing connection. Total ports: ' + ports.size
369
+ });
370
+ }
371
+ // If !mqttConnected and wsConfig is already set we're in the middle of
372
+ // connecting; the CONNACK broadcast will reach the new port once ready.
373
+ break;
374
+ }
375
+ case 'SUBSCRIBE':
376
+ {
377
+ // topic format matches emitter-io: "{channelKey}/{channelName}/"
378
+ const topic = "".concat(msg.channelKey, "/").concat(msg.channelName, "/");
379
+ activeSubscriptions.set(topic, {
380
+ channelKey: msg.channelKey,
381
+ channelName: msg.channelName
382
+ });
383
+ // Send SUBSCRIBE immediately if already connected; otherwise the topic
384
+ // will be replayed in onConnack() when the connection comes up.
385
+ if (mqttConnected) {
386
+ wsSend(buildSubscribe(nextPacketId(), topic));
387
+ }
388
+ break;
389
+ }
390
+ case 'UNSUBSCRIBE':
391
+ {
392
+ const topic = "".concat(msg.channelKey, "/").concat(msg.channelName, "/");
393
+ activeSubscriptions.delete(topic);
394
+ if (mqttConnected) {
395
+ wsSend(buildUnsubscribe(nextPacketId(), topic));
396
+ }
397
+ break;
398
+ }
399
+ default:
400
+ console.warn('[vc-worker] Unknown message type:', msg.type);
401
+ }
402
+ }
403
+
404
+ // ─── SharedWorker entry point ────────────────────────────────────────────────
405
+
406
+ /**
407
+ * Called once for every new client context (iframe) that does
408
+ * `new SharedWorker(url)`. Each gets its own MessagePort.
409
+ */
410
+ self.onconnect = event => {
411
+ const port = event.ports[0];
412
+ ports.add(port);
413
+ port.onmessage = ev => handlePortMessage(port, ev.data);
414
+ port.onmessageerror = () => ports.delete(port);
415
+
416
+ // start() is required when the port was not started implicitly via
417
+ // addEventListener — calling it on an already-started port is harmless.
418
+ port.start();
419
+ };
package/package.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "versacall": {
3
3
  "title": "Versacall Core Library React",
4
4
  "applicationType": "react-library",
5
- "build": 75
5
+ "build": 76
6
6
  },
7
7
  "name": "versacall-core-library-react",
8
- "version": "2.0.75",
8
+ "version": "2.0.76-dev",
9
9
  "description": "Versacall Core Library",
10
10
  "main": "dist/index.js",
11
11
  "module": "dist/index.js",