@river-build/sdk
Version:
For more details, visit the following resources:
156 lines • 6.99 kB
JavaScript
import debug from 'debug';
import { check, dlog, dlogError } from '@river-build/dlog';
import { hasElements, isDefined } from './check';
import { getMiniblocks } from './makeStreamRpcClient';
import { unpackStream } from './sign';
import { StreamStateView } from './streamStateView';
import { streamIdAsString, streamIdAsBytes, userIdFromAddress, makeUserStreamId } from './id';
const SCROLLBACK_MAX_COUNT = 20;
const SCROLLBACK_MULTIPLIER = 4n;
export class UnauthenticatedClient {
rpcClient;
logCall;
logEmitFromClient;
logError;
unpackEnvelopeOpts;
userId = 'unauthenticatedClientUser';
getScrollbackRequests = new Map();
constructor(rpcClient, logNamespaceFilter,
// this client is used for viewing public streams, disable signature and hash validation to make it as fast as possible
opts = {
disableSignatureValidation: true,
disableHashValidation: true,
}) {
if (logNamespaceFilter) {
debug.enable(logNamespaceFilter);
}
this.rpcClient = rpcClient;
const shortId = 'unauthClientShortId';
this.logCall = dlog('csb:cl:call').extend(shortId);
this.logEmitFromClient = dlog('csb:cl:emit').extend(shortId);
this.logError = dlogError('csb:cl:error').extend(shortId);
this.unpackEnvelopeOpts = opts;
this.logCall('new UnauthenticatedClient');
}
async userWithAddressExists(address) {
return this.userExists(userIdFromAddress(address));
}
async userExists(userId) {
const userStreamId = makeUserStreamId(userId);
return this.streamExists(userStreamId);
}
async streamExists(streamId) {
this.logCall('streamExists?', streamId);
const response = await this.rpcClient.getStream({
streamId: streamIdAsBytes(streamId),
optional: true,
});
this.logCall('streamExists=', streamId, response.stream);
return response.stream !== undefined;
}
async getStream(streamId) {
try {
this.logCall('getStream', streamId);
const response = await this.rpcClient.getStream({ streamId: streamIdAsBytes(streamId) });
this.logCall('getStream', response.stream);
check(isDefined(response.stream) && hasElements(response.stream.miniblocks), 'got bad stream');
const { streamAndCookie, snapshot, prevSnapshotMiniblockNum } = await unpackStream(response.stream, this.unpackEnvelopeOpts);
const streamView = new StreamStateView(this.userId, streamIdAsString(streamId));
streamView.initialize(streamAndCookie.nextSyncCookie, streamAndCookie.events, snapshot, streamAndCookie.miniblocks, [], prevSnapshotMiniblockNum, undefined, [], undefined);
return streamView;
}
catch (err) {
this.logCall('getStream', streamId, 'ERROR', err);
throw err;
}
}
/**
* @deprecated please use scrollbackByMs()
**/
async scrollbackToDate(streamView, toDate) {
return this.scrollbackByMs(streamView, toDate);
}
async scrollbackByMs(streamView, ms) {
this.logCall('scrollbackToDate', { streamId: streamView.streamId, ms });
const firstEvent = streamView.timeline.at(0);
// skip scrollback if limit is already reached
if (firstEvent?.createdAtEpochMs && !this.isWithin(firstEvent?.createdAtEpochMs, ms)) {
return;
}
// scrollback to get events till max scrollback, toDate or till no events are left
for (let i = 0; i < SCROLLBACK_MAX_COUNT; i++) {
const result = await this.scrollback(streamView);
if (result.terminus) {
break;
}
const currentOldestEvent = result.firstEvent;
this.logCall('scrollbackToDate result', {
oldest: currentOldestEvent?.createdAtEpochMs,
ms,
});
if (currentOldestEvent) {
if (!this.isWithin(currentOldestEvent.createdAtEpochMs, ms)) {
break;
}
}
}
}
async scrollback(streamView) {
const currentRequest = this.getScrollbackRequests.get(streamView.streamId);
if (currentRequest) {
return currentRequest;
}
const _scrollback = async () => {
check(isDefined(streamView.miniblockInfo), `stream not initialized: ${streamView.streamId}`);
if (streamView.miniblockInfo.terminusReached) {
this.logCall('scrollback', streamView.streamId, 'terminus reached');
return { terminus: true, firstEvent: streamView.timeline.at(0) };
}
check(streamView.miniblockInfo.min >= streamView.prevSnapshotMiniblockNum);
this.logCall('scrollback', {
streamId: streamView.streamId,
miniblockInfo: streamView.miniblockInfo,
prevSnapshotMiniblockNum: streamView.prevSnapshotMiniblockNum,
});
const toExclusive = streamView.miniblockInfo.min;
const fromInclusive = streamView.prevSnapshotMiniblockNum;
const span = toExclusive - fromInclusive;
let fromInclusiveNew = toExclusive - span * SCROLLBACK_MULTIPLIER;
fromInclusiveNew = fromInclusiveNew < 0n ? 0n : fromInclusiveNew;
const response = await this.getMiniblocks(streamView.streamId, fromInclusiveNew, toExclusive);
// a race may occur here: if the state view has been reinitialized during the scrollback
// request, we need to discard the new miniblocks.
if ((streamView.miniblockInfo?.min ?? -1n) === toExclusive) {
streamView.prependEvents(response.miniblocks, undefined, response.terminus, undefined, undefined);
return { terminus: response.terminus, firstEvent: streamView.timeline.at(0) };
}
return { terminus: false, firstEvent: streamView.timeline.at(0) };
};
try {
const request = _scrollback();
this.getScrollbackRequests.set(streamView.streamId, request);
return await request;
}
finally {
this.getScrollbackRequests.delete(streamView.streamId);
}
}
async getMiniblocks(streamId, fromInclusive, toExclusive) {
if (toExclusive === fromInclusive) {
return {
miniblocks: [],
terminus: toExclusive === 0n,
};
}
const { miniblocks, terminus } = await getMiniblocks(this.rpcClient, streamId, fromInclusive, toExclusive, this.unpackEnvelopeOpts);
return {
terminus: terminus,
miniblocks: miniblocks,
};
}
isWithin(number, time) {
const minEpochMs = Date.now() - time;
return number > minEpochMs;
}
}
//# sourceMappingURL=unauthenticatedClient.js.map