UNPKG

@litert/televoke

Version:
354 lines (236 loc) 7.82 kB
/** * Copyright 2025 Angus.Fenying <fenying@litert.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as Shared from '../../shared'; import type * as dT from '../Transporter.decl'; import { EventEmitter } from 'node:events'; import * as Crypto from 'node:crypto'; export interface IGatewayOptions { name?: string; server: dT.IServer; } function createRandomName(): string { return Crypto.randomBytes(8).toString('hex'); } interface IMemoryExchangeEvents extends Shared.IDefaultEvents { ['data_a'](data: any): void; ['data_b'](data: any): void; ['end_a'](): void; ['end_b'](): void; close(): void; } class MemoryExchange extends EventEmitter implements Shared.IEventListener<IMemoryExchangeEvents> { private _aEnded: boolean = false; private _bEnded: boolean = false; public constructor( public readonly name: string ) { super(); } public send(targetEndpoint: 'a' | 'b', data: any): void { setImmediate(() => this.emit('data_' + targetEndpoint, data)); } public isEnded(endpoint: 'a' | 'b'): boolean { if (endpoint === 'a') { return this._aEnded; } return this._bEnded; } public end(endpoint: 'a' | 'b'): void { if (this._aEnded && this._bEnded) { return; } if (endpoint === 'a') { if (this._aEnded) { return; } this._aEnded = true; setImmediate(() => this.emit('end_a')); } else { if (this._bEnded) { return; } this._bEnded = true; setImmediate(() => this.emit('end_b')); } if (this._aEnded && this._bEnded) { setImmediate(() => this.emit('close')); } } public close(): void { this.end('a'); this.end('b'); } } class MemorySocket extends EventEmitter implements dT.ITransporter, Shared.ITransporter { private readonly _targetEndpoint: 'a' | 'b'; public readonly protocol: string = 'memory'; public constructor( private readonly _exchange: MemoryExchange, private readonly _endpoint: 'a' | 'b' ) { super(); this._targetEndpoint = _endpoint === 'a' ? 'b' : 'a'; this._exchange .on('end_' + this._targetEndpoint, () => { try { this.emit('end'); } catch (e) { this.emit('error', e); } }) .on('data_' + this._endpoint, (data) => { try { this.emit('frame', data); } catch (e) { this.emit('error', e); } }) .on('error', (e) => this.emit('error', e)) .on('close', () => this.emit('close')); } public get writable(): boolean { return !this._exchange.isEnded(this._endpoint); } public getProperty(name: string): unknown { switch (name) { case 'remoteAddress': return 'localhost'; case 'remotePort': return 0; case 'localAddress': return 'localhost'; case 'localPort': return 0; default: return null; } } public getPropertyNames(): string[] { return ['remoteAddress', 'remotePort', 'localAddress', 'localPort']; } public getAllProperties(): Record<string, unknown> { return { 'remoteAddress': 'localhost', 'remotePort': 0, 'localAddress': 'localhost', 'localPort': 0, }; } public write(data: Array<string | Buffer>): void { if (this._exchange.isEnded(this._endpoint)) { throw new Shared.errors.network_error({ reason: 'conn_lost' }); } for (let i = 0; i < data.length; ++i) { if (!(data[i] instanceof Buffer)) { data[i] = Buffer.from(data[i]); } } this._exchange.send(this._targetEndpoint, data); } public destroy(): void { this._exchange.close(); } public end(): void { this._exchange.end(this._endpoint); } } export interface IMemoryGateway extends dT.IGateway { readonly running: boolean; readonly name: string; } class MemoryGateway extends EventEmitter implements IMemoryGateway { private _running: boolean = false; private readonly _exchanges: Record<string, MemoryExchange> = {}; public constructor( public readonly name: string, private readonly _server: dT.IServer, // private readonly _opts: D.IServerOptions ) { super(); } public get running(): boolean { return this._running; } private _createExchange(): MemoryExchange { const ex = new MemoryExchange(this._generateExchangeName()); return this._exchanges[ex.name] = ex; } private _generateExchangeName(): string { let name: string; do { name = createRandomName(); } while (this._exchanges[name]); return name; } public connect(): dT.ITransporter { if (!this._running) { throw new Shared.errors.network_error({ reason: 'conn_refused' }); } const ex = this._createExchange(); const serverSocket = new MemorySocket(ex, 'a'); const clientSocket = new MemorySocket(ex, 'b'); ex.on('close', () => { delete this._exchanges[ex.name]; }); this._server.registerChannel(serverSocket); return clientSocket; } public start(): Promise<void> { if (!this._running) { this._running = true; this.emit('listening'); } return Promise.resolve(); } public stop(): Promise<void> { if (!this._running) { return Promise.resolve(); } for (const ex of Object.values(this._exchanges)) { ex.close(); } this.emit('close'); return Promise.resolve(); } } const servers: Record<string, MemoryGateway> = {}; export function createServer(opts: IGatewayOptions, listener?: (socket: dT.ITransporter) => void): MemoryGateway { opts.name ??= createRandomName(); if (servers[opts.name]) { throw new Shared.errors.network_error({ reason: 'dup_listen' }); } const server = new MemoryGateway(opts.name, opts.server); servers[opts.name] = server; if (listener) { server.on('connection', listener); } server.on('close', () => { delete servers[opts.name!]; }); return server; } class MemoryConnector implements dT.IConnector { public constructor( private readonly _name: string ) {} public connect(): Promise<dT.ITransporter> { if (!servers[this._name]) { throw new Shared.errors.network_error({ reason: 'unknown_dest' }); } return Promise.resolve(servers[this._name].connect()); } } export function createConnector(name: string): dT.IConnector { return new MemoryConnector(name); }