UNPKG

walletlink-e

Version:
260 lines (259 loc) 10 kB
"use strict"; // Copyright (c) 2018-2020 WalletLink.org <https://www.walletlink.org/> // Copyright (c) 2018-2020 Coinbase, Inc. <https://www.coinbase.com/> // Licensed under the Apache License, version 2.0 Object.defineProperty(exports, "__esModule", { value: true }); exports.WalletLinkConnection = void 0; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const types_1 = require("../types"); const ClientMessage_1 = require("./ClientMessage"); const RxWebSocket_1 = require("./RxWebSocket"); const ServerMessage_1 = require("./ServerMessage"); const HEARTBEAT_INTERVAL = 10000; const REQUEST_TIMEOUT = 60000; /** * WalletLink Connection */ class WalletLinkConnection { /** * Constructor * @param sessionId Session ID * @param sessionKey Session Key * @param serverUrl Walletlinkd RPC URL * @param [WebSocketClass] Custom WebSocket implementation */ constructor(sessionId, sessionKey, serverUrl, WebSocketClass = WebSocket) { this.sessionId = sessionId; this.sessionKey = sessionKey; this.subscriptions = new rxjs_1.Subscription(); this.destroyed = false; this.lastHeartbeatResponse = 0; this.nextReqId = types_1.IntNumber(1); this.connectedSubject = new rxjs_1.BehaviorSubject(false); this.linkedSubject = new rxjs_1.BehaviorSubject(false); this.sessionConfigSubject = new rxjs_1.ReplaySubject(1); const ws = new RxWebSocket_1.RxWebSocket(serverUrl + "/rpc", WebSocketClass); this.ws = ws; // attempt to reconnect every 5 seconds when disconnected this.subscriptions.add(ws.connectionState$ .pipe( // ignore initial DISCONNECTED state operators_1.skip(1), // if DISCONNECTED and not destroyed operators_1.filter(cs => cs === RxWebSocket_1.ConnectionState.DISCONNECTED && !this.destroyed), // wait 5 seconds operators_1.delay(5000), // check whether it's destroyed again operators_1.filter(_ => !this.destroyed), // reconnect operators_1.flatMap(_ => ws.connect()), operators_1.retry()) .subscribe()); // perform authentication upon connection this.subscriptions.add(ws.connectionState$ .pipe( // ignore initial DISCONNECTED and CONNECTING states operators_1.skip(2), operators_1.switchMap(cs => rxjs_1.iif(() => cs === RxWebSocket_1.ConnectionState.CONNECTED, // if CONNECTED, authenticate, and then check link status this.authenticate().pipe(operators_1.tap(_ => this.sendIsLinked()), operators_1.tap(_ => this.sendGetSessionConfig()), operators_1.map(_ => true)), // if not CONNECTED, emit false immediately rxjs_1.of(false))), operators_1.distinctUntilChanged(), operators_1.catchError(_ => rxjs_1.of(false))) .subscribe(connected => this.connectedSubject.next(connected))); // send heartbeat every n seconds while connected this.subscriptions.add(ws.connectionState$ .pipe( // ignore initial DISCONNECTED state operators_1.skip(1), operators_1.switchMap(cs => rxjs_1.iif(() => cs === RxWebSocket_1.ConnectionState.CONNECTED, // if CONNECTED, start the heartbeat timer rxjs_1.timer(0, HEARTBEAT_INTERVAL)))) .subscribe(i => // first timer event updates lastHeartbeat timestamp // subsequent calls send heartbeat message i === 0 ? this.updateLastHeartbeat() : this.heartbeat())); // handle server's heartbeat responses this.subscriptions.add(ws.incomingData$ .pipe(operators_1.filter(m => m === "h")) .subscribe(_ => this.updateLastHeartbeat())); // handle link status updates this.subscriptions.add(ws.incomingJSONData$ .pipe(operators_1.filter(m => ["IsLinkedOK", "Linked"].includes(m.type))) .subscribe(m => { const msg = m; this.linkedSubject.next(msg.linked || msg.onlineGuests > 0); })); // handle session config updates this.subscriptions.add(ws.incomingJSONData$ .pipe(operators_1.filter(m => ["GetSessionConfigOK", "SessionConfigUpdated"].includes(m.type))) .subscribe(m => { const msg = m; this.sessionConfigSubject.next({ webhookId: msg.webhookId, webhookUrl: msg.webhookUrl, metadata: msg.metadata }); })); } /** * Make a connection to the server */ connect() { if (this.destroyed) { throw new Error("instance is destroyed"); } this.ws.connect().subscribe(); } /** * Terminate connection, and mark as destroyed. To reconnect, create a new * instance of WalletLinkConnection */ destroy() { this.subscriptions.unsubscribe(); this.ws.disconnect(); this.destroyed = true; } /** * Emit true if connected and authenticated, else false * @returns an Observable */ get connected$() { return this.connectedSubject.asObservable(); } /** * Emit once connected * @returns an Observable */ get onceConnected$() { return this.connected$.pipe(operators_1.filter(v => v), operators_1.take(1), operators_1.map(() => void 0)); } /** * Emit true if linked (a guest has joined before) * @returns an Observable */ get linked$() { return this.linkedSubject.asObservable(); } /** * Emit once when linked * @returns an Observable */ get onceLinked$() { return this.linked$.pipe(operators_1.filter(v => v), operators_1.take(1), operators_1.map(() => void 0)); } /** * Emit current session config if available, and subsequent updates * @returns an Observable for the session config */ get sessionConfig$() { return this.sessionConfigSubject.asObservable(); } /** * Emit incoming Event messages * @returns an Observable for the messages */ get incomingEvent$() { return this.ws.incomingJSONData$.pipe(operators_1.filter(m => { if (m.type !== "Event") { return false; } const sme = m; return (typeof sme.sessionId === "string" && typeof sme.eventId === "string" && typeof sme.event === "string" && typeof sme.data === "string"); }), operators_1.map(m => m)); } /** * Set session metadata in SessionConfig object * @param key * @param value * @returns an Observable that completes when successful */ setSessionMetadata(key, value) { const message = ClientMessage_1.ClientMessageSetSessionConfig({ id: types_1.IntNumber(this.nextReqId++), sessionId: this.sessionId, metadata: { [key]: value } }); return this.onceConnected$.pipe(operators_1.flatMap(_ => this.makeRequest(message)), operators_1.map(res => { if (ServerMessage_1.isServerMessageFail(res)) { throw new Error(res.error || "failed to set session metadata"); } })); } /** * Publish an event and emit event ID when successful * @param event event name * @param data event data * @param callWebhook whether the webhook should be invoked * @returns an Observable that emits event ID when successful */ publishEvent(event, data, callWebhook = false) { const message = ClientMessage_1.ClientMessagePublishEvent({ id: types_1.IntNumber(this.nextReqId++), sessionId: this.sessionId, event, data, callWebhook }); return this.onceLinked$.pipe(operators_1.flatMap(_ => this.makeRequest(message)), operators_1.map(res => { if (ServerMessage_1.isServerMessageFail(res)) { throw new Error(res.error || "failed to publish event"); } return res.eventId; })); } sendData(message) { this.ws.sendData(JSON.stringify(message)); } updateLastHeartbeat() { this.lastHeartbeatResponse = Date.now(); } heartbeat() { if (Date.now() - this.lastHeartbeatResponse > HEARTBEAT_INTERVAL * 2) { this.ws.disconnect(); return; } try { this.ws.sendData("h"); } catch (_a) { } } makeRequest(message, timeout = REQUEST_TIMEOUT) { const reqId = message.id; try { this.sendData(message); } catch (err) { return rxjs_1.throwError(err); } // await server message with corresponding id return this.ws.incomingJSONData$.pipe(operators_1.timeoutWith(timeout, rxjs_1.throwError(new Error(`request ${reqId} timed out`))), operators_1.filter(m => m.id === reqId), operators_1.take(1)); } authenticate() { const msg = ClientMessage_1.ClientMessageHostSession({ id: types_1.IntNumber(this.nextReqId++), sessionId: this.sessionId, sessionKey: this.sessionKey }); return this.makeRequest(msg).pipe(operators_1.map(res => { if (ServerMessage_1.isServerMessageFail(res)) { throw new Error(res.error || "failed to authentcate"); } })); } sendIsLinked() { const msg = ClientMessage_1.ClientMessageIsLinked({ id: types_1.IntNumber(this.nextReqId++), sessionId: this.sessionId }); this.sendData(msg); } sendGetSessionConfig() { const msg = ClientMessage_1.ClientMessageGetSessionConfig({ id: types_1.IntNumber(this.nextReqId++), sessionId: this.sessionId }); this.sendData(msg); } } exports.WalletLinkConnection = WalletLinkConnection;