mattermost-redux
Version:
Common code (API client, Redux stores, logic, utility functions) for building a Mattermost client
183 lines (182 loc) • 7.27 kB
JavaScript
"use strict";
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.DelayedDataLoader = exports.BackgroundDataLoader = void 0;
/**
* A DataLoader is an object that can be used to batch requests for fetching objects from the server for performance
* reasons.
*/
class DataLoader {
fetchBatch;
maxBatchSize;
comparator;
pendingIdentifiers = new Set();
constructor(args) {
this.fetchBatch = args.fetchBatch;
this.maxBatchSize = args.maxBatchSize;
this.comparator = args.comparator;
}
queue(identifiersToLoad) {
for (const identifier of identifiersToLoad) {
if (!identifier) {
continue;
}
// If a custom comparator is provided, manually check for duplicates
if (this.comparator) {
let exists = false;
for (const existing of this.pendingIdentifiers) {
if (this.comparator(existing, identifier)) {
exists = true;
break;
}
}
if (!exists) {
this.pendingIdentifiers.add(identifier);
}
}
else {
// Without a comparator, Set automatically handles uniqueness
this.pendingIdentifiers.add(identifier);
}
}
}
/**
* prepareBatch removes an array of identifiers for data to be loaded from pendingIdentifiers and returns it. If
* pendingIdentifiers contains more than maxBatchSize identifiers, then only that many are returned, but if it
* contains fewer than that, all of the identifiers are returned and pendingIdentifiers is cleared.
*/
prepareBatch() {
let nextBatch;
// Since we can only fetch a defined number of identifiers at a time, we need to batch the requests
if (this.pendingIdentifiers.size >= this.maxBatchSize) {
nextBatch = [];
// We use temp buffer here to store up until max buffer size
// and clear out processed identifiers
for (const identifier of this.pendingIdentifiers) {
nextBatch.push(identifier);
this.pendingIdentifiers.delete(identifier);
if (nextBatch.length >= this.maxBatchSize) {
break;
}
}
}
else {
// If we have less than max buffer size, we can directly fetch the data
nextBatch = Array.from(this.pendingIdentifiers);
this.pendingIdentifiers.clear();
}
return {
identifiers: nextBatch,
moreToLoad: this.pendingIdentifiers.size > 0,
};
}
/**
* isBusy is a method for testing which returns true if the DataLoader is waiting to request or receive any data.
*/
isBusy() {
return this.pendingIdentifiers.size > 0;
}
}
/**
* A BackgroundDataLoader is an object that can be used to batch requests for fetching objects from the server. Instead
* of requesting data immediately, it will periodically check if any objects need to be requested from the server.
*
* It's intended to be used for loading low priority data such as information needed in response to WebSocket messages
* that the user won't see immediately.
*/
class BackgroundDataLoader extends DataLoader {
intervalId = -1;
startIntervalIfNeeded(ms) {
if (this.intervalId !== -1) {
return;
}
this.intervalId = window.setInterval(() => this.fetchBatchNow(), ms);
}
stopInterval() {
clearInterval(this.intervalId);
this.intervalId = -1;
}
fetchBatchNow() {
const { identifiers } = this.prepareBatch();
if (identifiers.length === 0) {
return;
}
this.fetchBatch(identifiers);
}
isBusy() {
return super.isBusy() || this.intervalId !== -1;
}
}
exports.BackgroundDataLoader = BackgroundDataLoader;
/**
* A DelayedDataLoader is an object that can be used to batch requests for fetching objects from the server. Instead of
* requesting data immediately, it will wait for an amount of time and then send a request to the server for all of
* the data which would've been requested during that time.
*
* More specifically, when queue is first called, a timer will be started. Until that timer expires, any other
* calls to queue will have the provided identifiers added to the ones from the initial call. When the timer
* finally expires, the request will be sent to the server to fetch that data. After that, the timer will be reset and
* the next call to queue will start a new one.
*
* DelayedDataLoader is intended to be used for loading data for components which are unaware of each other and may appear
* in different places in the UI from each other which could otherwise send repeated requests for the same or similar
* data as one another.
*/
class DelayedDataLoader extends DataLoader {
wait = -1;
timeoutId = -1;
timeoutCallbacks = new Set();
constructor(args) {
super(args);
this.wait = args.wait;
}
queue(identifiersToLoad) {
super.queue(identifiersToLoad);
this.startTimeoutIfNeeded();
}
queueAndWait(identifiersToLoad) {
return new Promise((resolve) => {
super.queue(identifiersToLoad);
// Save the callback that will resolve this promise so that the caller of this method can wait for its
// data to be loaded
this.timeoutCallbacks.add({
identifiers: new Set(identifiersToLoad),
resolve,
});
this.startTimeoutIfNeeded();
});
}
startTimeoutIfNeeded() {
if (this.timeoutId !== -1) {
return;
}
this.timeoutId = window.setTimeout(() => {
// Ensure that timeoutId is cleared and we get a pop identifiers off of pendingIdentifiers before doing
// anything async so that any calls to queue that are made while fetching this batch will be
// added to the next batch instead
this.timeoutId = -1;
const { identifiers, moreToLoad } = this.prepareBatch();
// Start another timeout if there's still more data to load
if (moreToLoad) {
this.startTimeoutIfNeeded();
}
this.fetchBatch(identifiers).then(() => this.resolveCompletedCallbacks(identifiers));
}, this.wait);
}
resolveCompletedCallbacks(identifiers) {
for (const callback of this.timeoutCallbacks) {
for (const identifier of identifiers) {
callback.identifiers.delete(identifier);
}
if (callback.identifiers.size === 0) {
this.timeoutCallbacks.delete(callback);
callback.resolve();
}
}
}
isBusy() {
return super.isBusy() || this.timeoutCallbacks.size > 0 || this.timeoutId !== -1;
}
}
exports.DelayedDataLoader = DelayedDataLoader;