@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
JavaScript
"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