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.
- package/lib/BusManager.js +240 -68
- 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.
|
|
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
|
-
|
|
680
|
-
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
706
|
-
|
|
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 (
|
|
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(
|
|
906
|
+
await this.deleteSwitchBoardProxy(definition.port);
|
|
746
907
|
} catch(err){
|
|
747
|
-
console.error(` ...: ${err}. Interrupting process of stopping
|
|
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
|
-
|
|
784
|
-
|
|
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
|
-
|
|
801
|
-
|
|
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.
|
|
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": {
|