telemersive-bus 0.6.16 → 0.6.18
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/lib/BusManager.js +113 -79
- package/package.json +1 -1
package/lib/BusManager.js
CHANGED
|
@@ -16,6 +16,11 @@ const houseKeepingTimeOut = 1500;
|
|
|
16
16
|
const houseKeepingTimeOut_onRepeat = 1500;
|
|
17
17
|
const houseKeepingTimeOut_onPeerLeft= 1000;
|
|
18
18
|
const houseKeepingTimeOut_onPeerJoined = 5000;
|
|
19
|
+
// time to wait after sending DELETE requests to the switchboard before the room entry is
|
|
20
|
+
// removed from this.rooms. proxy processes need a moment to fully exit and release their
|
|
21
|
+
// OS ports — without this delay a room restart races against a still-dying proxy and fails
|
|
22
|
+
// to bind the same port.
|
|
23
|
+
const proxyShutdownSettleTime = 3000;
|
|
19
24
|
const ROOM_ID_MIN = 11;
|
|
20
25
|
const ROOM_ID_MAX = 50;
|
|
21
26
|
|
|
@@ -32,10 +37,19 @@ class BusManager {
|
|
|
32
37
|
this.roomHousekeep = {};
|
|
33
38
|
this.houseKeepingInProgress = false;
|
|
34
39
|
this.housekeepingId = 0;
|
|
35
|
-
// promise set while
|
|
36
|
-
//
|
|
37
|
-
// cannot start
|
|
38
|
-
|
|
40
|
+
// promise set while a housekeeping cycle is actively running (from subscribe
|
|
41
|
+
// through cleanOut's unsubscribe), null otherwise. startHousekeeping awaits it
|
|
42
|
+
// so a new cycle cannot start its own subscribe/ping sequence while a prior
|
|
43
|
+
// cycle — even an interrupted one — is still unwinding. without this, an
|
|
44
|
+
// interrupted cycle A and a new cycle B overlap: B resets peerHousekeep and
|
|
45
|
+
// re-subscribes, but retained peer-join messages arrive after B's ping timer,
|
|
46
|
+
// causing every alive peer to be rejected as "not joined anymore".
|
|
47
|
+
this.housekeepingCycleInProgress = null;
|
|
48
|
+
// resolver for the current cycle's promise. pingAllPeersAlive schedules
|
|
49
|
+
// cleanOut which calls this when the cycle is fully done (subscriptions
|
|
50
|
+
// cleaned up). if a cycle is interrupted, cleanOut still runs its unsubscribe
|
|
51
|
+
// path and resolves the promise.
|
|
52
|
+
this.resolveHousekeepingCycle = null;
|
|
39
53
|
this.chatManagers = {};
|
|
40
54
|
this.switchBoardURI = null;
|
|
41
55
|
}
|
|
@@ -124,11 +138,15 @@ class BusManager {
|
|
|
124
138
|
* when peer leaves a room
|
|
125
139
|
*/
|
|
126
140
|
startHousekeeping = async () => {
|
|
127
|
-
// wait for any in-flight
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
// wait for any in-flight cycle to fully unwind (subscriptions cleaned up and
|
|
142
|
+
// rooms mutations settled) before starting a new one. this guards two races:
|
|
143
|
+
// 1. a still-running cleanOut's slow stopServerSidePortScripts finishes (and
|
|
144
|
+
// deletes from this.rooms) after a new cycle already snapshotted them.
|
|
145
|
+
// 2. an interrupted cycle's pingAllPeersAlive/cleanOut is still pending while
|
|
146
|
+
// a new cycle resets peerHousekeep and pings — every response then gets
|
|
147
|
+
// rejected as "not joined anymore" because peerHousekeep was wiped.
|
|
148
|
+
if (this.housekeepingCycleInProgress) {
|
|
149
|
+
await this.housekeepingCycleInProgress;
|
|
132
150
|
}
|
|
133
151
|
// we dont want to start another housekeeping process until the previous one has finished
|
|
134
152
|
if(!this.houseKeepingInProgress){
|
|
@@ -137,6 +155,11 @@ class BusManager {
|
|
|
137
155
|
// they no longer own the housekeeping state and bail out.
|
|
138
156
|
this.housekeepingId += 1;
|
|
139
157
|
const myId = this.housekeepingId;
|
|
158
|
+
// claim the cycle promise up front so any overlapping startHousekeeping call
|
|
159
|
+
// will await this cycle's completion rather than racing it.
|
|
160
|
+
this.housekeepingCycleInProgress = new Promise((resolve) => {
|
|
161
|
+
this.resolveHousekeepingCycle = resolve;
|
|
162
|
+
});
|
|
140
163
|
console.log(` -> starting housekeeping -> gather all peers ...`);
|
|
141
164
|
this.peerHousekeep = {};
|
|
142
165
|
this.roomHousekeep = Object.assign({}, this.rooms); // clone the current room list
|
|
@@ -275,79 +298,82 @@ class BusManager {
|
|
|
275
298
|
*/
|
|
276
299
|
cleanOut = async (myId) => {
|
|
277
300
|
// if a newer cycle has started, this callback belongs to an interrupted cycle — drop it.
|
|
301
|
+
// note: this can only happen if housekeepingId was bumped without awaiting the prior
|
|
302
|
+
// cycle's promise (shouldn't happen with the await in startHousekeeping), but we keep
|
|
303
|
+
// the guard as defense in depth.
|
|
278
304
|
if (myId !== this.housekeepingId) {
|
|
279
305
|
return;
|
|
280
306
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
peerId: currentPeer.peerId,
|
|
306
|
-
peerName: currentPeer.peerName,
|
|
307
|
-
roomName: currentPeer.roomName
|
|
308
|
-
};
|
|
309
|
-
|
|
310
|
-
// sends the left message to all peers joined in the same room
|
|
311
|
-
await this.communicator.publish(
|
|
312
|
-
new BusMsgPub(
|
|
313
|
-
this.busTopics.roomPeerLeft(currentPeer.roomName, currentPeer.peerId).build(),
|
|
314
|
-
PeerInfo, 2, true).encode(infoPayload));
|
|
315
|
-
}
|
|
316
|
-
const peerCleanup = async () => {
|
|
317
|
-
for (const peer of Object.values(this.peerHousekeep)) {
|
|
318
|
-
await kickOutPeer(peer);
|
|
319
|
-
}
|
|
320
|
-
};
|
|
321
|
-
await peerCleanup();
|
|
322
|
-
|
|
323
|
-
// And now remove the rooms
|
|
324
|
-
const cleanupRooms = async (currentRoom) => await this.deleteRoom(currentRoom.roomName);
|
|
325
|
-
const housekeeping = async () => {
|
|
326
|
-
for (const room of Object.values(this.roomHousekeep)) {
|
|
327
|
-
await cleanupRooms(room);
|
|
328
|
-
}
|
|
307
|
+
try {
|
|
308
|
+
if (this.houseKeepingInProgress) {
|
|
309
|
+
console.log(` -> housekeeping -> cleaning out ...`);
|
|
310
|
+
// flag the room as being in the process to be removed. the entry may already have
|
|
311
|
+
// been deleted from this.rooms by an earlier overlapping cleanOut whose slow
|
|
312
|
+
// stopServerSidePortScripts only just finished — in that case, nothing to flag.
|
|
313
|
+
const flag4cleanupRooms = (currentRoom) => {
|
|
314
|
+
const room = this.rooms[currentRoom.roomName];
|
|
315
|
+
if (room) room.flagRoom4cleanup = true;
|
|
316
|
+
};
|
|
317
|
+
Object.values(this.roomHousekeep).forEach(flag4cleanupRooms);
|
|
318
|
+
|
|
319
|
+
// kick out the non-responding peers
|
|
320
|
+
const kickOutPeer = async (currentPeer) => {
|
|
321
|
+
console.log(` <- remove peer '${currentPeer.peerName}' from room '${currentPeer.roomName}' `);
|
|
322
|
+
// first we send clear retain message to peer joined addresses that have not replied:
|
|
323
|
+
await this.communicator.clearRetain(this.busTopics.roomPeerJoin(currentPeer.roomName, currentPeer.peerId).build());
|
|
324
|
+
|
|
325
|
+
// create the payload for the leave message
|
|
326
|
+
const infoPayload = {
|
|
327
|
+
timestamp: Math.round(new Date().getTime() / 1000),
|
|
328
|
+
peerId: currentPeer.peerId,
|
|
329
|
+
peerName: currentPeer.peerName,
|
|
330
|
+
roomName: currentPeer.roomName
|
|
329
331
|
};
|
|
330
|
-
await housekeeping();
|
|
331
332
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
// sends the left message to all peers joined in the same room
|
|
334
|
+
await this.communicator.publish(
|
|
335
|
+
new BusMsgPub(
|
|
336
|
+
this.busTopics.roomPeerLeft(currentPeer.roomName, currentPeer.peerId).build(),
|
|
337
|
+
PeerInfo, 2, true).encode(infoPayload));
|
|
335
338
|
}
|
|
339
|
+
const peerCleanup = async () => {
|
|
340
|
+
for (const peer of Object.values(this.peerHousekeep)) {
|
|
341
|
+
await kickOutPeer(peer);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
await peerCleanup();
|
|
345
|
+
|
|
346
|
+
// And now remove the rooms
|
|
347
|
+
const cleanupRooms = async (currentRoom) => await this.deleteRoom(currentRoom.roomName);
|
|
348
|
+
const housekeeping = async () => {
|
|
349
|
+
for (const room of Object.values(this.roomHousekeep)) {
|
|
350
|
+
await cleanupRooms(room);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
await housekeeping();
|
|
336
354
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
await this.communicator.unsubscribe(this.busTopics.baseRoomAlivePing("+").build());
|
|
341
|
-
// cleanup the communicator
|
|
342
|
-
this.communicator.clearAutoGeneratedSubscriptions();
|
|
343
|
-
// set this flag to false at the end to make sure a new houskeeping cycle can only be started once
|
|
344
|
-
// all topics are unsubscribed - otherwise it won't gather the joined peers inside the room.
|
|
345
|
-
this.houseKeepingInProgress = false;
|
|
346
|
-
} finally {
|
|
347
|
-
this.cleanOutInProgress = null;
|
|
355
|
+
console.log(` -> housekeeping DONE`);
|
|
356
|
+
} else {
|
|
357
|
+
console.log(` -> housekeeping was interrupted`);
|
|
348
358
|
}
|
|
349
|
-
|
|
350
|
-
|
|
359
|
+
|
|
360
|
+
// and we stop by unsubscribing to all peer info messages inside all rooms
|
|
361
|
+
await this.communicator.unsubscribe(this.busTopics.roomPeerJoin('+', '+').build());
|
|
362
|
+
// unsubscribing to all room ping alive topic
|
|
363
|
+
await this.communicator.unsubscribe(this.busTopics.baseRoomAlivePing("+").build());
|
|
364
|
+
// cleanup the communicator
|
|
365
|
+
this.communicator.clearAutoGeneratedSubscriptions();
|
|
366
|
+
// set this flag to false at the end to make sure a new houskeeping cycle can only be started once
|
|
367
|
+
// all topics are unsubscribed - otherwise it won't gather the joined peers inside the room.
|
|
368
|
+
this.houseKeepingInProgress = false;
|
|
369
|
+
} finally {
|
|
370
|
+
// resolve the cycle promise so any awaiting startHousekeeping can proceed.
|
|
371
|
+
// done in finally so an exception inside the try block cannot deadlock the next cycle.
|
|
372
|
+
const resolve = this.resolveHousekeepingCycle;
|
|
373
|
+
this.housekeepingCycleInProgress = null;
|
|
374
|
+
this.resolveHousekeepingCycle = null;
|
|
375
|
+
if (resolve) resolve();
|
|
376
|
+
}
|
|
351
377
|
}
|
|
352
378
|
|
|
353
379
|
/**
|
|
@@ -732,6 +758,9 @@ class BusManager {
|
|
|
732
758
|
console.error(` ...: ${err}. Interrupting process of stopping open stage control instance on port ${_roomId * 1000 + 900}.`);
|
|
733
759
|
}
|
|
734
760
|
console.log(` <- stopped all ports for room ${_roomName}`);
|
|
761
|
+
// wait for proxy processes to fully exit and release OS ports before allowing
|
|
762
|
+
// the room to be recreated. without this, a restart races against dying proxies.
|
|
763
|
+
await new Promise(resolve => setTimeout(resolve, proxyShutdownSettleTime));
|
|
735
764
|
delete this.rooms[_roomName];
|
|
736
765
|
console.log(` <- removed room ${_roomName}`);
|
|
737
766
|
}else {
|
|
@@ -750,9 +779,11 @@ class BusManager {
|
|
|
750
779
|
});
|
|
751
780
|
//let reply = JSON.parse(res["text"]);
|
|
752
781
|
} catch (err) {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
782
|
+
if (err.response?.text) {
|
|
783
|
+
let reply = JSON.parse(err.response["text"]);
|
|
784
|
+
throw new Error(reply["msg"]);
|
|
785
|
+
}
|
|
786
|
+
throw err;
|
|
756
787
|
}
|
|
757
788
|
}
|
|
758
789
|
|
|
@@ -765,8 +796,11 @@ class BusManager {
|
|
|
765
796
|
});
|
|
766
797
|
//let reply = JSON.parse(res["text"]);
|
|
767
798
|
} catch (err) {
|
|
768
|
-
|
|
769
|
-
|
|
799
|
+
if (err.response?.text) {
|
|
800
|
+
let reply = JSON.parse(err.response["text"]);
|
|
801
|
+
throw new Error(reply["msg"]);
|
|
802
|
+
}
|
|
803
|
+
throw err;
|
|
770
804
|
}
|
|
771
805
|
}
|
|
772
806
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telemersive-bus",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.18",
|
|
4
4
|
"description": "MQTT based data protocol to manage unlimited peers, connected by rooms, where all peers joined to a room can exchange private data. It provides a simple chat mechanism, latency pinging, publish-subscribe mechanism (based on mqtt) and a OSC-like data stream. ",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|