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.
Files changed (2) hide show
  1. package/lib/BusManager.js +113 -79
  2. 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 cleanOut is actively mutating this.rooms, null otherwise.
36
- // startHousekeeping awaits it before snapshotting this.rooms, so a new cycle
37
- // cannot start before the previous one's room deletions have fully settled.
38
- this.cleanOutInProgress = null;
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 cleanOut to fully settle before snapshotting this.rooms.
128
- // this prevents overlap where a still-running cleanOut's slow stopServerSidePortScripts
129
- // finishes (and deletes from this.rooms) after a new cycle already snapshotted them.
130
- if (this.cleanOutInProgress) {
131
- await this.cleanOutInProgress;
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
- // expose the in-flight cleanOut as a promise so the next startHousekeeping can await it
282
- // and avoid snapshotting this.rooms while deleteRoom is still mutating it.
283
- this.cleanOutInProgress = (async () => {
284
- try {
285
- if(this.houseKeepingInProgress) {
286
- console.log(` -> housekeeping -> cleaning out ...`);
287
- // flag the room as being in the process to be removed. the entry may already have
288
- // been deleted from this.rooms by an earlier overlapping cleanOut whose slow
289
- // stopServerSidePortScripts only just finished — in that case, nothing to flag.
290
- const flag4cleanupRooms = (currentRoom) => {
291
- const room = this.rooms[currentRoom.roomName];
292
- if (room) room.flagRoom4cleanup = true;
293
- };
294
- Object.values(this.roomHousekeep).forEach(flag4cleanupRooms);
295
-
296
- // kick out the non-responding peers
297
- const kickOutPeer = async (currentPeer) => {
298
- console.log(` <- remove peer '${currentPeer.peerName}' from room '${currentPeer.roomName}' `);
299
- // first we send clear retain message to peer joined addresses that have not replied:
300
- await this.communicator.clearRetain(this.busTopics.roomPeerJoin(currentPeer.roomName, currentPeer.peerId).build());
301
-
302
- // create the payload for the leave message
303
- const infoPayload = {
304
- timestamp: Math.round(new Date().getTime() / 1000),
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
- console.log(` -> housekeeping DONE`);
333
- } else {
334
- console.log(` -> housekeeping was interrupted`);
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
- // and we stop by unsubscribing to all peer info messages inside all rooms
338
- await this.communicator.unsubscribe(this.busTopics.roomPeerJoin('+', '+').build());
339
- // unsubscribing to all room ping alive topic
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
- await this.cleanOutInProgress;
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
- //console.error(err);
754
- let reply = JSON.parse(err.response["text"]);
755
- throw new Error(reply["msg"]);
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
- let reply = JSON.parse(err.response["text"]);
769
- throw new Error(reply["msg"]);
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.16",
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": {