UNPKG

@jsonjoy.com/reactive-rpc

Version:

Reactive-RPC is a library for building reactive APIs over WebSocket, HTTP, and other RPCs.

157 lines 6.74 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PersistentChannel = exports.WebSocketChannel = exports.ChannelState = void 0; const rxjs_1 = require("rxjs"); const toUint8Array_1 = require("@jsonjoy.com/util/lib/buffers/toUint8Array"); const operators_1 = require("rxjs/operators"); var ChannelState; (function (ChannelState) { ChannelState[ChannelState["CONNECTING"] = 0] = "CONNECTING"; ChannelState[ChannelState["OPEN"] = 1] = "OPEN"; ChannelState[ChannelState["CLOSED"] = 2] = "CLOSED"; })(ChannelState || (exports.ChannelState = ChannelState = {})); class WebSocketChannel { constructor({ newSocket }) { this.state$ = new rxjs_1.BehaviorSubject(ChannelState.CONNECTING); this.open$ = new rxjs_1.ReplaySubject(1); this.close$ = new rxjs_1.ReplaySubject(1); this.error$ = new rxjs_1.Subject(); this.message$ = new rxjs_1.Subject(); try { const ws = (this.ws = newSocket()); ws.binaryType = 'arraybuffer'; ws.onopen = () => { this.state$.next(ChannelState.OPEN); this.open$.next(this); this.open$.complete(); }; ws.onclose = (event) => { this.state$.next(ChannelState.CLOSED); this.close$.next([this, event]); this.close$.complete(); this.message$.complete(); }; ws.onerror = (event) => { const errorEvent = event; const error = errorEvent.error instanceof Error ? errorEvent.error : new Error(String(errorEvent.message) || 'ERROR'); this.error$.next(error); }; ws.onmessage = (event) => { const data = event.data; const message = (typeof data === 'string' ? data : (0, toUint8Array_1.toUint8Array)(data)); this.message$.next(message); }; } catch (error) { this.state$.next(ChannelState.CLOSED); this.error$.next(error); this.close$.next([this, { code: 0, wasClean: true, reason: 'CONSTRUCTOR' }]); this.close$.complete(); } } buffer() { if (!this.ws) return 0; return this.ws.bufferedAmount; } close(code, reason) { if (!this.ws) return; this.ws.close(code, reason); } isOpen() { return this.state$.getValue() === ChannelState.OPEN; } send(data) { if (!this.ws) return -1; const buffered = this.ws.bufferedAmount; this.ws.send(data); return this.ws.bufferedAmount - buffered; } send$(data) { return this.open$.pipe((0, operators_1.map)(() => { if (!this.isOpen()) throw new Error('CLOSED'); return this.send(data); })); } } exports.WebSocketChannel = WebSocketChannel; class PersistentChannel { constructor(params) { this.params = params; this.active$ = new rxjs_1.BehaviorSubject(false); this.channel$ = new rxjs_1.BehaviorSubject(undefined); this.open$ = new rxjs_1.BehaviorSubject(false); this.message$ = this.channel$.pipe((0, operators_1.filter)((channel) => !!channel), (0, operators_1.switchMap)((channel) => channel.message$)); this.retries = 0; const start$ = new rxjs_1.Subject(); const stop$ = new rxjs_1.Subject(); this.active$ .pipe((0, operators_1.skip)(1), (0, operators_1.filter)((active) => active)) .subscribe(() => { start$.next(undefined); }); this.active$ .pipe((0, operators_1.skip)(1), (0, operators_1.filter)((active) => !active)) .subscribe(() => { stop$.next(undefined); }); start$.subscribe(() => this.channel$.next(params.newChannel())); start$ .pipe((0, operators_1.switchMap)(() => this.channel$), (0, operators_1.filter)((channel) => !!channel), (0, operators_1.takeUntil)(stop$), (0, operators_1.switchMap)((channel) => channel.close$), (0, operators_1.takeUntil)(stop$), (0, operators_1.switchMap)(() => (0, rxjs_1.from)((async () => { const timeout = this.reconnectDelay(); this.retries++; await new Promise((resolve) => setTimeout(resolve, timeout)); })())), (0, operators_1.takeUntil)(stop$), (0, operators_1.tap)(() => this.channel$.next(params.newChannel())), (0, operators_1.delay)(params.minUptime || 5000), (0, operators_1.takeUntil)(stop$), (0, operators_1.tap)(() => { const isOpen = this.channel$.getValue()?.isOpen(); if (isOpen) { this.retries = 0; } })) .subscribe(); start$ .pipe((0, operators_1.switchMap)(() => this.channel$), (0, operators_1.filter)((channel) => !!channel), (0, operators_1.switchMap)((channel) => channel.state$), (0, operators_1.map)((state) => state === ChannelState.OPEN)) .subscribe((open) => { if (open !== this.open$.getValue()) this.open$.next(open); }); stop$.subscribe(() => { this.retries = 0; }); } start() { if (this.active$.getValue()) return; this.active$.next(true); } stop() { if (!this.active$.getValue()) return; this.active$.next(false); const channel = this.channel$.getValue(); if (channel) { channel.close(); this.channel$.next(undefined); } this.open$.next(false); } reconnectDelay() { if (this.retries <= 0) return 0; const minReconnectionDelay = this.params.minReconnectionDelay || Math.round(1000 + Math.random() * 1000); const maxReconnectionDelay = this.params.maxReconnectionDelay || 10000; const reconnectionDelayGrowFactor = this.params.reconnectionDelayGrowFactor || 1.3; const delay = Math.min(maxReconnectionDelay, minReconnectionDelay * reconnectionDelayGrowFactor ** (this.retries - 1)); return delay; } send$(data) { return this.channel$.pipe((0, operators_1.filter)((channel) => !!channel), (0, operators_1.switchMap)((channel) => channel.open$), (0, operators_1.filter)((channel) => channel.isOpen()), (0, operators_1.take)(1), (0, operators_1.map)((channel) => { const canSend = this.active$.getValue() && this.open$.getValue(); return canSend ? channel.send(data) : -1; })); } } exports.PersistentChannel = PersistentChannel; //# sourceMappingURL=channel.js.map