telemersive-bus 0.6.19 → 0.6.20

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 +240 -68
  2. package/package.json +1 -1
package/lib/BusManager.js CHANGED
@@ -21,6 +21,15 @@ const houseKeepingTimeOut_onPeerJoined = 5000;
21
21
  // OS ports — without this delay a room restart races against a still-dying proxy and fails
22
22
  // to bind the same port.
23
23
  const proxyShutdownSettleTime = 3000;
24
+ // how often the switchboard is asked whether it still holds the proxies it was
25
+ // told to run. deliberately on its own timer rather than part of housekeeping:
26
+ // housekeeping only runs when a peer joins or leaves, so a room in the middle of
27
+ // a performance can go for a very long time without a single cycle.
28
+ const switchBoardCheckInterval = 15000;
29
+ // how often a single port is sent to the switchboard again before it is left
30
+ // alone. a port that never starts is a real fault, and retrying it every round
31
+ // forever would only bury it in noise.
32
+ const maxPortRepairAttempts = 3;
24
33
  const ROOM_ID_MIN = 11;
25
34
  const ROOM_ID_MAX = 50;
26
35
 
@@ -52,6 +61,14 @@ class BusManager {
52
61
  this.resolveHousekeepingCycle = null;
53
62
  this.chatManagers = {};
54
63
  this.switchBoardURI = null;
64
+ this.switchBoardBaseURI = null;
65
+ // the switchboard's instance id as last seen. a different one means the
66
+ // switchboard was restarted and has forgotten every proxy we asked for.
67
+ this.switchBoardInstance = null;
68
+ this.switchBoardWatch = null;
69
+ // per room, how often each port has been sent to the switchboard again
70
+ // without it taking. keyed by room name, then by port.
71
+ this.portRepairAttempts = {};
55
72
  }
56
73
 
57
74
  /*********************************************************************
@@ -68,7 +85,134 @@ class BusManager {
68
85
  * @param _port switchboard port
69
86
  */
