telemersive-bus 0.6.16 → 0.6.17

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 +95 -74
  2. package/package.json +1 -1
package/lib/BusManager.js CHANGED
@@ -32,10 +32,19 @@ class BusManager {
32
32
  this.roomHousekeep = {};
33
33
  this.houseKeepingInProgress = false;
34
34
  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;
35
+ // promise set while a housekeeping cycle is actively running (from subscribe
36
+ // through cleanOut's unsubscribe), null otherwise. startHousekeeping awaits it
37
+ // so a new cycle cannot start its own subscribe/ping sequence while a prior
38
+ // cycle — even an interrupted one — is still unwinding. without this, an
39
+ // interrupted cycle A and a new cycle B overlap: B resets peerHousekeep and
40
+ // re-subscribes, but retained peer-join messages arrive after B's ping timer,
41
+ // causing every alive peer to be rejected as "not joined anymore".
42
+ this.housekeepingCycleInProgress = null;
43
+ // resolver for the current cycle's promise. pingAllPeersAlive schedules
44
+ // cleanOut which calls this when the cycle is fully done (subscriptions
45
+ // cleaned up). if a cycle is interrupted, cleanOut still runs its unsubscribe
46
+ // path and resolves the promise.
47
+ this.resolveHousekeepingCycle = null;
39
48
  this.chatManagers = {};
40
49
  this.switchBoardURI = null;
41
50
  }
@@ -124,11 +133,15 @@ class BusManager {
124
133
  * when peer leaves a room
125
134
  */
126
135
  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;
136
+ // wait for any in-flight cycle to fully unwind (subscriptions cleaned up and
137
+ // rooms mutations settled) before starting a new one. this guards two races:
138
+ // 1. a still-running cleanOut's slow stopServerSidePortScripts finishes (and
139
+ // deletes from this.rooms) after a new cycle already snapshotted them.
140
+ // 2. an interrupted cycle's pingAllPeersAlive/cleanOut is still pending while
141
+ // a new cycle resets peerHousekeep and pings — every response then gets
142
+ // rejected as "not joined anymore" because peerHousekeep was wiped.
143
+ if (this.housekeepingCycleInProgress) {
144
+ await this.housekeepingCycleInProgress;
132
145
  }
133
146
  // we dont want to start another housekeeping process until the previous one has finished
134
147
  if(!this.houseKeepingInProgress){
@@ -137,6 +150,11 @@ class BusManager {
137
150
  // they no longer own the housekeeping state and bail out.
138
151
  this.housekeepingId += 1;
139
152
  const myId = this.housekeepingId;
153
+ // claim the cycle promise up front so any overlapping startHousekeeping call
154
+ // will await this cycle's completion rather than racing it.
155
+ this.housekeepingCycleInProgress = new Promise((resolve) => {
156
+ this.resolveHousekeepingCycle = resolve;
157
+ });
140
158
  console.log(` -> starting housekeeping -> gather all peers ...`);
141
159
  this.peerHousekeep = {};
142
160
  this.roomHousekeep = Object.assign({}, this.rooms); // clone the current room list
@@ -275,79 +293,82 @@ class BusManager {
275
293
  */
276
294
  cleanOut = async (myId) => {
277
295
  // if a newer cycle has started, this callback belongs to an interrupted cycle — drop it.
296
+ // note: this can only happen if housekeepingId was bumped without awaiting the prior
297
+ // cycle's promise (shouldn't happen with the await in startHousekeeping), but we keep
298
+ // the guard as defense in depth.
278
299
  if (myId !== this.housekeepingId) {
279
300
  return;
280
301
  }
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
- }
302
+ try {
303
+ if (this.houseKeepingInProgress) {
304
+ console.log(` -> housekeeping -> cleaning out ...`);
305
+ // flag the room as being in the process to be removed. the entry may already have
306
+ // been deleted from this.rooms by an earlier overlapping cleanOut whose slow
307
+ // stopServerSidePortScripts only just finished — in that case, nothing to flag.
308
+ const flag4cleanupRooms = (currentRoom) => {
309
+ const room = this.rooms[currentRoom.roomName];
310
+ if (room) room.flagRoom4cleanup = true;
311
+ };
312
+ Object.values(this.roomHousekeep).forEach(flag4cleanupRooms);
313
+
314
+ // kick out the non-responding peers
315
+ const kickOutPeer = async (currentPeer) => {
316
+ console.log(` <- remove peer '${currentPeer.peerName}' from room '${currentPeer.roomName}' `);
317
+ // first we send clear retain message to peer joined addresses that have not replied:
318
+ await this.communicator.clearRetain(this.busTopics.roomPeerJoin(currentPeer.roomName, currentPeer.peerId).build());
319
+
320
+ // create the payload for the leave message
321
+ const infoPayload = {
322
+ timestamp: Math.round(new Date().getTime() / 1000),
323
+ peerId: currentPeer.peerId,
324
+ peerName: currentPeer.peerName,
325
+ roomName: currentPeer.roomName
329
326
  };
330
- await housekeeping();
331
327
 
332
- console.log(` -> housekeeping DONE`);
333
- } else {
334
- console.log(` -> housekeeping was interrupted`);
328
+ // sends the left message to all peers joined in the same room
329
+ await this.communicator.publish(
330
+ new BusMsgPub(
331
+ this.busTopics.roomPeerLeft(currentPeer.roomName, currentPeer.peerId).build(),
332
+ PeerInfo, 2, true).encode(infoPayload));
335
333
  }
334
+ const peerCleanup = async () => {
335
+ for (const peer of Object.values(this.peerHousekeep)) {
336
+ await kickOutPeer(peer);
337
+ }
338
+ };
339
+ await peerCleanup();
340
+
341
+ // And now remove the rooms
342
+ const cleanupRooms = async (currentRoom) => await this.deleteRoom(currentRoom.roomName);
343
+ const housekeeping = async () => {
344
+ for (const room of Object.values(this.roomHousekeep)) {
345
+ await cleanupRooms(room);
346
+ }
347
+ };
348
+ await housekeeping();
336
349
 
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;
350
+ console.log(` -> housekeeping DONE`);
351
+ } else {
352
+ console.log(` -> housekeeping was interrupted`);
348
353
  }
349
- })();
350
- await this.cleanOutInProgress;
354
+
355
+ // and we stop by unsubscribing to all peer info messages inside all rooms
356
+ await this.communicator.unsubscribe(this.busTopics.roomPeerJoin('+', '+').build());
357
+ // unsubscribing to all room ping alive topic
358
+ await this.communicator.unsubscribe(this.busTopics.baseRoomAlivePing("+").build());
359
+ // cleanup the communicator
360
+ this.communicator.clearAutoGeneratedSubscriptions();
361
+ // set this flag to false at the end to make sure a new houskeeping cycle can only be started once
362
+ // all topics are unsubscribed - otherwise it won't gather the joined peers inside the room.
363
+ this.houseKeepingInProgress = false;
364
+ } finally {
365
+ // resolve the cycle promise so any awaiting startHousekeeping can proceed.
366
+ // done in finally so an exception inside the try block cannot deadlock the next cycle.
367
+ const resolve = this.resolveHousekeepingCycle;
368
+ this.housekeepingCycleInProgress = null;
369
+ this.resolveHousekeepingCycle = null;
370
+ if (resolve) resolve();
371
+ }
351
372
  }
352
373
 
353
374
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telemersive-bus",
3
- "version": "0.6.16",
3
+ "version": "0.6.17",
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": {