@river-build/sdk
Version:
For more details, visit the following resources:
229 lines • 9.54 kB
JavaScript
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