@nori-zk/mina-token-bridge
Version:
A Mina zk-program contract allowing users to mint tokens on Nori Bridge.
153 lines • 6.08 kB
JavaScript
import { webSocket, } from 'rxjs/webSocket';
import { Subject, BehaviorSubject, Observable, Subscription, timer, filter, } from 'rxjs';
/**
* A Subject wrapper over `WebSocketSubject` that adds automatic reconnection behavior.
*
* Reconnection attempts use exponential backoff with an upper delay bound.
* The class exposes:
* - an observable stream for connection state changes,
* - an outgoing message buffer to prevent message loss during reconnects,
* - and a subscription proxy for incoming messages.
*
* Once the maximum number of retries is exceeded (if provided), the connection transitions
* to `permanently-closed`, completing all observables and releasing resources.
*
* @template T Type of message payload sent/received over the socket.
*/
export class ReconnectingWebSocketSubject extends Subject {
constructor(config, webSocketConnectionStateSubject) {
super();
this.outgoingBuffer = new Subject();
this.incomingSubject = new Subject();
this.socketSub = new Subscription();
this.socket = null;
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.config = config;
this.webSocketConnectionStateSubject = webSocketConnectionStateSubject;
this.webSocketConnectionState$ =
webSocketConnectionStateSubject.asObservable();
// Drive reconnect on every 'closed'
this.webSocketConnectionState$
.pipe(filter((state) => state === 'closed'))
.subscribe(() => this._reconnect());
// kick off first connect
this._connect();
}
forceReconnect() {
if (this.webSocketConnectionStateSubject.value === 'open') {
this.webSocketConnectionStateSubject.next('closed');
}
}
_connect() {
this.webSocketConnectionStateSubject.next('connecting');
const { reconnect = {}, ...wsConfig } = this.config;
const fullConfig = {
...wsConfig,
openObserver: {
next: (evt) => {
this.reconnectAttempt = 0;
this.isReconnecting = false;
this.webSocketConnectionStateSubject.next('open');
wsConfig.openObserver?.next?.(evt);
},
},
closeObserver: {
next: (evt) => {
this.webSocketConnectionStateSubject.next('closed');
wsConfig.closeObserver?.next?.(evt);
},
},
};
this.socket = webSocket(fullConfig);
this.socketSub.unsubscribe();
this.socketSub = new Subscription();
this.socketSub.add(this.socket.subscribe({
next: (msg) => this.incomingSubject.next(msg),
error: (err) => {
this.webSocketConnectionStateSubject.next('closed');
},
complete: () => {
this.webSocketConnectionStateSubject.next('closed');
},
}));
this.socketSub.add(this.outgoingBuffer.subscribe((msg) => this.socket?.next(msg)));
}
_reconnect() {
const { reconnect = {} } = this.config;
const { initialDelayMs = 1000, maxDelayMs = 30000, maxRetries = Infinity, } = reconnect;
if (this.isReconnecting)
return;
this.isReconnecting = true;
if (this.reconnectAttempt >= maxRetries) {
this.webSocketConnectionStateSubject.next('permanently-closed');
this.webSocketConnectionStateSubject.complete();
this.outgoingBuffer.complete();
this.incomingSubject.complete();
this.socketSub.unsubscribe();
this.socket = null;
return;
}
// emit 'reconnecting' right away
this.webSocketConnectionStateSubject.next('reconnecting');
// clean up any running timer or socket
this.reconnectTimerSub?.unsubscribe();
this.reconnectAttempt++;
if (!this.socketSub.closed)
this.socketSub.unsubscribe();
const delay = Math.min(initialDelayMs * 2 ** (this.reconnectAttempt - 1), maxDelayMs);
this.reconnectTimerSub = timer(delay).subscribe(() => {
this.isReconnecting = false;
this._connect();
});
}
next(value) {
this.outgoingBuffer.next(value);
}
error(err) {
this.socket?.error?.(err);
}
complete() {
this.socket?.complete?.();
}
subscribe(...args) {
return this.incomingSubject.subscribe(...args);
}
multiplex(subMsg, unsubMsg, messageFilter) {
return new Observable((observer) => {
const inner = this.subscribe({
next: (msg) => {
if (messageFilter(msg))
observer.next(msg);
},
error: (e) => observer.error(e),
complete: () => observer.complete(),
});
this.next(subMsg());
return () => {
this.next(unsubMsg());
inner.unsubscribe();
};
});
}
}
/**
* Factory for creating a reconnecting WebSocket and its associated connection state stream.
*
* The returned socket behaves like a regular `WebSocketSubject` but includes
* automatic reconnection with exponential backoff and a persistent state observable.
*
* @param config Configuration for WebSocketSubject and reconnection strategy.
* @returns Object containing:
* - `webSocket$`: the ReconnectingWebSocketSubject instance.
* - `webSocketConnectionState$`: observable emitting connection state changes.
*/
export function reconnectingWebSocket(config) {
const webSocketConnectionStateSubject = new BehaviorSubject('connecting');
const webSocketConnectionState$ = webSocketConnectionStateSubject.asObservable();
return {
webSocket$: new ReconnectingWebSocketSubject(config, webSocketConnectionStateSubject),
webSocketConnectionState$,
};
}
//# sourceMappingURL=reconnectingSocket.js.map