Skip to content

Commit 1207a15

Browse files
committed
feat: add cross-server event broadcasting
1 parent 3ae0773 commit 1207a15

File tree

6 files changed

+195
-2
lines changed

6 files changed

+195
-2
lines changed

packages/backend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"response-time": "^2.3.2",
6161
"seedrandom": "^3.0.5",
6262
"socket.io": "^4.6.2",
63+
"socket.io-client": "^4.6.2",
6364
"ssh2": "^1.13.0",
6465
"string-hash": "^1.1.3",
6566
"string-length": "^6.0.0",

packages/backend/src/CoreModule.js

+3
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ const install = async ({ services, app, useapi }) => {
239239

240240
const { ScriptService } = require('./services/ScriptService');
241241
services.registerService('script', ScriptService);
242+
243+
const { BroadcastService } = require('./services/BroadcastService');
244+
services.registerService('broadcast', BroadcastService);
242245
}
243246

244247
const install_legacy = async ({ services }) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
const { AdvancedBase } = require("@heyputer/puter-js-common");
2+
const { Endpoint } = require("../util/expressutil");
3+
const { UserActorType } = require("./auth/Actor");
4+
const BaseService = require("./BaseService");
5+
6+
class Peer extends AdvancedBase {
7+
static ONLINE = Symbol('ONLINE');
8+
static OFFLINE = Symbol('OFFLINE');
9+
10+
static MODULES = {
11+
sioclient: require('socket.io-client'),
12+
};
13+
14+
constructor (svc_broadcast, config) {
15+
super();
16+
this.svc_broadcast = svc_broadcast;
17+
this.log = this.svc_broadcast.log;
18+
this.config = config;
19+
}
20+
21+
send (data) {
22+
if ( ! this.socket ) return;
23+
this.socket.send(data)
24+
}
25+
26+
get state () {
27+
try {
28+
if ( this.socket?.connected ) return this.constructor.ONLINE;
29+
} catch (e) {
30+
console.error('could not get peer state', e);
31+
}
32+
return this.constructor.OFFLINE;
33+
}
34+
35+
connect () {
36+
const address = this.config.address;
37+
const socket = this.modules.sioclient(address, {
38+
transports: ['websocket'],
39+
path: '/wssinternal',
40+
reconnection: true,
41+
extraHeaders: {
42+
...(this.config.host ? {
43+
Host: this.config.host,
44+
} : {})
45+
}
46+
});
47+
socket.on('connect', () => {
48+
this.log.info(`connected`, {
49+
address: this.config.address
50+
});
51+
});
52+
socket.on('disconnect', () => {
53+
this.log.info(`disconnected`, {
54+
address: this.config.address
55+
});
56+
});
57+
socket.on('connect_error', e => {
58+
this.log.info(`connection error`, {
59+
address: this.config.address,
60+
message: e.message,
61+
});
62+
console.log(e);
63+
});
64+
socket.on('error', e => {
65+
this.log.info('error', {
66+
message: e.message,
67+
});
68+
});
69+
70+
this.socket = socket;
71+
}
72+
}
73+
74+
class BroadcastService extends BaseService {
75+
static MODULES = {
76+
express: require('express'),
77+
// ['socket.io']: require('socket.io'),
78+
};
79+
80+
_construct () {
81+
this.peers_ = [];
82+
}
83+
84+
async _init () {
85+
for ( const peer_config of this.config.peers ) {
86+
const peer = new Peer(this, peer_config);
87+
this.peers_.push(peer);
88+
peer.connect();
89+
}
90+
91+
const svc_event = this.services.get('event');
92+
svc_event.on('outer.*', this.on_event.bind(this));
93+
}
94+
95+
async on_event (key, data, meta) {
96+
if ( meta.from_outside ) return;
97+
98+
for ( const peer of this.peers_ ) {
99+
if ( peer.state !== Peer.ONLINE ) continue;
100+
peer.send({ key, data, meta });
101+
}
102+
}
103+
104+
async ['__on_install.websockets'] (_, { server }) {
105+
const svc_event = this.services.get('event');
106+
107+
const io = require('socket.io')(server, {
108+
cors: { origin: '*' },
109+
path: '/wssinternal',
110+
});
111+
112+
io.on('connection', async socket => {
113+
socket.on('message', ({ key, data, meta }) => {
114+
if ( meta.from_outside ) {
115+
this.log.noticeme('possible over-sending');
116+
return;
117+
}
118+
119+
meta.from_outside = true;
120+
svc_event.emit(key, data, meta);
121+
});
122+
});
123+
124+
125+
this.log.noticeme(
126+
require('node:util').inspect(this.config)
127+
);
128+
}
129+
}
130+
131+
module.exports = { BroadcastService };

