160 lines
4.6 KiB
JavaScript
160 lines
4.6 KiB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
|
|
/*
|
|
Copyright 2026 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import { calculateRetryBackoff } from "./http-api/index.js";
|
|
import { sleep } from "./utils.js";
|
|
|
|
/**
|
|
* How to fetch and cache a given value, as passed to {@link PollingCachedValue}.
|
|
*/
|
|
|
|
/**
|
|
* Defines a generic mechanism to fetch and cache a value, refreshing it periodically.
|
|
*/
|
|
export class PollingCachedValue {
|
|
/**
|
|
* Build a generic mechanism allowing to fetch and cache a given value.
|
|
* @param opts - Describes what to fetch and how to cache it, see {@link PollingCachedValueOptions}.
|
|
*/
|
|
constructor(opts) {
|
|
_defineProperty(this, "cached", void 0);
|
|
_defineProperty(this, "fetchPromise", void 0);
|
|
_defineProperty(this, "ttlTimeoutHandle", void 0);
|
|
_defineProperty(this, "isStopped", false);
|
|
_defineProperty(this, "ttlMillis", void 0);
|
|
_defineProperty(this, "logger", void 0);
|
|
this.opts = opts;
|
|
this.ttlMillis = opts.ttlMillis;
|
|
this.logger = opts.logger.getChild(`PollingCachedValue<${opts.name}>`);
|
|
}
|
|
|
|
/**
|
|
* Gets the cached value if any.
|
|
*/
|
|
get() {
|
|
return this.cached;
|
|
}
|
|
|
|
/**
|
|
* Ensures that the value was fetched once before getting the actual cached value.
|
|
*/
|
|
async wait() {
|
|
await this.doFetch();
|
|
return this.cached;
|
|
}
|
|
|
|
/**
|
|
* Call this as early as possible and when the value can already be fetched.
|
|
* @param ttlMillis - The time-to-live before the value gets refreshed, overriding the one
|
|
* given at construction. If omitted the configured default is kept.
|
|
*/
|
|
start(ttlMillis) {
|
|
if (ttlMillis !== undefined) {
|
|
this.ttlMillis = ttlMillis;
|
|
}
|
|
this.isStopped = false;
|
|
// Request a fetch as soon as possible
|
|
this.doFetch().catch(err => {
|
|
this.logger.debug("Cached value fetch on start did fail", err);
|
|
});
|
|
}
|
|
/**
|
|
* Call when the client is stopped, or when the cached value is no longer needed.
|
|
*/
|
|
stop() {
|
|
if (this.ttlTimeoutHandle) {
|
|
clearTimeout(this.ttlTimeoutHandle);
|
|
}
|
|
this.isStopped = true;
|
|
}
|
|
|
|
/**
|
|
* Force to refresh.
|
|
* @throws
|
|
*/
|
|
async refresh() {
|
|
await this.doFetch(true);
|
|
return this.cached;
|
|
}
|
|
|
|
/**
|
|
* Internal fetch method
|
|
* @param force - if set to true will force a fetch of the value even if a fetch is already in progress.
|
|
* @private
|
|
*/
|
|
async doFetch(force = false) {
|
|
if (this.fetchPromise && !force) {
|
|
await this.fetchPromise;
|
|
return;
|
|
}
|
|
this.fetchPromise = this.fetchWithRetryPolicy();
|
|
try {
|
|
this.cached = await this.fetchPromise;
|
|
if (this.isStopped) {
|
|
this.fetchPromise = undefined;
|
|
return;
|
|
}
|
|
this.logger.trace(`New cachedValue: ${this.cached}`);
|
|
this.opts.onValueCached?.(this.cached);
|
|
// The value is cached only for a given time.
|
|
if (this.ttlMillis) {
|
|
this.ttlTimeoutHandle = setTimeout(() => {
|
|
this.fetchPromise = undefined;
|
|
void this.doFetch();
|
|
}, this.ttlMillis);
|
|
}
|
|
} catch (error) {
|
|
this.logger.debug(`Error fetching server value for ${error}`);
|
|
if (this.isStopped) {
|
|
this.fetchPromise = undefined;
|
|
return;
|
|
}
|
|
if (!this.opts.shouldCacheError?.(error)) {
|
|
// clear the promise, i.e next tentative will retry to fetch
|
|
this.fetchPromise = undefined;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gracefully handle transient error retries, respect retry_after_millis for rate limits.
|
|
* @private
|
|
*/
|
|
async fetchWithRetryPolicy() {
|
|
let currentRetryCount = 0;
|
|
while (true) {
|
|
try {
|
|
return await this.opts.fetch();
|
|
} catch (e) {
|
|
this.logger.trace(`Failed to fetch retry: ${e}`);
|
|
if (this.isStopped) {
|
|
throw e;
|
|
}
|
|
currentRetryCount++;
|
|
const backoff = calculateRetryBackoff(e, currentRetryCount, true);
|
|
if (backoff < 0) {
|
|
// Max number of retries reached, or error is not retryable. rethrow the error
|
|
throw e;
|
|
}
|
|
// wait for the specified time and then retry the request
|
|
await sleep(backoff);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=pollingCachedValue.js.map
|