@ibm-cloud/cloudant
Version:
IBM Cloudant Node.js SDK
256 lines • 11 kB
JavaScript
"use strict";
/**
* © Copyright IBM Corporation 2022, 2023. All Rights Reserved.
*
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChangesResultIterableIterator = void 0;
const ibm_cloud_sdk_core_1 = require("ibm-cloud-sdk-core");
const node_util_1 = require("node:util");
const changesParamsHelper_1 = require("./changesParamsHelper");
var TransientErrorSuppression;
(function (TransientErrorSuppression) {
TransientErrorSuppression[TransientErrorSuppression["ALWAYS"] = 0] = "ALWAYS";
TransientErrorSuppression[TransientErrorSuppression["NEVER"] = 1] = "NEVER";
TransientErrorSuppression[TransientErrorSuppression["TIMER"] = 2] = "TIMER";
})(TransientErrorSuppression || (TransientErrorSuppression = {}));
class ChangesResultIterableIterator {
timeoutPromise = (0, node_util_1.promisify)(setTimeout);
cancelToken = 'CloudantChangesIteratorCancel';
client;
doneResult = {
done: true,
value: undefined,
};
errorTolerance;
logger = (0, ibm_cloud_sdk_core_1.getNewLogger)('cloudant-node-sdk');
mode;
promisedConfig;
transientErrorSuppression;
baseDelay = 100;
expRetryGate = Math.floor(Math.log2(changesParamsHelper_1.ChangesParamsHelper.LONGPOLL_TIMEOUT / this.baseDelay));
cancel;
countDown;
inflight = null;
params;
// Default to "infinite"
pending = Number.MAX_VALUE;
since;
stopped = false;
successTimestamp;
retry = 0;
/** @internal */
static BATCH_SIZE = 10_000;
constructor(client, params, mode, errorTolerance) {
this.client = client;
this.params = params;
this.mode = mode;
this.errorTolerance = errorTolerance;
if (this.params.limit !== undefined) {
this.logger.debug(`Applying changes limit ${this.params.limit}`);
this.countDown = this.params.limit;
}
if (this.errorTolerance === 0) {
this.transientErrorSuppression = TransientErrorSuppression.NEVER;
this.logger.debug('Not suppressing errors.');
}
else if (this.errorTolerance === Number.MAX_VALUE) {
this.transientErrorSuppression = TransientErrorSuppression.ALWAYS;
this.logger.debug('Maximum error suppression.');
}
else {
this.transientErrorSuppression = TransientErrorSuppression.TIMER;
this.logger.debug(`Suppress errors for ${this.errorTolerance} ms.`);
}
if (this.params.since === undefined) {
this.since = this.mode === changesParamsHelper_1.Mode.LISTEN ? 'now' : '0';
}
else {
this.since = this.params.since;
}
this.promisedConfig = this.configure();
this.successTimestamp = Date.now();
}
async configure() {
if (this.params.includeDocs) {
return this.client
.getDatabaseInformation({
db: this.params.db,
})
.then((info) => {
if (info.result &&
'docCount' in info.result &&
info.result.docCount > 0 &&
'sizes' in info.result &&
'external' in info.result.sizes &&
info.result.sizes.external > 0) {
// Calculate an average doc size + typical change content size
// and try to keep each batch to be about 5 MB
this.params.limit =
Math.floor((5 * 1024 * 1024) /
(info.result.sizes.external / info.result.docCount + 500)) || 1;
}
});
}
this.params.limit = ChangesResultIterableIterator.BATCH_SIZE;
return Promise.resolve();
}
[Symbol.asyncIterator]() {
return this;
}
async return(value) {
this.logger.debug('Iterator return entry.');
if (!this.stopped) {
this.logger.debug('Setting stopped flag.');
this.stopped = true;
if (this.cancel) {
this.logger.debug('Cancelling inflight requests.');
this.cancel(new Error(this.cancelToken));
}
}
this.logger.debug('Iterator return exiting done.');
return this.doneResult;
}
async next(value) {
this.logger.debug('Iterator next entry.');
// Stop the iterator if stopped is set to true.
if (this.stopped) {
this.logger.debug('Already stopped, iterator next exiting done.');
return this.doneResult;
}
// Await the async config and also
// yield to the event loop so our long-running requests don't enqueue
// more long-running requests on the same microtask queue and
// end up blocking I/O.
await Promise.all([
this.promisedConfig,
new Promise((resolve) => {
setImmediate(resolve);
// eslint-disable-next-line no-useless-return
return;
}),
]);
this.logger.debug('Making next request.');
// Make a new cancellable promise that can race with the request
// in case the follower is stopped.
let resolveCancellable;
const cancellable = new Promise((resolve, reject) => {
resolveCancellable = resolve;
this.cancel = reject;
});
return Promise.race([
cancellable,
this.client.postChanges(changesParamsHelper_1.ChangesParamsHelper.cloneParams(this.params, this.mode, this.since, this.countDown && this.countDown < this.params.limit
? this.countDown
: undefined)),
])
.then((response) => {
this.logger.debug('Got next response.');
// Reset the retry counter
this.retry = 0;
if (this.transientErrorSuppression === TransientErrorSuppression.TIMER) {
this.logger.debug('Setting new timestamp for timer suppression');
this.successTimestamp = Date.now();
}
this.since = response.result.lastSeq;
this.pending = response.result.pending;
if (this.mode === changesParamsHelper_1.Mode.FINITE && this.pending === 0) {
this.logger.debug('No more changes pending, setting stopped flag.');
this.stopped = true;
}
if (this.countDown !== undefined) {
this.logger.debug('Decrementing limit.');
this.countDown -= response.result.results.length;
if (this.countDown <= 0) {
this.logger.debug('Limit reached, setting stopped flag.');
this.stopped = true;
}
}
this.logger.debug('Iterator next exiting with result.');
return { done: false, value: response.result };
})
.catch((err) => {
this.logger.debug(`Caught error ${err.message}`);
if (err.message === this.cancelToken) {
this.logger.debug('Iterator next exiting cancelled.');
return this.doneResult;
}
switch (this.transientErrorSuppression) {
case TransientErrorSuppression.ALWAYS:
break;
case TransientErrorSuppression.TIMER:
if (Date.now() < this.successTimestamp + this.errorTolerance) {
break;
}
this.logger.debug('Error tolerance deadline exceeded.');
// In the case the timer has been exceeded we want to throw so
// fall through
case TransientErrorSuppression.NEVER:
this.logger.verbose(`ChangesResultStream stream: ${err.message}`);
throw err;
default:
err.message = `${err.message}\nMeanwhile this other error happened: No implementation available for TransientErrorSuppression of ${this.transientErrorSuppression}.`;
throw err;
}
switch (err.code) {
case 400:
case 401:
case 403:
case 404:
// Terminal error, stop running
this.logger.debug('Terminal error');
this.logger.verbose(`ChangesResultStream stream: ${err.message}`);
throw err;
default: {
// Note this includes Errors
// which handles cases like disconnections and incomplete
// bodies where we may have received a successful response
// code, but couldn't e.g. parse the body
this.logger.verbose(`Suppressing transient error ${err.message}.`);
const emptyChangesResultPromise = {
done: false,
value: {
lastSeq: this.since,
pending: this.pending,
results: [],
},
};
let expDelay;
if (this.retry > this.expRetryGate) {
// If we've exceeded the cap, use the timeout value
expDelay = changesParamsHelper_1.ChangesParamsHelper.LONGPOLL_TIMEOUT;
}
else {
expDelay = 2 ** this.retry * this.baseDelay;
}
const delay = Math.round(Math.random() * expDelay) + 1;
this.logger.debug(`Backing off for ${delay} ms.`);
this.retry += 1;
return this.timeoutPromise(delay).then(() => {
this.logger.debug(`Iterator next exiting with empty result.`);
return emptyChangesResultPromise;
});
}
}
})
.finally(() => {
this.logger.debug('Cleaning up cancellable.');
this.cancel = null;
// Resolve the cancellable to ensure clean up can happen
resolveCancellable();
});
}
}
exports.ChangesResultIterableIterator = ChangesResultIterableIterator;
//# sourceMappingURL=changesResultIterator.js.map