UNPKG

@river-build/sdk

Version:

For more details, visit the following resources:

285 lines 11.7 kB
import { isChannelStreamId, isSpaceStreamId, isUserDeviceStreamId, isUserSettingsStreamId, isUserStreamId, isUserInboxStreamId, spaceIdFromChannelId, isDMChannelStreamId, isGDMChannelStreamId, } from './id'; import { check, dlog, dlogError } from '@river-build/dlog'; import pLimit from 'p-limit'; const MAX_CONCURRENT_FROM_PERSISTENCE = 5; const MAX_CONCURRENT_FROM_NETWORK = 20; const concurrencyLimit = pLimit(MAX_CONCURRENT_FROM_NETWORK); export class SyncedStreamsExtension { persistenceStore; logId; log; logDebug; logError; delegate; tasks = new Array(); streamIds = new Set(); highPriorityIds; started = false; inProgressTick; timeoutId; initStreamsStartTime = performance.now(); startSyncRequested = false; didLoadStreamsFromPersistence = false; didLoadHighPriorityStreams = false; streamCountRequiringNetworkAccess = 0; numStreamsLoadedFromCache = 0; numStreamsLoadedFromNetwork = 0; numStreamsFailedToLoad = 0; totalStreamCount = 0; loadedStreamCount = 0; initStatus = { isHighPriorityDataLoaded: false, isLocalDataLoaded: false, isRemoteDataLoaded: false, progress: 0, }; constructor(highPriorityStreamIds, delegate, persistenceStore, logId) { this.persistenceStore = persistenceStore; this.logId = logId; this.log = dlog('csb:syncedStreamsExtension', { defaultEnabled: true }).extend(logId); this.logDebug = dlog('csb:syncedStreamsExtension:debug', { defaultEnabled: false }).extend(logId); this.logError = dlogError('csb:syncedStreamsExtension:error').extend(logId); this.highPriorityIds = new Set(highPriorityStreamIds ?? []); this.delegate = delegate; } setStreamIds(streamIds) { check(this.streamIds.size === 0, 'setStreamIds called twice'); this.streamIds = new Set(streamIds); this.totalStreamCount = streamIds.length; } setHighPriorityStreams(streamIds) { this.highPriorityIds = new Set(streamIds); } setStartSyncRequested(startSyncRequested) { this.startSyncRequested = startSyncRequested; if (startSyncRequested) { this.checkStartTicking(); } } start() { check(!this.started, 'start() called twice'); this.started = true; this.numStreamsLoadedFromCache = 0; this.numStreamsLoadedFromNetwork = 0; this.numStreamsFailedToLoad = 0; this.tasks.push(() => this.loadStreamsFromPersistence()); this.tasks.push(() => this.loadStreamsFromNetwork()); this.checkStartTicking(); } async stop() { await this.stopTicking(); } checkStartTicking() { if (!this.started || this.timeoutId) { return; } // This means that we're finished. Ticking stops here. if (this.tasks.length === 0 && !this.startSyncRequested) { this.emitClientStatus(); const initStreamsEndTime = performance.now(); const executionTime = initStreamsEndTime - this.initStreamsStartTime; this.log('streamInitializationDuration', { streamInitializationDuration: executionTime, streamsInitializedFromCache: this.numStreamsLoadedFromCache, streamsInitializedFromNetwork: this.numStreamsLoadedFromNetwork, streamsFailedToLoad: this.numStreamsFailedToLoad, }); this.log('Streams loaded from cache', this.numStreamsLoadedFromCache); this.log('Streams loaded from network', this.numStreamsLoadedFromNetwork); this.log('Streams failed to load', this.numStreamsFailedToLoad); this.log(`Total time: ${executionTime.toFixed(0)} ms`); return; } this.timeoutId = setTimeout(() => { this.inProgressTick = this.tick(); this.inProgressTick .catch((e) => this.logError('ProcessTick Error', e)) .finally(() => { this.timeoutId = undefined; setTimeout(() => this.checkStartTicking(), 0); }); }, 0); } async loadStreamsFromPersistence() { this.log('####loadingStreamsFromPersistence'); const now = performance.now(); // aellis it seems like it would be faster to pull the high priority streams first // then load the rest of the streams after, but it's not! // for 300ish streams,loading the rest of the streams after the application has started // going takes 30-50 seconds,doing it this way takes 4 seconds const loadedStreams = await this.persistenceStore.loadStreams([ ...Array.from(this.highPriorityIds), ...Array.from(this.streamIds), ]); const t1 = performance.now(); this.log('####Performance: loaded streams from persistence!!', t1 - now); const hpStreamIds = Array.from(this.highPriorityIds).filter((x) => loadedStreams[x] !== undefined); await Promise.all(hpStreamIds.map(async (streamId) => { await this.loadStreamFromPersistence(streamId, loadedStreams[streamId]); delete loadedStreams[streamId]; })); this.didLoadHighPriorityStreams = true; this.emitClientStatus(); // wait for 10ms to allow the client to update the status const t2 = performance.now(); this.log('####Performance: loadedHighPriorityStreams!!', t2 - t1); // this is real goofy, it makes the app smooth // push on a final task to update the client status and report stats this.tasks.unshift(async () => { const t3 = performance.now(); this.log('####Performance: loadedLowPriorityStreams!!', t3 - t2, 'total:', t3 - now); this.didLoadStreamsFromPersistence = true; this.emitClientStatus(); }); // freeze the remaining stream ids const streamIds = Array.from(this.streamIds).filter((x) => loadedStreams[x] !== undefined); // make a step task that will load the next batch of streams const stepTask = async () => { const tsn = performance.now(); if (streamIds.length === 0) { return; } // it sorts and slices the array const streamIdsForStep = streamIds .sort((a, b) => priorityFromStreamId(a, this.highPriorityIds) - priorityFromStreamId(b, this.highPriorityIds)) .splice(0, MAX_CONCURRENT_FROM_PERSISTENCE); // and then loads MAX_CONCURRENT_STREAMS streams await Promise.all(streamIdsForStep.map(async (streamId) => { await this.loadStreamFromPersistence(streamId, loadedStreams[streamId]); delete loadedStreams[streamId]; })); this.logDebug('####Performance: STEP STREAMS!! processed', streamIdsForStep.length, 'remaining', streamIds.length, performance.now() - tsn); // do the next few this.tasks.unshift(stepTask); }; // push on the step task as the next task to run this.tasks.unshift(stepTask); } async loadStreamFromPersistence(streamId, persistedData) { const allowGetStream = this.highPriorityIds.has(streamId); try { await this.delegate.initStream(streamId, allowGetStream, persistedData); this.loadedStreamCount++; this.numStreamsLoadedFromCache++; this.streamIds.delete(streamId); } catch (err) { this.streamCountRequiringNetworkAccess++; this.logError('Error initializing stream from persistence', streamId, err); } this.emitClientStatus(); } async loadStreamsFromNetwork() { const syncItems = Array.from(this.streamIds).map((streamId) => { return { streamId, priority: priorityFromStreamId(streamId, this.highPriorityIds), }; }); syncItems.sort((a, b) => a.priority - b.priority); await Promise.all(syncItems.map((item) => this.loadStreamFromNetwork(item.streamId))); this.emitClientStatus(); } async loadStreamFromNetwork(streamId) { this.logDebug('Performance: adding stream from network', streamId); return concurrencyLimit(async () => { try { await this.delegate.initStream(streamId, true); this.numStreamsLoadedFromNetwork++; this.streamIds.delete(streamId); } catch (err) { this.logError('Error initializing stream', streamId, err); this.numStreamsFailedToLoad++; } this.loadedStreamCount++; this.streamCountRequiringNetworkAccess--; this.emitClientStatus(); }); } async tick() { const task = this.tasks.shift(); if (task) { return task(); } // Finish everything before starting sync if (this.startSyncRequested) { this.startSyncRequested = false; return this.startSync(); } } async startSync() { try { await this.delegate.startSyncStreams(); } catch (err) { this.logError('sync failure', err); } } emitClientStatus() { this.initStatus.isHighPriorityDataLoaded = this.didLoadHighPriorityStreams; this.initStatus.isLocalDataLoaded = this.didLoadStreamsFromPersistence; this.initStatus.isRemoteDataLoaded = this.didLoadStreamsFromPersistence && this.streamCountRequiringNetworkAccess === 0; if (this.totalStreamCount > 0) { this.initStatus.progress = (this.totalStreamCount - this.streamIds.size) / this.totalStreamCount; } this.delegate.emitClientInitStatus(this.initStatus); } async stopTicking() { if (this.timeoutId) { clearTimeout(this.timeoutId); this.timeoutId = undefined; } if (this.inProgressTick) { try { await this.inProgressTick; } catch (e) { this.logError('ProcessTick Error while stopping', e); } finally { this.inProgressTick = undefined; } } } } // priority from stream id for loading, we need spaces to structure the app, dms if that's what we're looking at // and channels for any high priority spaces function priorityFromStreamId(streamId, highPriorityIds) { if (isUserDeviceStreamId(streamId) || isUserInboxStreamId(streamId) || isUserStreamId(streamId) || isUserSettingsStreamId(streamId)) { return 0; } if (highPriorityIds.has(streamId)) { return 1; } // if we're prioritizing dms, load other dms and gdm channels if (highPriorityIds.size > 0) { const hasHighPriorityDmORGDm = Array.from(highPriorityIds).some((x) => isDMChannelStreamId(x) || isGDMChannelStreamId(x)); if (hasHighPriorityDmORGDm) { if (isDMChannelStreamId(streamId) || isGDMChannelStreamId(streamId)) { return 2; } } } // we need spaces to structure the app if (isSpaceStreamId(streamId)) { return 3; } if (isChannelStreamId(streamId)) { const spaceId = spaceIdFromChannelId(streamId); if (highPriorityIds.has(spaceId)) { return 4; } else { return 5; } } return 6; } //# sourceMappingURL=syncedStreamsExtension.js.map