UNPKG

@softvisio/core

Version:
373 lines (286 loc) • 10.3 kB
import Context from "#lib/app/api/frontend/context"; import env from "#lib/env"; import Events from "#lib/events"; import ServerWebSocketConnection from "#lib/http/server/websocket-connection"; import JsonContainer from "#lib/json-container"; import uuid from "#lib/uuid"; const LOCAL_EVENTS = new Set( [ // "connect", "disconnect", ] ); export default class WebSocketConnection extends ServerWebSocketConnection { #id = uuid(); #ctx; #locale; #incomingEvents; #outgoingEvents; #publishRemoteIncomingEvent; #events; #abortControllers = {}; #sessionStateEvents; constructor ( { server, ws, options, incomingEvents, outgoingEvents, publishRemoteIncomingEvent } ) { super( server, ws, options ); this.#incomingEvents = incomingEvents; this.#outgoingEvents = outgoingEvents; this.#publishRemoteIncomingEvent = publishRemoteIncomingEvent; this.#events = new Events().link( this.#outgoingEvents, { "on": this.#subscriber.bind( this ), } ); const ctx = this.data.ctx; delete this.data.ctx; this.#locale = this.data.locale; delete this.data.locale; // clone context this.#ctx = new Context( ctx.api, { "token": ctx.token, "user": ctx.user, "telegramBotUser": ctx.telegramBotUser, "isDeleted": ctx.isDeleted, "connection": this, "hostname": ctx.hostname, "userAgent": ctx.userAgent, "remoteAddress": ctx.remoteAddress, } ); // update last activity timestamp this.#ctx.updateLastActivity(); this.#incomingEvents.emit( "connect", this ); if ( this.api.isApi ) { this.#sessionStateEvents = new Events().link( this.app, { "increaseMaxListeners": true, } ); if ( this.ctx.user ) { // user disabled this.#sessionStateEvents.on( `user/${ this.ctx.user.id }/disable`, this.#onSessionDisable.bind( this ) ); // user email updated this.#sessionStateEvents.on( `user/${ this.ctx.user.id }/email/update`, this.#onSessionReload.bind( this ) ); } // api token if ( this.ctx.token?.isApiToken ) { // api token disabled this.#sessionStateEvents.on( `api-token/${ this.ctx.token.id }/disable`, this.#onSessionDisable.bind( this ) ); // api token delete this.#sessionStateEvents.on( `api-token/${ this.ctx.token.id }/delete`, this.#onSessionDelete.bind( this ) ); } // session token else if ( this.ctx.token?.isSessionToken ) { // session token delete this.#sessionStateEvents.on( `api-session/${ this.ctx.token.id }/delete`, this.#onSessionDelete.bind( this ) ); } // telegram bot user else if ( this.ctx.telegramBotUser ) { // telegram bot user disabled this.#sessionStateEvents.on( `telegram-bot/${ this.ctx.telegramBot.id }/user/${ this.ctx.telegramBotUser.id }/disable`, this.#onSessionDisable.bind( this ) ); // telegram bot user unsubscribed this.#sessionStateEvents.on( `telegram-bot/${ this.ctx.telegramBot.id }/user/${ this.ctx.telegramBotUser.id }/unsubscribe`, this.#onSessionDisable.bind( this ) ); // telegram bot user api user update this.#sessionStateEvents.on( `telegram-bot/${ this.ctx.telegramBot.id }/user/${ this.ctx.telegramBotUser.id }/api-user/update`, this.#onSessionDisable.bind( this ) ); } } } // static static get localEvents () { return LOCAL_EVENTS; } // properties get id () { return this.#id; } get app () { return this.#ctx.api.app; } get api () { return this.#ctx.api; } get ctx () { return this.#ctx; } get locale () { return this.#locale; } // public send ( msg ) { if ( !this.isConnected ) return; // locale if ( this.#locale ) { msg = new JsonContainer( msg, { "translation": { "localeDomain": this.#locale, }, } ); } super.send( JSON.stringify( msg ), false ); } // protected _onDisconnect ( res ) { this.#sessionStateEvents?.unlinkAll(); // unsubscribe from all events this.#events.clear(); super._onDisconnect( res ); this.#incomingEvents.emit( "disconnect", this ); } async _onMessage ( msg, isBinary ) { if ( isBinary ) return; // update last activity timestamp this.#ctx.updateLastActivity(); // try to decode message try { msg = JSON.parse( Buffer.from( msg ) ); } catch { return; } // request if ( msg.method ) { if ( !Array.isArray( msg.params ) ) { msg.params = msg.params === undefined ? [] : [ msg.params ]; } // subscribe if ( msg.method === "/subscribe" ) { this.#onSubscribe( ...msg.params ); } // unsubscribe else if ( msg.method === "/unsubscribe" ) { this.#onUnsubscribe( ...msg.params ); } // publish else if ( msg.method === "/publish" ) { this.#publishRemoteIncomingEvent( this.#ctx, msg.params ); } // abort else if ( msg.method === "/abort" ) { this.#abortControllers[ msg.id ]?.abort(); delete this.#abortControllers[ msg.id ]; } // get schema else if ( msg.method === "/schema" ) { if ( msg.id ) { this.send( result( 200, this.ctx.api.schema ).toJsonRpc( msg.id ) ); } } // rpc else { // regular call if ( msg.id ) { const signal = ( this.#abortControllers[ msg.id ] = new AbortController() ).signal; const res = await this.#ctx.call( { "method": msg.method, "args": msg.params, signal, } ); delete this.#abortControllers[ msg.id ]; if ( !signal.aborted ) this.send( res.toJsonRpc( msg.id ) ); } // void call else { this.#ctx.voidCall( msg.method, ...msg.params ); } } } } // private #onSubscribe ( names ) { if ( !names || !Array.isArray( names ) ) return; const ctx = this.ctx; // context is deleted if ( ctx.isDeleted ) return; for ( const name of names ) { // already subscribed if ( this.#events.hasListeners( name ) ) continue; // event name is reserved if ( LOCAL_EVENTS.has( name ) ) continue; if ( !this.api.schema.isEventValid( name ) ) { if ( env.isDevelopment ) console.log( `ERROR: ignore unregistered event "${ name }"` ); continue; } // subscribe to the user events this.#events.on( name, this.#publishEvent.bind( this, name ) ); } } #onUnsubscribe ( names ) { if ( !names || !Array.isArray( names ) ) return; for ( const name of names ) this.#events.offAll( name ); } #subscriber ( name ) { const ctx = this.ctx, names = []; if ( this.api.isRpc ) { names.push( name ); } else { names.push( `${ name }/all` ); if ( ctx.user ) { names.push( `${ name }/users`, `${ name }/${ ctx.user.id }` ); if ( ctx.user.isRoot ) names.push( `${ name }/root` ); } else { names.push( `${ name }/guests` ); } } return names; } async #publishEvent ( name, args, cache, publisherId ) { // connection closed if ( !this.isConnected ) return; if ( publisherId === this.#id ) return; const ctx = this.#ctx; // context is deleted if ( ctx.isDeleted ) return; // update context if ( ctx.token ) { // update context await ctx.update(); // context is deleted or disabled if ( ctx.isDeleted || !ctx.isEnabled ) return; } if ( this.#locale ) { cache[ this.#locale ] ??= JSON.stringify( new JsonContainer( { "jsonrpc": "2.0", "method": "/publish", "params": [ name, ...args ], }, { "translation": { "localeDomain": this.#locale, }, } ) ); super.send( cache[ this.#locale ], false ); } else { cache.noLocale ??= JSON.stringify( { "jsonrpc": "2.0", "method": "/publish", "params": [ name, ...args ], } ); super.send( cache.noLocale, false ); } } #onSessionDisable () { const msg = JSON.stringify( { "jsonrpc": "2.0", "method": "/publish", "params": [ "sessionDisable" ], } ); super.send( msg ); } #onSessionDelete () { const msg = JSON.stringify( { "jsonrpc": "2.0", "method": "/publish", "params": [ "sessionDelete" ], } ); super.send( msg ); } #onSessionReload () { const msg = JSON.stringify( { "jsonrpc": "2.0", "method": "/publish", "params": [ "sessionReload" ], } ); super.send( msg ); } }