UNPKG

@azure/cosmos

Version:
397 lines (396 loc) • 15.2 kB
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 });