UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

229 lines 9.54 kB
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; import { makeStreamRpcClient } from '../../makeStreamRpcClient'; import { RiverChain } from './models/riverChain'; import { LoadPriority } from '../../store/store'; import { check, dlogger, shortenHexString } from '@river-build/dlog'; import { PromiseQueue } from '../utils/promiseQueue'; import { PersistedObservable, persistedObservable } from '../../observable/persistedObservable'; import { userIdFromAddress } from '../../id'; import { TransactionalClient } from './models/transactionalClient'; import { Observable } from '../../observable/observable'; import { AuthStatus } from './models/authStatus'; import { expiryInterceptor } from '../../rpcInterceptors'; import { isDefined } from '../../check'; class LoginContext { cancelled; constructor(cancelled = false) { this.cancelled = cancelled; } } let RiverConnection = class RiverConnection extends PersistedObservable { spaceDapp; riverRegistryDapp; clientParams; client; riverChain; authStatus = new Observable(AuthStatus.Initializing); loginError; logger; clientQueue = new PromiseQueue(); views = []; onStoppedFns = []; newUserMetadata; loginPromise; constructor(store, spaceDapp, riverRegistryDapp, clientParams) { super({ id: '0', userExists: false }, store, LoadPriority.high); this.spaceDapp = spaceDapp; this.riverRegistryDapp = riverRegistryDapp; this.clientParams = clientParams; const logId = this.clientParams.opts?.logId ?? shortenHexString(this.userId); this.logger = dlogger(`csb:rconn:${logId}`); this.riverChain = new RiverChain(store, riverRegistryDapp, this.userId, logId); } onLoaded() { // } get userId() { return userIdFromAddress(this.clientParams.signerContext.creatorAddress); } async start() { check(this.value.status === 'loaded', 'riverConnection not loaded'); const [urls, userStreamExists] = await Promise.all([ this.riverChain.urls(), this.riverChain.userStreamExists(), ]); if (!urls) { throw new Error('riverConnection::start urls is not set'); } await this.createStreamsClient(); if (userStreamExists) { await this.login(); } else { this.authStatus.setValue(AuthStatus.Credentialed); } } async stop() { for (const fn of this.onStoppedFns) { fn(); } this.onStoppedFns = []; if (this.loginPromise) { this.loginPromise.context.cancelled = true; } this.riverChain.stop(); await this.client?.stop(); this.client = undefined; this.authStatus.setValue(AuthStatus.Disconnected); } call(fn) { if (this.client) { return fn(this.client); } else { // Enqueue the request if client is not available return this.clientQueue.enqueue(fn); } } withStream(streamId) { return { call: (fn) => { return this.call(async (client) => { const stream = await client.waitForStream(streamId); return fn(client, stream); }); }, }; } callWithStream(streamId, fn) { return this.withStream(streamId).call(fn); } registerView(viewFn) { if (this.client) { const onStopFn = viewFn(this.client); this.onStoppedFns.push(onStopFn); } this.views.push(viewFn); } async createStreamsClient() { const urls = await this.riverChain.urls(); if (this.client !== undefined) { // this is wired up to be reactive to changes in the urls this.logger.log('RiverConnection: rpc urls changed, client already set', urls); return; } if (!urls) { this.logger.error('RiverConnection: urls is not set'); return; } this.logger.info(`setting rpcClient with urls: "${urls}"`); const rpcClient = makeStreamRpcClient(urls, () => this.riverRegistryDapp.getOperationalNodeUrls(), { retryParams: this.clientParams.rpcRetryParams, interceptors: [ expiryInterceptor({ onTokenExpired: this.clientParams.onTokenExpired, }), ], }); const client = new TransactionalClient(this.store, this.clientParams.signerContext, rpcClient, this.clientParams.cryptoStore, this.clientParams.entitlementsDelegate, this.clientParams.opts); client.setMaxListeners(100); this.client = client; // initialize views this.store.withTransaction('RiverConnection::onNewClient', () => { this.views.forEach((viewFn) => { const onStopFn = viewFn(client); this.onStoppedFns.push(onStopFn); }); }); } async login(newUserMetadata) { this.newUserMetadata = newUserMetadata ?? this.newUserMetadata; this.logger.log('login', { newUserMetadata }); await this.loginWithRetries(); } async loginWithRetries() { check(isDefined(this.client), 'riverConnection::loginWithRetries client is not defined'); this.logger.info('login', { authStatus: this.authStatus.value, promise: this.loginPromise }); if (this.loginPromise) { this.loginPromise.context.cancelled = true; await this.loginPromise.promise; } if (this.authStatus.value === AuthStatus.ConnectedToRiver) { return; } const loginContext = new LoginContext(); this.authStatus.setValue(AuthStatus.EvaluatingCredentials); const login = async () => { let retryCount = 0; const MAX_RETRY_COUNT = 20; while (!loginContext.cancelled) { check(isDefined(this.client), 'riverConnection::loginWithRetries client is not defined'); try { this.logger.info('logging in', { userExists: this.data.userExists, newUserMetadata: this.newUserMetadata, }); this.authStatus.setValue(AuthStatus.ConnectingToRiver); const client = this.client; await client.initializeUser({ spaceId: this.newUserMetadata?.spaceId, encryptionDeviceInit: this.clientParams.encryptionDevice, }); this.logger.info('user initialized'); client.startSync(); this.setData({ userExists: true }); this.authStatus.setValue(AuthStatus.ConnectedToRiver); // New rpcClient is available, resolve all queued requests this.clientQueue.flush(client); this.loginPromise = undefined; break; } catch (err) { retryCount++; this.loginError = err; this.logger.error(`encountered exception while initializing ${this.userId}`, err); for (const fn of this.onStoppedFns) { fn(); } this.onStoppedFns = []; await this.client.stop(); this.client = undefined; await this.createStreamsClient(); if (loginContext.cancelled) { this.logger.info('login cancelled after error'); this.loginPromise = undefined; break; } else if (retryCount >= MAX_RETRY_COUNT) { this.logger.info('login MAX_RETRY_COUNT reached'); this.authStatus.setValue(AuthStatus.Error); this.loginPromise = undefined; throw err; } else { const retryDelay = getRetryDelay(retryCount); this.logger.info('retrying', { retryDelay, retryCount }); // sleep await new Promise((resolve) => setTimeout(resolve, retryDelay)); } } } }; this.loginPromise = { promise: login(), context: loginContext }; return this.loginPromise.promise; } }; RiverConnection = __decorate([ persistedObservable({ tableName: 'riverConnection' }) ], RiverConnection); export { RiverConnection }; // exponentially back off, but never wait more than 20 seconds function getRetryDelay(retryCount) { return Math.min(1000 * 2 ** retryCount, 20000); } //# sourceMappingURL=riverConnection.js.map