70
87
  configureSwitchBoard = (_url, _port) => {
71
- this.switchBoardURI = 'http://' + _url + ":" + _port + "/proxies/";
88
+ this.switchBoardBaseURI = 'http://' + _url + ":" + _port;
89
+ this.switchBoardURI = this.switchBoardBaseURI + "/proxies/";
90
+ }
91
+
92
+ /**
93
+ * start watching whether the switchboard still holds the proxies it was told
94
+ * to run.
95
+ *
96
+ * proxies live only in the switchboard's memory, so a restart leaves every
97
+ * room without a media path while the rooms themselves carry on: peers keep
98
+ * answering their pings, no room is torn down, and nothing would ever ask for
99
+ * the ports again. This manager is the only part that knows which proxies a
100
+ * room should have, so it is the one that has to notice and send them again.
101
+ */
102
+ startSwitchBoardWatch = () => {
103
+ if (this.switchBoardBaseURI === null || this.switchBoardWatch !== null) {
104
+ return;
105
+ }
106
+ this.switchBoardWatch = setInterval(() => {
107
+ this.checkSwitchBoard().catch((err) => {
108
+ console.error(` ...: ${err}. Could not check the switchboard.`);
109
+ });
110
+ }, switchBoardCheckInterval);
111
+ }
112
+
113
+ stopSwitchBoardWatch = () => {
114
+ if (this.switchBoardWatch !== null) {
115
+ clearInterval(this.switchBoardWatch);
116
+ this.switchBoardWatch = null;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * ask the switchboard what it is running and rebuild whatever is missing.
122
+ *
123
+ * only ports the switchboard does not know about are sent again. a port it
124
+ * knows but is not running is its own business: it revives those itself, and
125
+ * re-sending one would reset the switchboard's own give-up counter and leave
126
+ * the two of us restarting the same broken proxy forever.
127
+ */
128
+ checkSwitchBoard = async () => {
129
+ const health = await this.getSwitchBoardHealth();
130
+ if (health === null) {
131
+ return;
132
+ }
133
+ if (this.switchBoardInstance !== null && this.switchBoardInstance !== health.instance) {
134
+ console.log(` <- switchboard was restarted - it has forgotten the ports of ${Object.keys(this.rooms).length} room(s)`);
135
+ }
136
+ this.switchBoardInstance = health.instance;
137
+
138
+ for (const roomName of Object.keys(this.rooms)) {
139
+ const room = this.rooms[roomName];
140
+ if (room === undefined || room.flagRoom4cleanup) {
141
+ // a room on its way out must not have its ports built up again
142
+ continue;
143
+ }
144
+ const expected = this.roomProxyDefinitions(room.roomId);
145
+ const held = (health.rooms && health.rooms[roomName]) ? health.rooms[roomName] : 0;
146
+ if (held >= expected.length) {
147
+ continue;
148
+ }
149
+ await this.repairServerSidePortScripts(roomName, room.roomId);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * send the build commands again for the ports of a room the switchboard does
155
+ * not have.
156
+ */
157
+ repairServerSidePortScripts = async (_roomName, _roomId) => {
158
+ const held = await this.getSwitchBoardRoom(_roomName);
159
+ if (held === null) {
160
+ return;
161
+ }
162
+ const attempts = this.portRepairAttempts[_roomName] || {};
163
+ this.portRepairAttempts[_roomName] = attempts;
164
+ const missing = this.roomProxyDefinitions(_roomId).filter(
165
+ (definition) => !Object.prototype.hasOwnProperty.call(held, String(definition.port)));
166
+ const worthTrying = missing.filter(
167
+ (definition) => (attempts[definition.port] || 0) < maxPortRepairAttempts);
168
+ if (worthTrying.length === 0) {
169
+ return;
170
+ }
171
+ console.log(` -> room '${_roomName}' is missing ${missing.length} port(s) on the switchboard - sending ${worthTrying.length} of them again`);
172
+ const failed = await this.addSwitchBoardProxies(_roomName, worthTrying);
173
+ const failedPorts = new Set(failed.map((definition) => definition.port));
174
+ for (const definition of worthTrying) {
175
+ if (failedPorts.has(definition.port)) {
176
+ attempts[definition.port] = (attempts[definition.port] || 0) + 1;
177
+ if (attempts[definition.port] >= maxPortRepairAttempts) {
178
+ console.error(` <- giving up on port ${definition.port} of room '${_roomName}' after ${maxPortRepairAttempts} attempts`);
179
+ }
180
+ } else {
181
+ delete attempts[definition.port];
182
+ }
183
+ }
184
+ console.log(` <- restored ${worthTrying.length - failed.length} of ${worthTrying.length} port(s) for room ${_roomName}`);
185
+ }
186
+
187
+ /**
188
+ * what the switchboard is currently running. returns null when it cannot be
189
+ * reached, so a momentary hiccup is simply retried on the next round.
190
+ */
191
+ getSwitchBoardHealth = async () => {
192
+ try {
193
+ const res = await superagent.get(this.switchBoardBaseURI + '/health');
194
+ return JSON.parse(res.text);
195
+ } catch (err) {
196
+ console.error(` ...: ${err}. Could not reach the switchboard.`);
197
+ return null;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * the proxies the switchboard holds for a room, keyed by port. an unknown
203
+ * room answers 404, which means it holds none of them.
204
+ */
205
+ getSwitchBoardRoom = async (_roomName) => {
206
+ try {
207
+ const res = await superagent.get(this.switchBoardBaseURI + '/rooms/' + encodeURIComponent(_roomName));
208
+ return JSON.parse(res.text);
209
+ } catch (err) {
210
+ if (err.status === 404) {
211
+ return {};
212
+ }
213
+ console.error(` ...: ${err}. Could not read room '${_roomName}' from the switchboard.`);
214
+ return null;
215
+ }
72
216
  }
73
217
 
74
218
  /*********************************************************************
@@ -112,12 +256,15 @@ class BusManager {
112
256
  console.log(`-> clean out the house ... `);
113
257
  // we wait a moment to start with houskeeping
114
258
  setTimeout(this.startHousekeeping, houseKeepingTimeOut);
259
+ // and keep an eye on the switchboard, independently of housekeeping
260
+ this.startSwitchBoardWatch();
115
261
  };
116
262
 
117
263
  /**
118
264
  * disconnect from mqtt broker
119
265
  */
120
266
  disconnectServer = async () => {
267
+ this.stopSwitchBoardWatch();
121
268
  await this.communicator.disconnect();
122
269
  this.communicator.clearSubscriptions();
123
270
  };
@@ -673,45 +820,74 @@ class BusManager {
673
820
  /**
674
821
  * Start server side port scripts
675
822
  */
823
+ /**
824
+ * the full set of proxies a room is made of.
825
+ *
826
+ * this is the single description of a room's ports: it is used both to build
827
+ * a new room and to work out what is missing from one that already exists, so
828
+ * the two can not drift apart.
829
+ */
830
+ roomProxyDefinitions = (_roomId) => {
831
+ const definitions = [];
832
+ /** the ultragrid needs two hundert range */
833
+ for (let i = 0; i < 20; i++) {
834
+ // OSC proxies
835
+ definitions.push({
836
+ port: _roomId * 1000 + i * 10 + 9, many_port: _roomId * 1000 + i * 10 + 9,
837
+ type: 'many2manyBi', description: "UDP proxy for OSC, channel:" + i});
838
+ // Ultragrid video and audio proxies
839
+ definitions.push({
840
+ port: _roomId * 1000 + i * 10 + 2, many_port: _roomId * 1000 + i * 10 + 6,
841
+ type: 'one2manyMo', description: "Ultragrid-video proxy, channel:" + i});
842
+ definitions.push({
843
+ port: _roomId * 1000 + i * 10 + 4, many_port: _roomId * 1000 + i * 10 + 8,
844
+ type: 'one2manyMo', description: "Ultragrid-audio / NatNet-data proxy, channel:" + i});
845
+ // NatNet2OSC and NatNetBridge proxies
846
+ definitions.push({
847
+ port: _roomId * 1000 + i * 10 + 0, many_port: _roomId * 1000 + i * 10 + 1,
848
+ type: 'one2manyBi', description: "MoCap/NatNet-Ctrl proxy, channel:" + i});
849
+ }
850
+ // Stage Control Many to many proxy
851
+ definitions.push({
852
+ port: _roomId * 1000 + 902, many_port: _roomId * 1000 + 902,
853
+ type: 'many2manyBi', description: "UDP proxy for Open Stage Control"});
854
+ definitions.push({
855
+ port: _roomId * 1000 + 900, many_port: _roomId * 1000 + 902,
856
+ type: 'OpenStageControl', description: "Open Stage Control instance"});
857
+ return definitions;
858
+ }
859
+
860
+ /**
861
+ * ask the switchboard to run the given proxies. returns the definitions it
862
+ * would not start, so the caller can tell a fully built room from a partial
863
+ * one instead of the failures only being logged and forgotten.
864
+ */
865
+ addSwitchBoardProxies = async (_roomName, _definitions) => {
866
+ const failed = [];
867
+ for (const definition of _definitions) {
868
+ try {
869
+ await this.addSwitchBoardProxy(_roomName, definition.port,
870
+ definition.many_port, definition.type, definition.description);
871
+ } catch (err) {
872
+ failed.push(definition);
873
+ console.error(` ...: ${err}. Interrupting process of starting up ${definition.type} script on port ${definition.port}.`);
874
+ }
875
+ }
876
+ return failed;
877
+ }
878
+
676
879
  startServerSidePortScripts = async (_roomName, _roomId) => {
677
880
  if(this.switchBoardURI !== null){
678
881
  console.log(` -> starting ports for room '${_roomName}' on range '${_roomId}000 - ${_roomId}999`);
679
- /** the ultragrid needs two hundert range */
680
- for (let i = 0; i < 20; i++) {
681
- // OSC proxies
682
- try{
683
- await this.addSwitchBoardProxy(_roomName,_roomId * 1000 + i * 10 + 9, _roomId * 1000 + i * 10 + 9, 'many2manyBi', "UDP proxy for OSC, channel:" + i);
684
- } catch(err){
685
- console.error(` ...: ${err}. Interrupting process of starting up many2manyBi script on port ${_roomId * 1000 + i * 10 + 9}.`);
686
- }
687
- // Ultragrid video and audio proxies
688
- try{
689
- await this.addSwitchBoardProxy(_roomName, _roomId * 1000 + i * 10 + 2, _roomId * 1000 + i * 10 + 6,'one2manyMo', "Ultragrid-video proxy, channel:" + i);
690
- } catch(err){
691
- console.error(` ...: ${err}. Interrupting process of starting up one2manyMo script on port ${_roomId * 1000 + i * 10 + 2}.`);
692
- }
693
- try{
694
- await this.addSwitchBoardProxy(_roomName, _roomId * 1000 + i * 10 + 4, _roomId * 1000 + i * 10 + 8, 'one2manyMo', "Ultragrid-audio / NatNet-data proxy, channel:" + i);
695
- } catch(err){
696
- console.error(` ...: ${err}. Interrupting process of starting up one2manyMo script on port ${_roomId * 1000 + i * 10 + 4}.`);
697
- }
698
- // NatNet2OSC and NatNetBridge proxies
699
- try{
700
- await this.addSwitchBoardProxy(_roomName, _roomId * 1000 + i * 10 + 0, _roomId * 1000 + i * 10 + 1, 'one2manyBi', "MoCap/NatNet-Ctrl proxy, channel:" + i);
701
- } catch(err){
702
- console.error(` ...: ${err}. Interrupting process of starting up one2manyBi script on port ${_roomId * 1000 + i * 10 + 0}.`);
703
- }
882
+ let failed = await this.addSwitchBoardProxies(_roomName, this.roomProxyDefinitions(_roomId));
883
+ if (failed.length > 0) {
884
+ // retry once before giving up: a port that is not created here is
885
+ // simply absent, and nothing downstream would ever notice it.
886
+ console.log(` -> retrying ${failed.length} port(s) for room '${_roomName}' that could not be started`);
887
+ failed = await this.addSwitchBoardProxies(_roomName, failed);
704
888
  }
705
- // Stage Control Many to many proxy
706
- try{
707
- await this.addSwitchBoardProxy(_roomName,_roomId * 1000 + 902, _roomId * 1000 + 902, 'many2manyBi', "UDP proxy for Open Stage Control");
708
- } catch(err){
709
- console.error(` ...: ${err}. Interrupting process of starting up many2manyBi script on port ${_roomId * 1000 + 902}.`);
710
- }
711
- try{
712
- await this.addSwitchBoardProxy(_roomName,_roomId * 1000 + 900, _roomId * 1000 + 902, 'OpenStageControl', "Open Stage Control instance");
713
- } catch(err){
714
- console.error(` ...: ${err}. Interrupting process of starting up many2manyBi script on port ${_roomId * 1000 + 902}.`);
889
+ if (failed.length > 0) {
890
+ console.error(` <- room ${_roomName} is missing ${failed.length} of ${this.roomProxyDefinitions(_roomId).length} ports: ${failed.map(d => d.port).join(', ')}`);
715
891
  }
716
892
  console.log(` <- started all ports for room ${_roomName}`);
717
893
  } else {
@@ -725,43 +901,19 @@ class BusManager {
725
901
  stopServerSidePortScripts = async (_roomName, _roomId) => {
726
902
  if(this.switchBoardURI !== null) {
727
903
  console.log(` -> stopping ports for room '${_roomName}' on range '${_roomId}000 - ${_roomId}999`);
728
- for (let i = 0; i < 20; i++) {
729
- try{
730
- await this.deleteSwitchBoardProxy(_roomId * 1000 + i * 10 + 9);
731
- } catch(err){
732
- console.error(` ...: ${err}. Interrupting process of stopping many2manyBi script on port ${_roomId * 1000 + i * 10 + 9}.`);
733
- }
734
- try{
735
- await this.deleteSwitchBoardProxy(_roomId * 1000 + i * 10 + 2);
736
- } catch(err){
737
- console.error(` ...: ${err}. Interrupting process of stopping one2manyMo script on port ${_roomId * 1000 + i * 10 + 2}.`);
738
- }
739
- try{
740
- await this.deleteSwitchBoardProxy(_roomId * 1000 + i * 10 + 4);
741
- } catch(err){
742
- console.error(` ...: ${err}. Interrupting process of stopping one2manyMo script on port ${_roomId * 1000 + i * 10 + 4}.`);
743
- }
904
+ for (const definition of this.roomProxyDefinitions(_roomId)) {
744
905
  try{
745
- await this.deleteSwitchBoardProxy(_roomId * 1000 + i * 10 + 0);
906
+ await this.deleteSwitchBoardProxy(definition.port);
746
907
  } catch(err){
747
- console.error(` ...: ${err}. Interrupting process of stopping one2manyBi script on port ${_roomId * 1000 + i * 10 + 0}.`);
908
+ console.error(` ...: ${err}. Interrupting process of stopping ${definition.type} script on port ${definition.port}.`);
748
909
  }
749
910
  }
750
- try{
751
- await this.deleteSwitchBoardProxy(_roomId * 1000 + 902);
752
- } catch(err){
753
- console.error(` ...: ${err}. Interrupting process of stopping many2manyBi script on port ${_roomId * 1000 + 902}.`);
754
- }
755
- try{
756
- await this.deleteSwitchBoardProxy(_roomId * 1000 + 900);
757
- } catch(err){
758
- console.error(` ...: ${err}. Interrupting process of stopping open stage control instance on port ${_roomId * 1000 + 900}.`);
759
- }
760
911
  console.log(` <- stopped all ports for room ${_roomName}`);
761
912
  // wait for proxy processes to fully exit and release OS ports before allowing
762
913
  // the room to be recreated. without this, a restart races against dying proxies.
763
914
  await new Promise(resolve => setTimeout(resolve, proxyShutdownSettleTime));
764
915
  delete this.rooms[_roomName];
916
+ delete this.portRepairAttempts[_roomName];
765
917
  console.log(` <- removed room ${_roomName}`);
766
918
  }else {
767
919
  console.log(` ...stopped no ports for room ${_roomName}: no switchboard url/port defined`);
@@ -780,8 +932,18 @@ class BusManager {
780
932
  //let reply = JSON.parse(res["text"]);
781
933
  } catch (err) {
782
934
  if (err.response?.text) {
783
- let reply = JSON.parse(err.response["text"]);
784
- throw new Error(reply["msg"]);
935
+ // the switchboard answers errors as json, but an unhandled one
936
+ // arrives as an html page - do not let parsing that hide the
937
+ // actual failure behind a SyntaxError.
938
+ try {
939
+ const reply = JSON.parse(err.response["text"]);
940
+ throw new Error(reply["msg"]);
941
+ } catch (parseErr) {
942
+ if (parseErr instanceof SyntaxError) {
943
+ throw new Error(`switchboard responded ${err.status}`);
944
+ }
945
+ throw parseErr;
946
+ }
785
947
  }
786
948
  throw err;
787
949
  }
@@ -797,8 +959,18 @@ class BusManager {
797
959
  //let reply = JSON.parse(res["text"]);
798
960
  } catch (err) {
799
961
  if (err.response?.text) {
800
- let reply = JSON.parse(err.response["text"]);
801
- throw new Error(reply["msg"]);
962
+ // the switchboard answers errors as json, but an unhandled one
963
+ // arrives as an html page - do not let parsing that hide the
964
+ // actual failure behind a SyntaxError.
965
+ try {
966
+ const reply = JSON.parse(err.response["text"]);
967
+ throw new Error(reply["msg"]);
968
+ } catch (parseErr) {
969
+ if (parseErr instanceof SyntaxError) {
970
+ throw new Error(`switchboard responded ${err.status}`);
971
+ }
972
+ throw parseErr;
973
+ }
802
974
  }
803
975
  throw err;
804
976
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telemersive-bus",
3
- "version": "0.6.19",
3
+ "version": "0.6.20",
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": {