@river-build/sdk
Version:
For more details, visit the following resources:
779 lines • 35 kB
JavaScript
import { SyncOp } from '@river-build/proto';
import { dlog, dlogError } from '@river-build/dlog';
import { unpackStream, unpackStreamAndCookie } from './sign';
import { nanoid } from 'nanoid';
import { isMobileSafari } from './utils';
import { spaceIdFromChannelId, isDMChannelStreamId, isGDMChannelStreamId, isSpaceStreamId, isUserDeviceStreamId, isUserInboxStreamId, isUserSettingsStreamId, isUserStreamId, streamIdAsBytes, streamIdAsString, isChannelStreamId, } from './id';
import { isDefined, logNever } from './check';
export var SyncState;
(function (SyncState) {
SyncState["Canceling"] = "Canceling";
SyncState["NotSyncing"] = "NotSyncing";
SyncState["Retrying"] = "Retrying";
SyncState["Starting"] = "Starting";
SyncState["Syncing"] = "Syncing";
})(SyncState || (SyncState = {}));
/**
* Valid state transitions:
* - [*] -\> NotSyncing
* - NotSyncing -\> Starting
* - Starting -\> Syncing
* - Starting -\> Canceling: failed / stop sync
* - Starting -\> Retrying: connection error
* - Syncing -\> Canceling: connection aborted / stop sync
* - Syncing -\> Retrying: connection error
* - Syncing -\> Syncing: resync
* - Retrying -\> Canceling: stop sync
* - Retrying -\> Syncing: resume
* - Retrying -\> Retrying: still retrying
* - Canceling -\> NotSyncing
* @see https://www.notion.so/herenottherelabs/RFC-Sync-hardening-e0552a4ed68a4d07b42ae34c69ee1bec?pvs=4#861081756f86423ea668c62b9eb76f4b
*/
export const stateConstraints = {
[SyncState.NotSyncing]: new Set([SyncState.Starting]),
[SyncState.Starting]: new Set([SyncState.Syncing, SyncState.Retrying, SyncState.Canceling]),
[SyncState.Syncing]: new Set([SyncState.Canceling, SyncState.Retrying]),
[SyncState.Retrying]: new Set([
SyncState.Starting,
SyncState.Canceling,
SyncState.Syncing,
SyncState.Retrying,
]),
[SyncState.Canceling]: new Set([SyncState.NotSyncing]),
};
export class SyncedStreamsLoop {
unpackEnvelopeOpts;
highPriorityIds;
streamOpts;
// mapping of stream id to stream
streams;
// loggers
logSync;
logDebug;
logError;
// clientEmitter is used to proxy the events from the streams to the client
clientEmitter;
// Starting the client creates the syncLoop
// While a syncLoop exists, the client tried to keep the syncLoop connected, and if it reconnects, it
// will restart sync for all Streams
// on stop, the syncLoop will be cancelled if it is runnign and removed once it stops
syncLoop;
// syncId is used to add and remove streams from the sync subscription
// The syncId is only set once a connection is established
// On retry, it is cleared
// After being cancelled, it is cleared
syncId;
// rpcClient is used to receive sync updates from the server
rpcClient;
// syncState is used to track the current sync state
_syncState = SyncState.NotSyncing;
// retry logic
releaseRetryWait;
currentRetryCount = 0;
forceStopSyncStreams;
interruptSync;
isMobileSafariBackgrounded = false;
// Only responses related to the current syncId are processed.
// Responses are queued and processed in order
// and are cleared when sync stops
responsesQueue = [];
inProgressTick;
pendingSyncCookies = [];
inFlightSyncCookies = new Set();
pendingStreamsToDelete = [];
lastLogInflightAt = 0;
syncStartedAt = undefined;
MAX_IN_FLIGHT_COOKIES = 40;
MIN_IN_FLIGHT_COOKIES = 10;
MAX_IN_FLIGHT_STREAMS_TO_DELETE = 40;
pingInfo = {
currentSequence: 0,
nonces: {},
};
constructor(clientEmitter, rpcClient, streams, logNamespace, unpackEnvelopeOpts, highPriorityIds, streamOpts) {
this.unpackEnvelopeOpts = unpackEnvelopeOpts;
this.highPriorityIds = highPriorityIds;
this.streamOpts = streamOpts;
this.rpcClient = rpcClient;
this.clientEmitter = clientEmitter;
this.streams = new Map(streams.map(({ syncCookie, stream }) => [
streamIdAsString(syncCookie.streamId),
{ syncCookie, stream },
]));
this.logDebug = dlog('csb:cl:sync:debug').extend(logNamespace);
this.logSync = dlog('csb:cl:sync', { defaultEnabled: true }).extend(logNamespace);
this.logError = dlogError('csb:cl:sync:stream').extend(logNamespace);
}
get syncState() {
return this._syncState;
}
stats() {
return {
syncState: this.syncState,
streams: this.streams.size,
syncId: this.syncId,
queuedResponses: this.responsesQueue.length,
};
}
getSyncId() {
return this.syncId;
}
start() {
if (isMobileSafari()) {
document.addEventListener('visibilitychange', this.onMobileSafariBackgrounded);
}
this.createSyncLoop();
}
async stop() {
this.log('sync STOP CALLED');
this.responsesQueue = [];
if (stateConstraints[this.syncState].has(SyncState.Canceling)) {
const syncId = this.syncId;
const syncLoop = this.syncLoop;
const syncState = this.syncState;
this.setSyncState(SyncState.Canceling);
this.stopPing();
try {
this.releaseRetryWait?.();
// Give the server 5 seconds to respond to the cancelSync RPC before forceStopSyncStreams
const breakTimeout = syncId
? setTimeout(() => {
this.log('calling forceStopSyncStreams', syncId);
this.forceStopSyncStreams?.();
}, 5000)
: undefined;
this.log('stopSync syncState', syncState);
this.log('stopSync syncLoop', syncLoop);
this.log('stopSync syncId', syncId);
const result = await Promise.allSettled([
syncId ? await this.rpcClient.cancelSync({ syncId }) : undefined,
syncLoop,
]);
this.log('syncLoop awaited', syncId, result);
clearTimeout(breakTimeout);
}
catch (e) {
this.log('sync STOP ERROR', e);
}
this.log('sync STOP DONE', syncId);
}
else {
this.log(`WARN: stopSync called from invalid state ${this.syncState}`);
}
if (isMobileSafari()) {
document.removeEventListener('visibilitychange', this.onMobileSafariBackgrounded);
}
}
// adds stream to the sync subscription
addStreamToSync(streamId, syncCookie, stream) {
this.logDebug('addStreamToSync', streamId);
if (this.streams.has(streamId)) {
this.log('stream already in sync', streamId);
return;
}
// check if pending delete
const pendingIndex = this.pendingStreamsToDelete.indexOf(streamId);
if (pendingIndex !== -1) {
this.pendingStreamsToDelete.splice(pendingIndex, 1);
this.log('removed stream from pending deletion list', streamId);
}
// add to streams, enqueue for add
this.streams.set(streamId, { syncCookie, stream });
this.pendingSyncCookies.push(streamId);
this.checkStartTicking();
}
// remove stream from the sync subsbscription
async removeStreamFromSync(inStreamId) {
const streamId = streamIdAsString(inStreamId);
const streamRecord = this.streams.get(streamId);
if (!streamRecord) {
this.log('removeStreamFromSync streamId not found', streamId);
// no such stream
return;
}
const pendingIndex = this.pendingSyncCookies.indexOf(streamId);
if (pendingIndex !== -1) {
this.pendingSyncCookies.splice(pendingIndex, 1);
streamRecord.stream.stop();
this.streams.delete(streamId);
this.log('removed stream from pending sync', streamId);
return;
}
if (this.pendingStreamsToDelete.includes(streamId)) {
this.log('stream already in pending delete', streamId);
return;
}
if (this.syncState === SyncState.Starting || this.syncState === SyncState.Retrying) {
await this.waitForSyncingState();
}
if (this.syncState === SyncState.Syncing) {
this.pendingStreamsToDelete.push(streamId);
streamRecord.stream.stop();
this.streams.delete(streamId);
this.log('removed stream from sync', streamId);
this.clientEmitter.emit('streamRemovedFromSync', streamId);
}
else {
this.log('removeStreamFromSync: not in "syncing" state; let main sync loop handle this with its streams map', { streamId, syncState: this.syncState });
}
this.inFlightSyncCookies.delete(streamId);
}
setHighPriorityStreams(streamIds) {
this.highPriorityIds = new Set(streamIds);
}
onNetworkStatusChanged(isOnline) {
if (isOnline) {
this.log('back online, release retry wait', { syncState: this.syncState });
this.releaseRetryWait?.();
}
}
createSyncLoop() {
if (stateConstraints[this.syncState].has(SyncState.Starting)) {
this.setSyncState(SyncState.Starting);
this.log('starting sync loop');
}
else {
this.log('runSyncLoop: invalid state transition', this.syncState, '->', SyncState.Starting);
throw new Error('invalid state transition');
}
if (this.syncLoop) {
throw new Error('createSyncLoop called while a loop exists');
}
this.syncLoop = (async () => {
let iteration = 0;
this.log('sync loop created');
try {
while (this.syncState === SyncState.Starting ||
this.syncState === SyncState.Syncing ||
this.syncState === SyncState.Retrying) {
// get cookies from all the known streams to sync
this.inFlightSyncCookies.clear();
this.pendingSyncCookies = [];
this.pendingStreamsToDelete = [];
const syncCookies = [];
if (this.streamOpts?.useModifySync == true) {
this.pendingSyncCookies.push(...Array.from(this.streams.keys()));
}
else {
syncCookies.push(...Array.from(this.streams.entries())
.sort((a, b) => {
const aPriority = priorityFromStreamId(a[0], this.highPriorityIds);
const bPriority = priorityFromStreamId(b[0], this.highPriorityIds);
return aPriority - bPriority;
})
.map((streamRecord) => {
this.inFlightSyncCookies.add(streamRecord[0]);
return streamRecord[1].syncCookie;
}));
}
this.syncStartedAt = performance.now();
this.log('sync ITERATION start', ++iteration, this.syncState, `pending: ${this.pendingSyncCookies.length}`, `pendingDelete: ${this.pendingStreamsToDelete.length}`);
if (this.syncState === SyncState.Retrying) {
this.setSyncState(SyncState.Starting);
}
try {
// syncId needs to be reset before starting a new syncStreams
// syncStreams() should return a new syncId
this.syncId = undefined;
const streams = this.rpcClient.syncStreams({
syncPos: syncCookies,
}, { timeoutMs: -1 });
const iterator = streams[Symbol.asyncIterator]();
while (this.syncState === SyncState.Syncing ||
this.syncState === SyncState.Starting) {
const interruptSyncPromise = new Promise((resolve, reject) => {
this.forceStopSyncStreams = () => {
this.log('forceStopSyncStreams called');
resolve();
};
this.interruptSync = (e) => {
this.logError('sync interrupted', e);
reject(e);
};
});
const { value, done } = await Promise.race([
iterator.next(),
interruptSyncPromise.then(() => ({
value: undefined,
done: true,
})),
]);
if (done || value === undefined) {
this.log('exiting syncStreams', done, value);
// exit the syncLoop, it's done
this.forceStopSyncStreams = undefined;
this.interruptSync = undefined;
return iteration;
}
this.logDebug('got syncStreams response', 'syncOp', value.syncOp, 'syncId', value.syncId);
if (!value.syncId || !value.syncOp) {
this.log('missing syncId or syncOp', value);
continue;
}
let pingStats;
switch (value.syncOp) {
case SyncOp.SYNC_NEW:
this.syncStarted(value.syncId);
break;
case SyncOp.SYNC_CLOSE:
this.syncClosed();
break;
case SyncOp.SYNC_UPDATE:
this.responsesQueue.push(value);
this.checkStartTicking();
break;
case SyncOp.SYNC_PONG:
pingStats = this.pingInfo.nonces[value.pongNonce];
if (pingStats) {
pingStats.receivedAt = performance.now();
pingStats.duration = pingStats.receivedAt - pingStats.pingAt;
}
else {
this.logError('pong nonce not found', value.pongNonce);
this.printNonces();
}
break;
case SyncOp.SYNC_DOWN:
this.syncDown(value.streamId);
break;
default:
logNever(value.syncOp, `unknown syncOp { syncId: ${this.syncId}, syncOp: ${value.syncOp} }`);
break;
}
}
}
catch (err) {
this.logError('syncLoop error', err);
await this.attemptRetry();
}
}
}
finally {
this.log('sync loop stopping ITERATION', {
iteration,
syncState: this.syncState,
});
this.stopPing();
if (stateConstraints[this.syncState].has(SyncState.NotSyncing)) {
this.setSyncState(SyncState.NotSyncing);
this.streams.forEach((streamRecord) => {
streamRecord.stream.stop();
});
this.streams.clear();
this.releaseRetryWait = undefined;
this.syncId = undefined;
this.clientEmitter.emit('streamSyncActive', false);
}
else {
this.log('onStopped: invalid state transition', this.syncState, '->', SyncState.NotSyncing);
}
this.log('sync loop stopped ITERATION', iteration);
}
return iteration;
})();
}
onMobileSafariBackgrounded = () => {
this.isMobileSafariBackgrounded = document.visibilityState === 'hidden';
this.log('onMobileSafariBackgrounded', this.isMobileSafariBackgrounded);
if (!this.isMobileSafariBackgrounded) {
// if foregrounded, attempt to retry
this.log('foregrounded, release retry wait', { syncState: this.syncState });
this.releaseRetryWait?.();
this.checkStartTicking();
}
};
checkStartTicking() {
if (this.inProgressTick) {
return;
}
if (this.responsesQueue.length === 0 &&
this.pendingSyncCookies.length === 0 &&
this.pendingStreamsToDelete.length === 0) {
return;
}
if (this.isMobileSafariBackgrounded) {
return;
}
const tick = this.tick();
this.inProgressTick = tick;
queueMicrotask(() => {
tick.catch((e) => this.logError('ProcessTick Error', e)).finally(() => {
this.inProgressTick = undefined;
setTimeout(() => this.checkStartTicking());
});
});
}
async tick() {
if (this.syncState === SyncState.Syncing) {
if ((this.inFlightSyncCookies.size <= this.MIN_IN_FLIGHT_COOKIES &&
this.pendingSyncCookies.length > 0) ||
this.pendingStreamsToDelete.length > 0) {
const syncId = this.syncId;
this.pendingSyncCookies.sort((a, b) => {
const aPriority = priorityFromStreamId(a, this.highPriorityIds);
const bPriority = priorityFromStreamId(b, this.highPriorityIds);
return aPriority - bPriority;
});
const streamsToAdd = this.pendingSyncCookies.splice(0, this.MAX_IN_FLIGHT_COOKIES);
const streamsToDelete = this.pendingStreamsToDelete.splice(0, this.MAX_IN_FLIGHT_STREAMS_TO_DELETE);
this.logSync('tick: modifySync', {
syncId,
addStreams: streamsToAdd,
deleteStreams: streamsToDelete,
inFlight: this.inFlightSyncCookies.size,
});
streamsToAdd.forEach((x) => this.inFlightSyncCookies.add(x));
const syncPos = streamsToAdd.map((x) => this.streams.get(x)?.syncCookie);
try {
const resp = await this.rpcClient.modifySync({
syncId,
addStreams: syncPos.filter(isDefined),
removeStreams: streamsToDelete.map(streamIdAsBytes),
});
if (resp.removals.length > 0) {
this.logError('modifySync removal errors', resp.removals);
}
if (resp.adds.length > 0) {
this.logError('modifySync addition errors', resp.adds);
}
}
catch (err) {
this.logError('modifySync error', err);
if (this.syncId === syncId && this.syncState === SyncState.Syncing) {
streamsToAdd.forEach((x) => {
if (this.inFlightSyncCookies.delete(x)) {
this.pendingSyncCookies.push(x);
}
});
this.pendingStreamsToDelete.push(...streamsToDelete);
this.checkStartTicking();
}
}
}
}
const item = this.responsesQueue.shift();
if (!item || item.syncId !== this.syncId) {
return;
}
await this.onUpdate(item);
}
async waitForSyncingState() {
// if we can transition to syncing, wait for it
if (stateConstraints[this.syncState].has(SyncState.Syncing)) {
this.log('waitForSyncing', this.syncState);
// listen for streamSyncStateChange event from client emitter
return new Promise((resolve) => {
const onStreamSyncStateChange = (syncState) => {
if (!stateConstraints[this.syncState].has(SyncState.Syncing)) {
this.log('waitForSyncing complete', syncState);
this.clientEmitter.off('streamSyncStateChange', onStreamSyncStateChange);
resolve();
}
else {
this.log('waitForSyncing continues', syncState);
}
};
this.clientEmitter.on('streamSyncStateChange', onStreamSyncStateChange);
});
}
}
setSyncState(newState) {
if (this._syncState === newState) {
throw new Error('setSyncState called for the existing state');
}
if (!stateConstraints[this._syncState].has(newState)) {
throw this.logInvalidStateAndReturnError(this._syncState, newState);
}
this.log('syncState', this._syncState, '->', newState);
this._syncState = newState;
this.clientEmitter.emit('streamSyncStateChange', newState);
}
// The sync loop will keep retrying until it is shutdown, it has no max attempts
async attemptRetry() {
this.log(`attemptRetry`, this.syncState);
this.stopPing();
if (stateConstraints[this.syncState].has(SyncState.Retrying)) {
if (this.syncState !== SyncState.Retrying) {
this.setSyncState(SyncState.Retrying);
this.syncId = undefined;
this.streams.forEach((streamRecord) => {
streamRecord.stream.resetUpToDate();
});
this.inFlightSyncCookies.clear();
this.pendingSyncCookies = [];
this.pendingStreamsToDelete = [];
this.clientEmitter.emit('streamSyncActive', false);
}
// currentRetryCount will increment until MAX_RETRY_COUNT. Then it will stay
// fixed at this value
// 7 retries = 2^7 = 128 seconds (~2 mins)
const MAX_RETRY_DELAY_FACTOR = 7;
const nextRetryCount = this.currentRetryCount >= MAX_RETRY_DELAY_FACTOR
? MAX_RETRY_DELAY_FACTOR
: this.currentRetryCount + 1;
const retryDelay = 2 ** nextRetryCount * 1000; // 2^n seconds
this.log('sync error, retrying in', retryDelay, 'ms', ', { currentRetryCount:', this.currentRetryCount, ', nextRetryCount:', nextRetryCount, ', MAX_RETRY_COUNT:', MAX_RETRY_DELAY_FACTOR, '}');
this.currentRetryCount = nextRetryCount;
await new Promise((resolve) => {
const timeout = setTimeout(() => {
this.releaseRetryWait = undefined;
resolve();
}, retryDelay);
this.releaseRetryWait = () => {
clearTimeout(timeout);
this.releaseRetryWait = undefined;
resolve();
this.log('retry released');
};
});
}
else {
this.logError('attemptRetry: invalid state transition', this.syncState);
// throw new Error('attemptRetry from invalid state')
}
}
syncStarted(syncId) {
if (!this.syncId && stateConstraints[this.syncState].has(SyncState.Syncing)) {
this.setSyncState(SyncState.Syncing);
this.syncId = syncId;
// On sucessful sync, reset retryCount
this.currentRetryCount = 0;
this.sendKeepAlivePings(); // ping the server periodically to keep the connection alive
this.log('syncStarted', 'syncId', this.syncId);
this.clientEmitter.emit('streamSyncActive', true);
this.log('emitted streamSyncActive', true);
this.checkStartTicking();
}
else {
this.log('syncStarted: invalid state transition', this.syncState, '->', SyncState.Syncing);
//throw new Error('syncStarted: invalid state transition')
}
}
syncDown(streamId, retryParams) {
if (this.syncId === undefined) {
return;
}
if (retryParams !== undefined && retryParams.syncId !== this.syncId) {
return;
}
if (streamId === undefined || streamId.length === 0) {
this.logError('syncDown: streamId is empty');
return;
}
if (this.syncState !== SyncState.Syncing) {
this.logError('syncDown: invalid state transition', this.syncState);
return;
}
const stream = this.streams.get(streamIdAsString(streamId));
if (!stream) {
this.log('syncDown: stream not found', streamIdAsString(streamId));
return;
}
const cookie = stream.syncCookie;
if (!cookie) {
this.logError('syncDown: syncCookie not found', streamIdAsString(streamId));
return;
}
const syncId = this.syncId;
const retryCount = retryParams?.retryCount ?? 0;
this.rpcClient
.addStreamToSync({
syncId: this.syncId,
syncPos: cookie,
})
.catch((err) => {
const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 60000);
this.logError('syncDown: addStreamToSync error', err, 'retryParams', retryParams, 'retryDelay', retryDelay);
setTimeout(() => {
this.syncDown(streamId, {
syncId,
retryCount: retryCount + 1,
});
}, retryDelay);
});
}
syncClosed() {
this.stopPing();
if (this.syncState === SyncState.Canceling) {
this.log('server acknowledged our close atttempt', this.syncId);
}
else {
this.log('server cancelled unepexectedly, go through the retry loop', this.syncId);
this.setSyncState(SyncState.Retrying);
}
}
async onUpdate(res) {
// Until we've completed canceling, accept responses
if (this.syncState === SyncState.Syncing || this.syncState === SyncState.Canceling) {
if (this.syncId != res.syncId) {
throw new Error(`syncId mismatch; has:'${this.syncId}', got:${res.syncId}'. Throw away update.`);
}
const syncStream = res.stream;
if (syncStream !== undefined) {
try {
/*
this.log(
'sync RESULTS for stream',
streamId,
'events=',
streamAndCookie.events.length,
'nextSyncCookie=',
streamAndCookie.nextSyncCookie,
'startSyncCookie=',
streamAndCookie.startSyncCookie,
)
*/
const streamIdBytes = syncStream.nextSyncCookie?.streamId ?? Uint8Array.from([]);
const streamId = streamIdAsString(streamIdBytes);
if (this.inFlightSyncCookies.has(streamId)) {
this.inFlightSyncCookies.delete(streamId);
if (this.inFlightSyncCookies.size === 0 ||
Date.now() - this.lastLogInflightAt > 3000) {
if (this.inFlightSyncCookies.size === 0 &&
this.syncStartedAt !== undefined) {
const duration = performance.now() - this.syncStartedAt;
this.log('sync completed in', duration, 'ms');
this.syncStartedAt = undefined;
}
else {
this.log('onUpdate: remaining streams in flight', this.inFlightSyncCookies.size);
}
this.lastLogInflightAt = Date.now();
}
}
const streamRecord = this.streams.get(streamId);
if (streamRecord === undefined) {
this.log('sync got stream', streamId, 'NOT FOUND');
}
else if (syncStream.syncReset) {
this.logDebug('initStream from sync reset', streamId, 'RESET');
const response = await unpackStream(syncStream, this.unpackEnvelopeOpts);
streamRecord.syncCookie = response.streamAndCookie.nextSyncCookie;
await streamRecord.stream.initializeFromResponse(response);
}
else {
const streamAndCookie = await unpackStreamAndCookie(syncStream, this.unpackEnvelopeOpts);
streamRecord.syncCookie = streamAndCookie.nextSyncCookie;
await streamRecord.stream.appendEvents(streamAndCookie.events, streamAndCookie.nextSyncCookie, undefined);
}
}
catch (err) {
const e = err;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
switch (e.name) {
case 'AbortError':
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (e.inner) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logError('AbortError reason:', e.inner);
}
else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logError('AbortError message:' + e.message);
}
break;
case 'QuotaExceededError':
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
this.logError('QuotaExceededError:', e.message);
break;
default:
this.logError('onUpdate error:', err);
break;
}
}
}
else {
this.log('sync RESULTS no stream', syncStream);
}
}
else {
this.log('onUpdate: invalid state', this.syncState, 'should have been', SyncState.Syncing);
}
}
sendKeepAlivePings() {
// periodically ping the server to keep the connection alive
this.pingInfo.pingTimeout = setTimeout(() => {
const ping = async () => {
if (this.syncState === SyncState.Syncing && this.syncId) {
const n = nanoid();
this.pingInfo.nonces[n] = {
sequence: this.pingInfo.currentSequence++,
nonce: n,
pingAt: performance.now(),
};
await this.rpcClient.pingSync({
syncId: this.syncId,
nonce: n,
});
}
if (this.syncState === SyncState.Syncing) {
// schedule the next ping
this.sendKeepAlivePings();
}
};
ping().catch((err) => {
this.interruptSync?.(err);
});
}, 5 * 1000 * 60); // every 5 minutes
}
stopPing() {
clearTimeout(this.pingInfo.pingTimeout);
this.pingInfo.pingTimeout = undefined;
// print out the nonce stats
this.printNonces();
// reset the nonce stats
this.pingInfo.nonces = {};
this.pingInfo.currentSequence = 0;
}
printNonces() {
const sortedNonces = Object.values(this.pingInfo.nonces).sort((a, b) => a.sequence - b.sequence);
for (const n of sortedNonces) {
this.log(`sequence=${n.sequence}, nonce=${n.nonce}, pingAt=${n.pingAt}, receivedAt=${n.receivedAt ?? 'none'}, duration=${n.duration ?? 'none'}`);
}
}
logInvalidStateAndReturnError(currentState, newState) {
this.log(`invalid state transition ${currentState} -> ${newState}`);
return new Error(`invalid state transition ${currentState} -> ${newState}`);
}
log(...args) {
this.logSync(...args);
}
}
// priority from stream id for syncing, dms if that's what we're looking at
// channels for any high priority spaces are more important than spaces we're not looking at
// then spaces, then other channels
function priorityFromStreamId(streamId, highPriorityIds) {
if (isUserDeviceStreamId(streamId) ||
isUserInboxStreamId(streamId) ||
isUserStreamId(streamId) ||
isUserSettingsStreamId(streamId)) {
return 0;
}
if (highPriorityIds.has(streamId)) {
return 1;
}
if (isChannelStreamId(streamId)) {
const spaceId = spaceIdFromChannelId(streamId);
if (highPriorityIds.has(spaceId)) {
return 2;
}
else {
return 3;
}
}
// if we're prioritizing dms, load other dms and gdm channels
if (highPriorityIds.size > 0) {
const firstHPI = Array.from(highPriorityIds.values())[0];
if (isDMChannelStreamId(firstHPI) || isGDMChannelStreamId(firstHPI)) {
if (isDMChannelStreamId(streamId) || isGDMChannelStreamId(streamId)) {
return 4;
}
}
}
// we need spaces to structure the app
if (isSpaceStreamId(streamId)) {
return 5;
}
return 6;
}
//# sourceMappingURL=syncedStreamsLoop.js.map