import * as plugins from './smartsocket.plugins.js'; import * as pluginsTyped from './smartsocket.pluginstyped.js'; import * as interfaces from './interfaces/index.js'; import { SocketConnection } from './smartsocket.classes.socketconnection.js'; import { type ISocketFunctionCallDataRequest, SocketFunction, } from './smartsocket.classes.socketfunction.js'; import { type ISocketRequestDataObject, SocketRequest } from './smartsocket.classes.socketrequest.js'; import { logger } from './smartsocket.logging.js'; /** * interface for class SmartsocketClient */ export interface ISmartsocketClientOptions { port: number; url: string; alias: string; // an alias makes it easier to identify this client in a multo client environment autoReconnect?: boolean; } export class SmartsocketClient { // a unique id public shortId = plugins.isounique.uni(); // the shortId of the remote we connect to public remoteShortId: string = null; public alias: string; public socketConnection: SocketConnection; public serverUrl: string; public serverPort: number; public autoReconnect: boolean; // status handling public eventSubject = new plugins.smartrx.rxjs.Subject(); public eventStatus: interfaces.TConnectionStatus = 'new'; public socketFunctions = new plugins.lik.ObjectMap>(); public socketRequests = new plugins.lik.ObjectMap>(); // tagStore private tagStore: { [key: string]: interfaces.ITag } = {}; private tagStoreSubscription: plugins.smartrx.rxjs.Subscription; /** * adds a tag to a connection */ public async addTag(tagArg: interfaces.ITag) { if (this.socketConnection) { await this.socketConnection.addTag(tagArg); } else { this.tagStore[tagArg.id] = tagArg; } } /** * gets a tag by id * @param tagIdArg */ public async getTagById(tagIdArg: interfaces.ITag['id']) { return this.tagStore[tagIdArg]; } /** * removes a tag from a connection */ public async removeTagById(tagIdArg: interfaces.ITag['id']) { if (this.socketConnection) { this.socketConnection.removeTagById(tagIdArg); } else { delete this.tagStore[tagIdArg]; } } constructor(optionsArg: ISmartsocketClientOptions) { this.alias = optionsArg.alias; this.serverUrl = optionsArg.url; this.serverPort = optionsArg.port; this.autoReconnect = optionsArg.autoReconnect; } public addSocketFunction(socketFunction: SocketFunction) { this.socketFunctions.add(socketFunction); } /** * connect the client to the server */ public async connect() { const done = plugins.smartpromise.defer(); const smartenvInstance = new plugins.smartenv.Smartenv(); const socketIoClient: any = await smartenvInstance.getEnvAwareModule({ nodeModuleName: 'socket.io-client', webUrlArg: 'https://cdn.jsdelivr.net/npm/socket.io-client@4/dist/socket.io.js', getFunction: () => { const socketIoBrowserModule = (globalThis as any).io; // console.log('loaded socket.io for browser'); return socketIoBrowserModule; }, }); // console.log(socketIoClient); logger.log('info', 'trying to connect...'); const socketUrl = `${this.serverUrl}:${this.serverPort}`; this.socketConnection = new SocketConnection({ alias: this.alias, authenticated: false, side: 'client', smartsocketHost: this, socket: await socketIoClient .connect(socketUrl, { multiplex: true, rememberUpgrade: true, autoConnect: false, reconnectionAttempts: 0, rejectUnauthorized: socketUrl.startsWith('https://localhost') ? false : true, }) .open(), }); const timer = new plugins.smarttime.Timer(5000); timer.start(); timer.completed.then(() => { this.updateStatus('timedOut'); logger.log('warn', 'connection to server timed out.'); this.disconnect(true); }); // authentication flow this.socketConnection.socket.on('requestAuth', (dataArg: interfaces.IRequestAuthPayload) => { timer.reset(); logger.log('info', `server ${dataArg.serverAlias} requested authentication`); // lets register the authenticated event this.socketConnection.socket.on('authenticated', async () => { this.remoteShortId = dataArg.serverAlias; logger.log('info', 'client is authenticated'); this.socketConnection.authenticated = true; await this.socketConnection.listenToFunctionRequests(); }); this.socketConnection.socket.on('serverFullyReactive', async () => { // lets take care of retagging const oldTagStore = this.tagStore; this.tagStoreSubscription?.unsubscribe(); for (const keyArg of Object.keys(this.tagStore)) { this.socketConnection.addTag(this.tagStore[keyArg]); } this.tagStoreSubscription = this.socketConnection.tagStoreObservable.subscribe( (tagStoreArg) => { this.tagStore = tagStoreArg; } ); for (const tag of Object.keys(oldTagStore)) { await this.addTag(oldTagStore[tag]); } this.updateStatus('connected'); done.resolve(); }); // lets register the forbidden event this.socketConnection.socket.on('forbidden', async () => { logger.log('warn', `disconnecting due to being forbidden to use the ressource`); await this.disconnect(); }); // lets provide the actual auth data this.socketConnection.socket.emit('dataAuth', { alias: this.alias, }); }); // handle connection this.socketConnection.socket.on('connect', async () => {}); // handle disconnection and errors this.socketConnection.socket.on('disconnect', async () => { await this.disconnect(true); }); this.socketConnection.socket.on('reconnect_failed', async () => { await this.disconnect(true); }); this.socketConnection.socket.on('connect_error', async () => { await this.disconnect(true); }); return done.promise; } private disconnectRunning = false; /** * disconnect from the server */ public async disconnect(useAutoReconnectSetting = false) { if (this.disconnectRunning) { return; } this.disconnectRunning = true; this.updateStatus('disconnecting'); this.tagStoreSubscription?.unsubscribe(); if (this.socketConnection) { await this.socketConnection.disconnect(); this.socketConnection = undefined; logger.log('ok', 'disconnected socket!'); } else { this.disconnectRunning = false; logger.log('warn', 'tried to disconnect, without a SocketConnection'); return; } logger.log('warn', `disconnected from server ${this.remoteShortId}`); this.remoteShortId = null; if (this.autoReconnect && useAutoReconnectSetting && this.eventStatus !== 'connecting') { this.updateStatus('connecting'); console.log('debounced reconnect!'); await plugins.smartdelay.delayForRandom(10000, 20000); this.disconnectRunning = false; await this.connect(); } else { this.disconnectRunning = false; } } /** * stops the client completely */ public async stop() { this.autoReconnect = false; await this.disconnect(); } /** * dispatches a server call * @param functionNameArg * @param dataArg */ public async serverCall( functionNameArg: T['method'], dataArg: T['request'] ): Promise { const done = plugins.smartpromise.defer(); const socketRequest = new SocketRequest(this, { side: 'requesting', originSocketConnection: this.socketConnection, shortId: plugins.isounique.uni(), funcCallData: { funcName: functionNameArg, funcDataArg: dataArg, }, }); const response = await socketRequest.dispatch(); const result = response.funcDataArg; return result; } private updateStatus(statusArg: interfaces.TConnectionStatus) { if (this.eventStatus !== statusArg) { this.eventSubject.next(statusArg); } this.eventStatus = statusArg; } }