packages/backend/src/services/EventService.js

+24-2
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,11 @@ class ScopedEventBus {
3636
class EventService extends BaseService {
3737
async _construct () {
3838
this.listeners_ = {};
39+
this.global_listeners_ = [];
3940
}
4041

41-
emit (key, data) {
42+
emit (key, data, meta) {
43+
meta = meta ?? {};
4244
const parts = key.split('.');
4345
for ( let i = 0; i < parts.length; i++ ) {
4446
const part = i === parts.length - 1
@@ -55,7 +57,7 @@ class EventService extends BaseService {
5557
// event dispatch.
5658
(async () => {
5759
try {
58-
await callback(key, data);
60+
await callback(key, data, meta);
5961
} catch (e) {
6062
this.errors.report('event-service.emit', {
6163
source: e,
@@ -66,6 +68,22 @@ class EventService extends BaseService {
6668
})();
6769
}
6870
}
71+
72+
for ( const callback of this.global_listeners_ ) {
73+
// IIAFE wrapper to catch errors without blocking
74+
// event dispatch.
75+
(async () => {
76+
try {
77+
await callback(key, data, meta);
78+
} catch (e) {
79+
this.errors.report('event-service.emit', {
80+
source: e,
81+
trace: true,
82+
alarm: true,
83+
});
84+
}
85+
})();
86+
}
6987

7088
}
7189

@@ -86,6 +104,10 @@ class EventService extends BaseService {
86104

87105
return det;
88106
}
107+
108+
on_all (callback) {
109+
this.global_listeners_.push(callback);
110+
}
89111

90112
get_scoped (scope) {
91113
return new ScopedEventBus(this, scope);

packages/backend/src/services/WSPushService.js

+34
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class WSPushService extends AdvancedBase {
3636
this._on_upload_progress.bind(this));
3737
this.svc_event.on('fs.storage.progress.*',
3838
this._on_upload_progress.bind(this));
39+
this.svc_event.on('outer.gui.*',
40+
this._on_outer_gui.bind(this));
3941
}
4042

4143
async _on_fs_create (key, data) {
@@ -70,6 +72,11 @@ class WSPushService extends AdvancedBase {
7072
for ( const user_id of user_id_list ) {
7173
io.to(user_id).emit('item.added', response);
7274
}
75+
76+
this.svc_event.emit('outer.gui.item.added', {
77+
user_id_list,
78+
response,
79+
});
7380
}
7481

7582
async _on_fs_update (key, data) {
@@ -104,6 +111,11 @@ class WSPushService extends AdvancedBase {
104111
for ( const user_id of user_id_list ) {
105112
io.to(user_id).emit('item.updated', response);
106113
}
114+
115+
this.svc_event.emit('outer.gui.item.updated', {
116+
user_id_list,
117+
response,
118+
});
107119
}
108120

109121
async _on_fs_move (key, data) {
@@ -139,6 +151,11 @@ class WSPushService extends AdvancedBase {
139151
for ( const user_id of user_id_list ) {
140152
io.to(user_id).emit('item.moved', response);
141153
}
154+
155+
this.svc_event.emit('outer.gui.item.moved', {
156+
user_id_list,
157+
response,
158+
});
142159
}
143160

144161
async _on_fs_pending (key, data) {
@@ -172,6 +189,10 @@ class WSPushService extends AdvancedBase {
172189
for ( const user_id of user_id_list ) {
173190
io.to(user_id).emit('item.pending', response);
174191
}
192+
this.svc_event.emit('outer.gui.item.pending', {
193+
user_id_list,
194+
response,
195+
});
175196
}
176197

177198
async _on_upload_progress (key, data) {
@@ -225,6 +246,19 @@ class WSPushService extends AdvancedBase {
225246
})
226247
})
227248
}
249+
250+
async _on_outer_gui (key, { user_id_list, response }, meta) {
251+
if ( ! meta.from_outside ) return;
252+
253+
key = key.slice('outer.gui.'.length);
254+
255+
const { socketio } = this.modules;
256+
257+
const io = socketio.getio();
258+
for ( const user_id of user_id_list ) {
259+
io.to(user_id).emit(key, response);
260+
}
261+
}
228262
}
229263

230264
module.exports = {

packages/backend/src/services/WebServerService.js

+2
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ class WebServerService extends BaseService {
183183
socket.broadcast.to(socket.user.id).emit('trash.is_empty', msg);
184184
});
185185
});
186+
187+
await this.services.emit('install.websockets', { server });
186188
}
187189

188190
async _init () {

0 commit comments

Comments
 (0)