UNPKG

grafast

Version:

Cutting edge GraphQL planning and execution engine

342 lines 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.isDistributor = isDistributor; exports.distributor = distributor; exports.resolveDistributorOptions = resolveDistributorOptions; const tslib_1 = require("tslib"); const __1 = require(".."); const assert = tslib_1.__importStar(require("../assert")); const deferred_1 = require("../deferred"); const utils_1 = require("../utils"); const DEFAULT_DISTRIBUTOR_BUFFER_SIZE = 1001; const DEFAULT_DISTRIBUTOR_BUFFER_SIZE_INCREMENT = 1001; const DEFAULT_DISTRIBUTOR_PAUSE_DURATION = 5; // milliseconds const $$isDistributor = Symbol("$$isDistributor"); function isDistributor(value) { return value != null && value[$$isDistributor] === true; } // Save on garbage collection by just using this promise for everything const DONE_PROMISE = Promise.resolve({ done: true, value: undefined, }); /** * Creates a "distributor" for the sourceIterable such that the dependent steps * may each consume it independently and safely. * * @param sourceIterable - the iterable or async iterable to clone * @param dependentSteps - the steps we're expecting to depend on this (so we * know how many clones we'll need) * @param grafastOptions - the options (from the preset) that may be relevant */ function distributor(sourceIterable, dependentSteps, abortSignal, distributorOptions) { const { distributorTargetBufferSize: targetBufferSize, distributorTargetBufferSizeIncrement: bufferSizeIncrement, distributorPauseDuration: pauseDuration, } = distributorOptions; /** * Once we start the underlying sourceIterable, we store the iterator here. */ let sourceIterator = null; /** * An array of the currently delivered index for each of the dependent steps. * One a given index has been delivered by all streams, the lowWaterMark may * advance. * -1 indicates that no item has been delivered yet. */ const deliveredIndex = dependentSteps.map(() => -1); /** * An array of the highest index requested for each of the dependent steps. * -1 indicates that no item has been requested yet. */ const requestedIndex = dependentSteps.map(() => -1); /** * If a consumer is terminated via `.return()` or `.throw()` (or if the * underlying stream terminates) then the `deliveredIndex` for that consumer * will be set to `Infinity` and their final result, to be returned from all * further `.next()` calls, will be stored here. */ const terminalResult = dependentSteps.map(() => null); /** * What's the lowest index we must retain? Equal to the lowest * `deliveredIndex` + 1 */ let lowWaterMark = 0; /** * This is our buffer of iterator results, the first consumer to request the * next index will fetch from the underlying stream and store the value here, * as the slowest consumers catch up, old results will be shifted from the * start of the array and the `lowWaterMark` will be advanced. The item to * pull for a given position is * `buffer[requestedIndex[stepIndex] - lowWaterMark]`. */ const buffer = []; // Easy way to resolve a promise for slowing down the fastest consumer let wmi = null; function lowWaterMarkIncreased() { if (wmi === null) { const d = (0, deferred_1.defer)(); wmi = d; } return wmi; } /** * The maximum index that can be retrieved from our sourceIterator. Infinity * until the iterator terminates. */ let finalIndex = Infinity; /** The final result from our sourceIterator (at index `finalIndex`), or `null` if it hasn't terminated yet. */ let finalResult = null; /** * Once termination has been handled, this will be true. * * NOTE: when this is `true`, `finalResult` will be set, but the reverse is * not necessarily true. */ let stopped = false; // Stop us retaining data we don't need to retain function maybeAdvanceLowWaterMark() { if (stopped) { return; } const smallest = Math.min(...deliveredIndex); if (!Number.isFinite(smallest) || smallest >= finalIndex) { // They're all done stopped = true; if (finalResult === null) { // finalIndex was Infinity finalIndex = 0; finalResult = DONE_PROMISE; } // Even if we never started the iterator... we should still clean it up // if we can. const iterator = (sourceIterator ?? sourceIterable); // We're not using `using` so it's not appropriate for us to async dispose //if (iterator[Symbol.asyncDispose]) { // iterator[Symbol.asyncDispose]!(); //} else if (iterator[Symbol.dispose]) { // iterator[Symbol.dispose]!(); //} else if (iterator.return) { iterator.return(); } else if (iterator?.throw) { iterator.throw(new Error("Stop")); } else { // Just ignore it? Or do we need to call `.next()` indefinitely? // Since it could be infinite, the next chain doesn't make sense, so // we'll just stop. } } else { // Advance the lowWaterMark as far as we can let advanced = false; while (smallest >= lowWaterMark) { advanced = true; buffer.shift(); lowWaterMark++; } // Announce that the lowWaterMark advanced if (advanced && wmi !== null) { // Avoid race condition const deferred = wmi; wmi = null; deferred.resolve(); } } } /** * Called when the sourceIterator completes - either with success or error. */ function sourceIteratorCompleted(_finalIndex, _finalResult) { // This indicates that sourceIterator has completed at index `finalIndex`. // This does not mean that we are `stopped`, since some clients still need // to catch up. // Note that this might be called more than once (because promises), we // want the earliest termination index to "win". if (finalResult === null || _finalIndex < finalIndex) { finalIndex = _finalIndex; finalResult = _finalResult; } } function advance(stepIndex) { const terminal = terminalResult[stepIndex]; if (terminal) return terminal; // Keep in mind `advance(stepIndex)` might be called more than once for the // same step without waiting for previous calls to resolve. const index = ++requestedIndex[stepIndex]; return yieldValue(stepIndex, index); } /** * Gets the next value from the source iterator _and_ checks to see if this * is the result that completes the source iterator. */ function getNext(index) { if (finalResult !== null) { return finalResult; } const result = Promise.resolve(sourceIterator.next()); // Check if iterator is complete result.then((value) => { if (value.done) { sourceIteratorCompleted(index, result); } }, () => void sourceIteratorCompleted(index, result)); return result; } /** * **ONLY CALL THIS** if you've already checked for a terminal result. * * Called from advance(), returns the relevant iterator result. If * necessary, waits for the low water mark to advance. */ function yieldValue(stepIndex, index) { // !! Function must be synchronous to avoid race conditions !! const bufferLength = buffer.length; /** The index within the buffer that we'd like to retrieve */ const bufferIndex = index - lowWaterMark; let result; if (bufferIndex >= bufferLength) { if (__1.isDev) { assert.strictEqual(bufferIndex, bufferLength, "We've missed some indexes?!"); } // It's our job to pull the next value! // But first... did the source iterator already complete? if (finalResult !== null) { result = finalResult; } else { if (bufferIndex >= targetBufferSize && // If this next check fails then the slowest consumer is too slow; we // should race ahead (until the next `bufferSizeIncrement`) (bufferIndex - targetBufferSize) % bufferSizeIncrement === 0) { // Whoa there! Getting a little ahead of ourselves! Wait for the slowest // consumer to advance (or for it to time out), before resolving. // const oldLowWaterMark = lowWaterMark; const next = Promise.race([ lowWaterMarkIncreased(), (0, utils_1.sleep)(pauseDuration), ]).then( // const advanced = lowWaterMark > oldLowWaterMark; // TODO: should we wait a little longer if we did advance so we're // not creating a new timer for each and every low watermark // increase? () => getNext(index)); buffer[bufferIndex] = next; /* * TODO: should we be reflecting the terminalResult here? * ``` * const terminal = terminalResult[stepIndex]; * if (terminal) return terminal; * // if (finalResult !== null) return finalResult; * ``` */ result = next; } else { result = getNext(index); buffer[bufferIndex] = result; } } } else { // The next value already exists result = buffer[bufferIndex]; } if (result == null) { return stop(stepIndex, new Error(`GrafastInternalError<330dba8c-baf0-4352-9cb7-3445e7f14bfc>: bug in Distributor; deliveredIndex: ${deliveredIndex[stepIndex]}, requestedIndex: ${requestedIndex[stepIndex]}, currentIndex: ${index}, bufferIndex: ${bufferIndex}, buffer.length: ${buffer.length}`)); } if (__1.isDev) { assert.strictEqual(deliveredIndex[stepIndex], index - 1, "Expectation of delivered index did not match"); } deliveredIndex[stepIndex] = index; maybeAdvanceLowWaterMark(); return result; } function stop(stepIndex, error, advance = true) { if (!terminalResult[stepIndex]) { deliveredIndex[stepIndex] = Infinity; terminalResult[stepIndex] = error ? Promise.reject(error) : DONE_PROMISE; if (advance) { maybeAdvanceLowWaterMark(); } } return terminalResult[stepIndex]; } function getStepIndex(stepId) { const stepIndex = dependentSteps.findIndex((s) => s.id === stepId); if (stepIndex === -1) { throw new Error(`Didn't expect step ${stepId} to depend on this distributor. Expected one of ${dependentSteps}`); } return stepIndex; } function newIterator(stepIndex) { // Kick-start the source iterator if need be if (sourceIterator === null) { sourceIterator = Symbol.asyncIterator in sourceIterable ? sourceIterable[Symbol.asyncIterator]() : sourceIterable[Symbol.iterator](); } const onAbort = () => { iterator.return(); }; const iterator = { [Symbol.asyncIterator]() { return this; }, next() { return advance(stepIndex); }, return() { abortSignal.removeEventListener("abort", onAbort); return stop(stepIndex); }, throw(e) { abortSignal.removeEventListener("abort", onAbort); return stop(stepIndex, e); }, }; abortSignal.addEventListener("abort", onAbort); return iterator; } const hasIterator = (0, utils_1.arrayOfLength)(dependentSteps.length, false); const distributor = { [$$isDistributor]: true, iterableFor(stepId) { const stepIndex = getStepIndex(stepId); return { [Symbol.asyncIterator]() { if (hasIterator[stepIndex]) { throw new Error(`Attempted to create iterator a second time (or possibly after release)!`); } hasIterator[stepIndex] = true; return newIterator(stepIndex); }, }; }, releaseIfUnused(stepId) { const stepIndex = getStepIndex(stepId); if (!hasIterator[stepIndex]) { hasIterator[stepIndex] = true; stop(stepIndex); } }, }; abortSignal.addEventListener("abort", () => { for (let stepIndex = 0, l = dependentSteps.length; stepIndex < l; stepIndex++) { hasIterator[stepIndex] = true; stop(stepIndex, undefined, false); } maybeAdvanceLowWaterMark(); }); return distributor; } function resolveDistributorOptions(options) { return { distributorTargetBufferSize: options?.distributorTargetBufferSize ?? DEFAULT_DISTRIBUTOR_BUFFER_SIZE, distributorTargetBufferSizeIncrement: options?.distributorTargetBufferSizeIncrement ?? DEFAULT_DISTRIBUTOR_BUFFER_SIZE_INCREMENT, distributorPauseDuration: options?.distributorPauseDuration ?? DEFAULT_DISTRIBUTOR_PAUSE_DURATION, }; } //# sourceMappingURL=distributor.js.map