Procházet zdrojové kódy

feat: add cross-server event broadcasting

KernelDeimos před 11 měsíci
rodič
revize
1207a158bd

+ 1 - 0
packages/backend/package.json

@@ -60,6 +60,7 @@
     "response-time": "^2.3.2",
     "response-time": "^2.3.2",
     "seedrandom": "^3.0.5",
     "seedrandom": "^3.0.5",
     "socket.io": "^4.6.2",
     "socket.io": "^4.6.2",
+    "socket.io-client": "^4.6.2",
     "ssh2": "^1.13.0",
     "ssh2": "^1.13.0",
     "string-hash": "^1.1.3",
     "string-hash": "^1.1.3",
     "string-length": "^6.0.0",
     "string-length": "^6.0.0",

+ 3 - 0
packages/backend/src/CoreModule.js

@@ -239,6 +239,9 @@ const install = async ({ services, app, useapi }) => {
 
 
     const { ScriptService } = require('./services/ScriptService');
     const { ScriptService } = require('./services/ScriptService');
     services.registerService('script', ScriptService);
     services.registerService('script', ScriptService);
+    
+    const { BroadcastService } = require('./services/BroadcastService');
+    services.registerService('broadcast', BroadcastService);
 }
 }
 
 
 const install_legacy = async ({ services }) => {
 const install_legacy = async ({ services }) => {

+ 131 - 0
packages/backend/src/services/BroadcastService.js

@@ -0,0 +1,131 @@
+const { AdvancedBase } = require("@heyputer/puter-js-common");
+const { Endpoint } = require("../util/expressutil");
+const { UserActorType } = require("./auth/Actor");
+const BaseService = require("./BaseService");
+
+class Peer extends AdvancedBase {
+    static ONLINE = Symbol('ONLINE');
+    static OFFLINE = Symbol('OFFLINE');
+    
+    static MODULES = {
+        sioclient: require('socket.io-client'),
+    };
+
+    constructor (svc_broadcast, config) {
+        super();
+        this.svc_broadcast = svc_broadcast;
+        this.log = this.svc_broadcast.log;
+        this.config = config;
+    }
+    
+    send (data) {
+        if ( ! this.socket ) return;
+        this.socket.send(data)
+    }
+    
+    get state () {
+        try {
+            if ( this.socket?.connected ) return this.constructor.ONLINE;
+        } catch (e) {
+            console.error('could not get peer state', e);
+        }
+        return this.constructor.OFFLINE;
+    }
+    
+    connect () {
+        const address = this.config.address;
+        const socket = this.modules.sioclient(address, {
+            transports: ['websocket'],
+            path: '/wssinternal',
+            reconnection: true,
+            extraHeaders: {
+                ...(this.config.host ? {
+                    Host: this.config.host,
+                } : {})
+            }
+        });
+        socket.on('connect', () => {
+            this.log.info(`connected`, {
+                address: this.config.address
+            });
+        });
+        socket.on('disconnect', () => {
+            this.log.info(`disconnected`, {
+                address: this.config.address
+            });
+        });
+        socket.on('connect_error', e => {
+            this.log.info(`connection error`, {
+                address: this.config.address,
+                message: e.message,
+            });
+            console.log(e);
+        });
+        socket.on('error', e => {
+            this.log.info('error', {
+                message: e.message,
+            });
+        });
+
+        this.socket = socket;
+    }
+}
+
+class BroadcastService extends BaseService {
+    static MODULES = {
+        express: require('express'),
+        // ['socket.io']: require('socket.io'),
+    };
+    
+    _construct () {
+        this.peers_ = [];
+    }
+    
+    async _init () {
+        for ( const peer_config of this.config.peers ) {
+            const peer = new Peer(this, peer_config);
+            this.peers_.push(peer);
+            peer.connect();
+        }
+        
+        const svc_event = this.services.get('event');
+        svc_event.on('outer.*', this.on_event.bind(this));
+    }
+    
+    async on_event (key, data, meta) {
+        if ( meta.from_outside ) return;
+        
+        for ( const peer of this.peers_ ) {
+            if ( peer.state !== Peer.ONLINE ) continue;
+            peer.send({ key, data, meta });
+        }
+    }
+    
+    async ['__on_install.websockets'] (_, { server }) {
+        const svc_event = this.services.get('event');
+
+        const io = require('socket.io')(server, {
+            cors: { origin: '*' },
+            path: '/wssinternal',
+        });
+        
+        io.on('connection', async socket => {
+            socket.on('message', ({ key, data, meta }) => {
+                if ( meta.from_outside ) {
+                    this.log.noticeme('possible over-sending');
+                    return;
+                }
+                
+                meta.from_outside = true;
+                svc_event.emit(key, data, meta);
+            });
+        });
+        
+        
+        this.log.noticeme(
+            require('node:util').inspect(this.config)
+        );
+    }
+}
+
+module.exports = { BroadcastService };

+ 24 - 2
packages/backend/src/services/EventService.js

@@ -36,9 +36,11 @@ class ScopedEventBus {
 class EventService extends BaseService {
 class EventService extends BaseService {
     async _construct () {
     async _construct () {
         this.listeners_ = {};
         this.listeners_ = {};
+        this.global_listeners_ = [];
     }
     }
 
 
-    emit (key, data) {
+    emit (key, data, meta) {
+        meta = meta ?? {};
         const parts = key.split('.');
         const parts = key.split('.');
         for ( let i = 0; i < parts.length; i++ ) {
         for ( let i = 0; i < parts.length; i++ ) {
             const part = i === parts.length - 1
             const part = i === parts.length - 1
@@ -55,7 +57,7 @@ class EventService extends BaseService {
                 // event dispatch.
                 // event dispatch.
                 (async () => {
                 (async () => {
                     try {
                     try {
-                        await callback(key, data);
+                        await callback(key, data, meta);
                     } catch (e) {
                     } catch (e) {
                         this.errors.report('event-service.emit', {
                         this.errors.report('event-service.emit', {
                             source: e,
                             source: e,
@@ -66,6 +68,22 @@ class EventService extends BaseService {
                 })();
                 })();
             }
             }
         }
         }
+        
+        for ( const callback of this.global_listeners_ ) {
+            // IIAFE wrapper to catch errors without blocking
+            // event dispatch.
+            (async () => {
+                try {
+                    await callback(key, data, meta);
+                } catch (e) {
+                    this.errors.report('event-service.emit', {
+                        source: e,
+                        trace: true,
+                        alarm: true,
+                    });
+                }
+            })();
+        }
 
 
     }
     }
 
 
@@ -86,6 +104,10 @@ class EventService extends BaseService {
 
 
         return det;
         return det;
     }
     }
+    
+    on_all (callback) {
+        this.global_listeners_.push(callback);
+    }
 
 
     get_scoped (scope) {
     get_scoped (scope) {
         return new ScopedEventBus(this, scope);
         return new ScopedEventBus(this, scope);

+ 34 - 0
packages/backend/src/services/WSPushService.js

@@ -36,6 +36,8 @@ class WSPushService  extends AdvancedBase {
             this._on_upload_progress.bind(this));
             this._on_upload_progress.bind(this));
         this.svc_event.on('fs.storage.progress.*',
         this.svc_event.on('fs.storage.progress.*',
             this._on_upload_progress.bind(this));
             this._on_upload_progress.bind(this));
+        this.svc_event.on('outer.gui.*',
+            this._on_outer_gui.bind(this));
     }
     }
 
 
     async _on_fs_create (key, data) {
     async _on_fs_create (key, data) {
@@ -70,6 +72,11 @@ class WSPushService  extends AdvancedBase {
         for ( const user_id of user_id_list ) {
         for ( const user_id of user_id_list ) {
             io.to(user_id).emit('item.added', response);
             io.to(user_id).emit('item.added', response);
         }
         }
+
+        this.svc_event.emit('outer.gui.item.added', {
+            user_id_list,
+            response,
+        });
     }
     }
 
 
     async _on_fs_update (key, data) {
     async _on_fs_update (key, data) {
@@ -104,6 +111,11 @@ class WSPushService  extends AdvancedBase {
         for ( const user_id of user_id_list ) {
         for ( const user_id of user_id_list ) {
             io.to(user_id).emit('item.updated', response);
             io.to(user_id).emit('item.updated', response);
         }
         }
+        
+        this.svc_event.emit('outer.gui.item.updated', {
+            user_id_list,
+            response,
+        });
     }
     }
 
 
     async _on_fs_move (key, data) {
     async _on_fs_move (key, data) {
@@ -139,6 +151,11 @@ class WSPushService  extends AdvancedBase {
         for ( const user_id of user_id_list ) {
         for ( const user_id of user_id_list ) {
             io.to(user_id).emit('item.moved', response);
             io.to(user_id).emit('item.moved', response);
         }
         }
+
+        this.svc_event.emit('outer.gui.item.moved', {
+            user_id_list,
+            response,
+        });
     }
     }
 
 
     async _on_fs_pending (key, data) {
     async _on_fs_pending (key, data) {
@@ -172,6 +189,10 @@ class WSPushService  extends AdvancedBase {
         for ( const user_id of user_id_list ) {
         for ( const user_id of user_id_list ) {
             io.to(user_id).emit('item.pending', response);
             io.to(user_id).emit('item.pending', response);
         }
         }
+        this.svc_event.emit('outer.gui.item.pending', {
+            user_id_list,
+            response,
+        });
     }
     }
 
 
     async _on_upload_progress (key, data) {
     async _on_upload_progress (key, data) {
@@ -225,6 +246,19 @@ class WSPushService  extends AdvancedBase {
             })
             })
         })
         })
     }
     }
+    
+    async _on_outer_gui (key, { user_id_list, response }, meta) {
+        if ( ! meta.from_outside ) return;
+
+        key = key.slice('outer.gui.'.length);
+
+        const { socketio } = this.modules;
+
+        const io = socketio.getio();
+        for ( const user_id of user_id_list ) {
+            io.to(user_id).emit(key, response);
+        }
+    }
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 2 - 0
packages/backend/src/services/WebServerService.js

@@ -183,6 +183,8 @@ class WebServerService extends BaseService {
                 socket.broadcast.to(socket.user.id).emit('trash.is_empty', msg);
                 socket.broadcast.to(socket.user.id).emit('trash.is_empty', msg);
             });
             });
         });
         });
+        
+        await this.services.emit('install.websockets', { server });
     }
     }
 
 
     async _init () {
     async _init () {