@azure/cosmos
Version:
Microsoft Azure Cosmos DB Service Node.js SDK for NOSQL API
397 lines (396 loc) • 15.2 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var ChangeFeedForEpkRange_exports = {};
__export(ChangeFeedForEpkRange_exports, {
ChangeFeedForEpkRange: () => ChangeFeedForEpkRange
});
module.exports = __toCommonJS(ChangeFeedForEpkRange_exports);
var import_ChangeFeedRange = require("./ChangeFeedRange.js");
var import_ChangeFeedIteratorResponse = require("./ChangeFeedIteratorResponse.js");
var import_routing = require("../../routing/index.js");
var import_FeedRangeQueue = require("./FeedRangeQueue.js");
var import_common = require("../../common/index.js");
var import_request = require("../../request/index.js");
var import_CompositeContinuationToken = require("./CompositeContinuationToken.js");
var import_changeFeedUtils = require("./changeFeedUtils.js");
var import_diagnostics = require("../../utils/diagnostics.js");
class ChangeFeedForEpkRange {
/**
* @internal
*/
constructor(clientContext, container, partitionKeyRangeCache, resourceId, resourceLink, url, changeFeedOptions, epkRange) {
this.clientContext = clientContext;
this.container = container;
this.partitionKeyRangeCache = partitionKeyRangeCache;
this.resourceId = resourceId;
this.resourceLink = resourceLink;
this.url = url;
this.changeFeedOptions = changeFeedOptions;
this.epkRange = epkRange;
this.queue = new import_FeedRangeQueue.FeedRangeQueue();
this.continuationToken = changeFeedOptions.continuationToken ? JSON.parse(changeFeedOptions.continuationToken) : void 0;
this.isInstantiated = false;
if (changeFeedOptions.startFromNow) {
this.startFromNow = true;
} else if (changeFeedOptions.startTime) {
this.startTime = changeFeedOptions.startTime.toUTCString();
}
}
continuationToken;
queue;
startTime;
isInstantiated;
rId;
startFromNow;
async setIteratorRid(diagnosticNode) {
const { resource } = await this.container.readInternal(diagnosticNode);
this.rId = resource._rid;
}
continuationTokenRidMatchContainerRid() {
if (this.continuationToken.rid !== this.rId) {
return false;
}
return true;
}
async fillChangeFeedQueue(diagnosticNode) {
if (this.continuationToken) {
await this.fetchContinuationTokenFeedRanges(diagnosticNode);
} else {
await this.fetchOverLappingFeedRanges(diagnosticNode);
}
this.isInstantiated = true;
}
/**
* Fill the queue with the feed ranges overlapping with the given epk range.
*/
async fetchOverLappingFeedRanges(diagnosticNode) {
try {
const overLappingRanges = await this.partitionKeyRangeCache.getOverlappingRanges(
this.url,
this.epkRange,
diagnosticNode
);
for (const overLappingRange of overLappingRanges) {
const [epkMinHeader, epkMaxHeader] = await (0, import_changeFeedUtils.extractOverlappingRanges)(
this.epkRange,
overLappingRange
);
const feedRange = new import_ChangeFeedRange.ChangeFeedRange(
overLappingRange.minInclusive,
overLappingRange.maxExclusive,
"",
epkMinHeader,
epkMaxHeader
);
this.queue.enqueue(feedRange);
}
} catch (err) {
throw new import_request.ErrorResponse(err.message);
}
}
/**
* Fill the queue with feed ranges from continuation token
*/
async fetchContinuationTokenFeedRanges(diagnosticNode) {
const contToken = this.continuationToken;
if (!this.continuationTokenRidMatchContainerRid()) {
throw new import_request.ErrorResponse("The continuation token is not for the current container definition");
} else {
for (const cToken of contToken.Continuation) {
const queryRange = new import_routing.QueryRange(cToken.minInclusive, cToken.maxExclusive, true, false);
try {
const overLappingRanges = await this.partitionKeyRangeCache.getOverlappingRanges(
this.url,
queryRange,
diagnosticNode
);
for (const overLappingRange of overLappingRanges) {
const [epkMinHeader, epkMaxHeader] = await (0, import_changeFeedUtils.extractOverlappingRanges)(
queryRange,
overLappingRange
);
const feedRange = new import_ChangeFeedRange.ChangeFeedRange(
overLappingRange.minInclusive,
overLappingRange.maxExclusive,
cToken.continuationToken,
epkMinHeader,
epkMaxHeader
);
this.queue.enqueue(feedRange);
}
} catch (err) {
throw new import_request.ErrorResponse(err.message);
}
}
}
}
/**
* Change feed is an infinite feed. hasMoreResults is always true.
*/
get hasMoreResults() {
return true;
}
/**
* Gets an async iterator which will yield change feed results.
*/
async *getAsyncIterator() {
do {
const result = await this.readNext();
yield result;
} while (this.hasMoreResults);
}
/**
* Gets an async iterator which will yield pages of results from Azure Cosmos DB.
*
* Keeps iterating over the feedranges and checks if any feed range has new result. Keeps note of the last feed range which returned non 304 result.
*
* When same feed range is reached and no new changes are found, a 304 (not Modified) is returned to the end user. Then starts process all over again.
*/
async readNext() {
return (0, import_diagnostics.withDiagnostics)(async (diagnosticNode) => {
if (!this.isInstantiated) {
await this.setIteratorRid(diagnosticNode);
await this.fillChangeFeedQueue(diagnosticNode);
}
let firstNotModifiedFeedRange = void 0;
let result;
do {
const [processedFeedRange, response] = await this.fetchNext(diagnosticNode);
result = response;
if (result !== void 0) {
{
if (firstNotModifiedFeedRange === void 0) {
firstNotModifiedFeedRange = processedFeedRange;
}
this.queue.moveFirstElementToTheEnd();
if (result.statusCode === import_common.StatusCodes.Ok) {
result.headers[import_common.Constants.HttpHeaders.ContinuationToken] = this.generateContinuationToken();
if (this.clientContext.enableEncryption) {
await (0, import_changeFeedUtils.decryptChangeFeedResponse)(
result,
diagnosticNode,
this.changeFeedOptions.changeFeedMode,
this.container.encryptionProcessor
);
}
return result;
}
}
}
} while (!this.checkedAllFeedRanges(firstNotModifiedFeedRange));
result.headers[import_common.Constants.HttpHeaders.ContinuationToken] = this.generateContinuationToken();
return result;
}, this.clientContext);
}
generateContinuationToken = () => {
return JSON.stringify(new import_CompositeContinuationToken.CompositeContinuationToken(this.rId, this.queue.returnSnapshot()));
};
/**
* Read feed and retrieves the next page of results in Azure Cosmos DB.
*/
async fetchNext(diagnosticNode) {
const feedRange = this.queue.peek();
if (feedRange) {
const result = await this.getFeedResponse(feedRange, diagnosticNode);
const shouldRetry = await this.shouldRetryOnFailure(
feedRange,
result,
diagnosticNode
);
if (shouldRetry) {
this.queue.dequeue();
return this.fetchNext(diagnosticNode);
} else {
const continuationValueForFeedRange = result.headers[import_common.Constants.HttpHeaders.ETag];
const newFeedRange = this.queue.peek();
newFeedRange.continuationToken = continuationValueForFeedRange;
return [newFeedRange, result];
}
} else {
return [void 0, void 0];
}
}
checkedAllFeedRanges(firstNotModifiedFeedRange) {
if (firstNotModifiedFeedRange === void 0) {
return false;
}
const feedRangeQueueFirstElement = this.queue.peek();
return firstNotModifiedFeedRange.minInclusive === feedRangeQueueFirstElement?.minInclusive && firstNotModifiedFeedRange.maxExclusive === feedRangeQueueFirstElement?.maxExclusive && firstNotModifiedFeedRange.epkMinHeader === feedRangeQueueFirstElement?.epkMinHeader && firstNotModifiedFeedRange.epkMaxHeader === feedRangeQueueFirstElement?.epkMaxHeader;
}
/**
* Checks whether the current EpkRange is split into multiple ranges or not.
*
* If yes, it force refreshes the partitionKeyRange cache and enqueue children epk ranges.
*/
async shouldRetryOnFailure(feedRange, response, diagnosticNode) {
if (response.statusCode === import_common.StatusCodes.Ok || response.statusCode === import_common.StatusCodes.NotModified) {
return false;
}
const partitionSplit = response.statusCode === import_common.StatusCodes.Gone && (response.subStatusCode === import_common.SubStatusCodes.PartitionKeyRangeGone || response.subStatusCode === import_common.SubStatusCodes.CompletingSplit);
if (partitionSplit) {
const queryRange = new import_routing.QueryRange(
feedRange.epkMinHeader ? feedRange.epkMinHeader : feedRange.minInclusive,
feedRange.epkMaxHeader ? feedRange.epkMaxHeader : feedRange.maxExclusive,
true,
false
);
const resolvedRanges = await this.partitionKeyRangeCache.getOverlappingRanges(
this.url,
queryRange,
diagnosticNode,
true
);
if (resolvedRanges.length < 1) {
throw new import_request.ErrorResponse("Partition split/merge detected but no overlapping ranges found.");
}
if (resolvedRanges.length >= 1) {
await this.handleSplitOrMerge(
false,
resolvedRanges,
queryRange,
feedRange.continuationToken
);
}
return true;
}
return false;
}
/*
* Enqueues all the children feed ranges for the given feed range.
*/
async handleSplitOrMerge(shiftLeft, resolvedRanges, oldFeedRange, continuationToken) {
let flag = 0;
if (shiftLeft) {
const [epkMinHeader, epkMaxHeader] = await (0, import_changeFeedUtils.extractOverlappingRanges)(
oldFeedRange,
resolvedRanges[0]
);
const newFeedRange = new import_ChangeFeedRange.ChangeFeedRange(
resolvedRanges[0].minInclusive,
resolvedRanges[0].maxExclusive,
continuationToken,
epkMinHeader,
epkMaxHeader
);
this.queue.modifyFirstElement(newFeedRange);
flag = 1;
}
for (let i = flag; i < resolvedRanges.length; i++) {
const [epkMinHeader, epkMaxHeader] = await (0, import_changeFeedUtils.extractOverlappingRanges)(
oldFeedRange,
resolvedRanges[i]
);
const newFeedRange = new import_ChangeFeedRange.ChangeFeedRange(
resolvedRanges[i].minInclusive,
resolvedRanges[i].maxExclusive,
continuationToken,
epkMinHeader,
epkMaxHeader
);
this.queue.enqueue(newFeedRange);
}
}
/**
* Fetch the partitionKeyRangeId for the given feed range.
*
* This partitionKeyRangeId is passed to queryFeed to fetch the results.
*/
async getPartitionRangeId(feedRange, diagnosticNode) {
const min = feedRange.epkMinHeader ? feedRange.epkMinHeader : feedRange.minInclusive;
const max = feedRange.epkMaxHeader ? feedRange.epkMaxHeader : feedRange.maxExclusive;
const queryRange = new import_routing.QueryRange(min, max, true, false);
const resolvedRanges = await this.partitionKeyRangeCache.getOverlappingRanges(
this.url,
queryRange,
diagnosticNode,
false
);
if (resolvedRanges.length < 1) {
throw new import_request.ErrorResponse("No overlapping ranges found.");
}
const firstResolvedRange = resolvedRanges[0];
const isPartitionRangeChanged = feedRange.minInclusive !== firstResolvedRange.minInclusive || feedRange.maxExclusive !== firstResolvedRange.maxExclusive || resolvedRanges.length > 1;
if (isPartitionRangeChanged) {
await this.handleSplitOrMerge(true, resolvedRanges, queryRange, feedRange.continuationToken);
}
return firstResolvedRange.id;
}
async getFeedResponse(feedRange, diagnosticNode) {
const feedOptions = (0, import_changeFeedUtils.buildFeedOptions)(
this.changeFeedOptions,
feedRange.continuationToken,
this.startFromNow,
this.startTime
);
const rangeId = await this.getPartitionRangeId(feedRange, diagnosticNode);
if (this.clientContext.enableEncryption) {
await this.container.checkAndInitializeEncryption();
feedOptions.containerRid = this.container._rid;
}
try {
const finalFeedRange = this.fetchFinalFeedRange();
const response = await this.clientContext.queryFeed({
path: this.resourceLink,
resourceType: import_common.ResourceType.item,
resourceId: this.resourceId,
resultFn: (result) => result ? result.Documents : [],
query: void 0,
options: feedOptions,
diagnosticNode,
partitionKey: void 0,
partitionKeyRangeId: rangeId,
startEpk: finalFeedRange.epkMinHeader,
endEpk: finalFeedRange.epkMaxHeader
});
return new import_ChangeFeedIteratorResponse.ChangeFeedIteratorResponse(
response.result,
response.result ? response.result.length : 0,
response.code,
response.headers,
(0, import_diagnostics.getEmptyCosmosDiagnostics)()
);
} catch (err) {
if (err.code === import_common.StatusCodes.Gone) {
return new import_ChangeFeedIteratorResponse.ChangeFeedIteratorResponse(
[],
0,
err.code,
err.headers,
(0, import_diagnostics.getEmptyCosmosDiagnostics)(),
err.substatus
);
}
const errorResponse = new import_request.ErrorResponse(err.message);
errorResponse.code = err.code;
errorResponse.headers = err.headers;
throw errorResponse;
}
}
fetchFinalFeedRange() {
const feedRange = this.queue.peek();
if (feedRange) {
return feedRange;
} else {
throw new import_request.ErrorResponse("No feed range found.");
}
}
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ChangeFeedForEpkRange
});