versacall-core-library-react 2.0.75 → 2.0.77-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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
54
|
-
if (
|
|
55
|
-
|
|
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
|
|
65
|
-
if (
|
|
66
|
-
|
|
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
|
|
81
|
-
if (
|
|
82
|
-
connectionChangedHandlers.splice(connectionChangedHandlers.indexOf(
|
|
96
|
+
const existingCC = connectionChangedHandlers.find(x => x.id === id);
|
|
97
|
+
if (existingCC !== undefined) {
|
|
98
|
+
connectionChangedHandlers.splice(connectionChangedHandlers.indexOf(existingCC), 1);
|
|
83
99
|
}
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
86
|
-
subscribingHandlers.splice(subscribingHandlers.indexOf(
|
|
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('/plugins/dashboards/viewer/freehand/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
|
-
//
|
|
135
|
-
|
|
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
|
|
219
|
+
const host = urlArray[urlArray.length - 1];
|
|
141
220
|
const isSecure = protocol === 'https';
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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(
|
|
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":
|
|
5
|
+
"build": 77
|
|
6
6
|
},
|
|
7
7
|
"name": "versacall-core-library-react",
|
|
8
|
-
"version": "2.0.
|
|
8
|
+
"version": "2.0.77-dev",
|
|
9
9
|
"description": "Versacall Core Library",
|
|
10
10
|
"main": "dist/index.js",
|
|
11
11
|
"module": "dist/index.js",
|