'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var logger$1 = require('@azure/logger'); // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * When a poller is manually stopped through the `stopPolling` method, * the poller will be rejected with an instance of the PollerStoppedError. */ class PollerStoppedError extends Error { constructor(message) { super(message); this.name = "PollerStoppedError"; Object.setPrototypeOf(this, PollerStoppedError.prototype); } } /** * When a poller is cancelled through the `cancelOperation` method, * the poller will be rejected with an instance of the PollerCancelledError. */ class PollerCancelledError extends Error { constructor(message) { super(message); this.name = "PollerCancelledError"; Object.setPrototypeOf(this, PollerCancelledError.prototype); } } /** * A class that represents the definition of a program that polls through consecutive requests * until it reaches a state of completion. * * A poller can be executed manually, by polling request by request by calling to the `poll()` method repeatedly, until its operation is completed. * It also provides a way to wait until the operation completes, by calling `pollUntilDone()` and waiting until the operation finishes. * Pollers can also request the cancellation of the ongoing process to whom is providing the underlying long running operation. * * ```ts * const poller = new MyPoller(); * * // Polling just once: * await poller.poll(); * * // We can try to cancel the request here, by calling: * // * // await poller.cancelOperation(); * // * * // Getting the final result: * const result = await poller.pollUntilDone(); * ``` * * The Poller is defined by two types, a type representing the state of the poller, which * must include a basic set of properties from `PollOperationState`, * and a return type defined by `TResult`, which can be anything. * * The Poller class implements the `PollerLike` interface, which allows poller implementations to avoid having * to export the Poller's class directly, and instead only export the already instantiated poller with the PollerLike type. * * ```ts * class Client { * public async makePoller: PollerLike { * const poller = new MyPoller({}); * // It might be preferred to return the poller after the first request is made, * // so that some information can be obtained right away. * await poller.poll(); * return poller; * } * } * * const poller: PollerLike = myClient.makePoller(); * ``` * * A poller can be created through its constructor, then it can be polled until it's completed. * At any point in time, the state of the poller can be obtained without delay through the getOperationState method. * At any point in time, the intermediate forms of the result type can be requested without delay. * Once the underlying operation is marked as completed, the poller will stop and the final value will be returned. * * ```ts * const poller = myClient.makePoller(); * const state: MyOperationState = poller.getOperationState(); * * // The intermediate result can be obtained at any time. * const result: MyResult | undefined = poller.getResult(); * * // The final result can only be obtained after the poller finishes. * const result: MyResult = await poller.pollUntilDone(); * ``` * */ // eslint-disable-next-line no-use-before-define class Poller { /** * A poller needs to be initialized by passing in at least the basic properties of the `PollOperation`. * * When writing an implementation of a Poller, this implementation needs to deal with the initialization * of any custom state beyond the basic definition of the poller. The basic poller assumes that the poller's * operation has already been defined, at least its basic properties. The code below shows how to approach * the definition of the constructor of a new custom poller. * * ```ts * export class MyPoller extends Poller { * constructor({ * // Anything you might need outside of the basics * }) { * let state: MyOperationState = { * privateProperty: private, * publicProperty: public, * }; * * const operation = { * state, * update, * cancel, * toString * } * * // Sending the operation to the parent's constructor. * super(operation); * * // You can assign more local properties here. * } * } * ``` * * Inside of this constructor, a new promise is created. This will be used to * tell the user when the poller finishes (see `pollUntilDone()`). The promise's * resolve and reject methods are also used internally to control when to resolve * or reject anyone waiting for the poller to finish. * * The constructor of a custom implementation of a poller is where any serialized version of * a previous poller's operation should be deserialized into the operation sent to the * base constructor. For example: * * ```ts * export class MyPoller extends Poller { * constructor( * baseOperation: string | undefined * ) { * let state: MyOperationState = {}; * if (baseOperation) { * state = { * ...JSON.parse(baseOperation).state, * ...state * }; * } * const operation = { * state, * // ... * } * super(operation); * } * } * ``` * * @param operation - Must contain the basic properties of `PollOperation`. */ constructor(operation) { this.stopped = true; this.pollProgressCallbacks = []; this.operation = operation; this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); // This prevents the UnhandledPromiseRejectionWarning in node.js from being thrown. // The above warning would get thrown if `poller.poll` is called, it returns an error, // and pullUntilDone did not have a .catch or await try/catch on it's return value. this.promise.catch(() => { /* intentionally blank */ }); } /** * Starts a loop that will break only if the poller is done * or if the poller is stopped. */ async startPolling() { if (this.stopped) { this.stopped = false; } while (!this.isStopped() && !this.isDone()) { await this.poll(); await this.delay(); } } /** * pollOnce does one polling, by calling to the update method of the underlying * poll operation to make any relevant change effective. * * It only optionally receives an object with an abortSignal property, from \@azure/abort-controller's AbortSignalLike. * * @param options - Optional properties passed to the operation's update method. */ async pollOnce(options = {}) { try { if (!this.isDone()) { this.operation = await this.operation.update({ abortSignal: options.abortSignal, fireProgress: this.fireProgress.bind(this), }); if (this.isDone() && this.resolve) { // If the poller has finished polling, this means we now have a result. // However, it can be the case that TResult is instantiated to void, so // we are not expecting a result anyway. To assert that we might not // have a result eventually after finishing polling, we cast the result // to TResult. this.resolve(this.operation.state.result); } } } catch (e) { this.operation.state.error = e; if (this.reject) { this.reject(e); } throw e; } } /** * fireProgress calls the functions passed in via onProgress the method of the poller. * * It loops over all of the callbacks received from onProgress, and executes them, sending them * the current operation state. * * @param state - The current operation state. */ fireProgress(state) { for (const callback of this.pollProgressCallbacks) { callback(state); } } /** * Invokes the underlying operation's cancel method, and rejects the * pollUntilDone promise. */ async cancelOnce(options = {}) { this.operation = await this.operation.cancel(options); if (this.reject) { this.reject(new PollerCancelledError("Poller cancelled")); } } /** * Returns a promise that will resolve once a single polling request finishes. * It does this by calling the update method of the Poller's operation. * * It only optionally receives an object with an abortSignal property, from \@azure/abort-controller's AbortSignalLike. * * @param options - Optional properties passed to the operation's update method. */ poll(options = {}) { if (!this.pollOncePromise) { this.pollOncePromise = this.pollOnce(options); const clearPollOncePromise = () => { this.pollOncePromise = undefined; }; this.pollOncePromise.then(clearPollOncePromise, clearPollOncePromise).catch(this.reject); } return this.pollOncePromise; } /** * Returns a promise that will resolve once the underlying operation is completed. */ async pollUntilDone() { if (this.stopped) { this.startPolling().catch(this.reject); } return this.promise; } /** * Invokes the provided callback after each polling is completed, * sending the current state of the poller's operation. * * It returns a method that can be used to stop receiving updates on the given callback function. */ onProgress(callback) { this.pollProgressCallbacks.push(callback); return () => { this.pollProgressCallbacks = this.pollProgressCallbacks.filter((c) => c !== callback); }; } /** * Returns true if the poller has finished polling. */ isDone() { const state = this.operation.state; return Boolean(state.isCompleted || state.isCancelled || state.error); } /** * Stops the poller from continuing to poll. */ stopPolling() { if (!this.stopped) { this.stopped = true; if (this.reject) { this.reject(new PollerStoppedError("This poller is already stopped")); } } } /** * Returns true if the poller is stopped. */ isStopped() { return this.stopped; } /** * Attempts to cancel the underlying operation. * * It only optionally receives an object with an abortSignal property, from \@azure/abort-controller's AbortSignalLike. * * If it's called again before it finishes, it will throw an error. * * @param options - Optional properties passed to the operation's update method. */ cancelOperation(options = {}) { if (!this.stopped) { this.stopped = true; } if (!this.cancelPromise) { this.cancelPromise = this.cancelOnce(options); } else if (options.abortSignal) { throw new Error("A cancel request is currently pending"); } return this.cancelPromise; } /** * Returns the state of the operation. * * Even though TState will be the same type inside any of the methods of any extension of the Poller class, * implementations of the pollers can customize what's shared with the public by writing their own * version of the `getOperationState` method, and by defining two types, one representing the internal state of the poller * and a public type representing a safe to share subset of the properties of the internal state. * Their definition of getOperationState can then return their public type. * * Example: * * ```ts * // Let's say we have our poller's operation state defined as: * interface MyOperationState extends PollOperationState { * privateProperty?: string; * publicProperty?: string; * } * * // To allow us to have a true separation of public and private state, we have to define another interface: * interface PublicState extends PollOperationState { * publicProperty?: string; * } * * // Then, we define our Poller as follows: * export class MyPoller extends Poller { * // ... More content is needed here ... * * public getOperationState(): PublicState { * const state: PublicState = this.operation.state; * return { * // Properties from PollOperationState * isStarted: state.isStarted, * isCompleted: state.isCompleted, * isCancelled: state.isCancelled, * error: state.error, * result: state.result, * * // The only other property needed by PublicState. * publicProperty: state.publicProperty * } * } * } * ``` * * You can see this in the tests of this repository, go to the file: * `../test/utils/testPoller.ts` * and look for the getOperationState implementation. */ getOperationState() { return this.operation.state; } /** * Returns the result value of the operation, * regardless of the state of the poller. * It can return undefined or an incomplete form of the final TResult value * depending on the implementation. */ getResult() { const state = this.operation.state; return state.result; } /** * Returns a serialized version of the poller's operation * by invoking the operation's toString method. */ toString() { return this.operation.toString(); } } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. /** * Detects where the continuation token is and returns it. Notice that azure-asyncoperation * must be checked first before the other location headers because there are scenarios * where both azure-asyncoperation and location could be present in the same response but * azure-asyncoperation should be the one to use for polling. */ function getPollingUrl(rawResponse, defaultPath) { var _a, _b, _c; return ((_c = (_b = (_a = getAzureAsyncOperation(rawResponse)) !== null && _a !== void 0 ? _a : getOperationLocation(rawResponse)) !== null && _b !== void 0 ? _b : getLocation(rawResponse)) !== null && _c !== void 0 ? _c : defaultPath); } function getLocation(rawResponse) { return rawResponse.headers["location"]; } function getOperationLocation(rawResponse) { return rawResponse.headers["operation-location"]; } function getAzureAsyncOperation(rawResponse) { return rawResponse.headers["azure-asyncoperation"]; } function findResourceLocation(requestMethod, rawResponse, requestPath) { switch (requestMethod) { case "PUT": { return requestPath; } case "POST": case "PATCH": { return getLocation(rawResponse); } default: { return undefined; } } } function inferLroMode(requestPath, requestMethod, rawResponse) { if (getAzureAsyncOperation(rawResponse) !== undefined || getOperationLocation(rawResponse) !== undefined) { return { mode: "Location", resourceLocation: findResourceLocation(requestMethod, rawResponse, requestPath), }; } else if (getLocation(rawResponse) !== undefined) { return { mode: "Location", }; } else if (["PUT", "PATCH"].includes(requestMethod)) { return { mode: "Body", }; } return {}; } class SimpleRestError extends Error { constructor(message, statusCode) { super(message); this.name = "RestError"; this.statusCode = statusCode; Object.setPrototypeOf(this, SimpleRestError.prototype); } } function isUnexpectedInitialResponse(rawResponse) { const code = rawResponse.statusCode; if (![203, 204, 202, 201, 200, 500].includes(code)) { throw new SimpleRestError(`Received unexpected HTTP status code ${code} in the initial response. This may indicate a server issue.`, code); } return false; } function isUnexpectedPollingResponse(rawResponse) { const code = rawResponse.statusCode; if (![202, 201, 200, 500].includes(code)) { throw new SimpleRestError(`Received unexpected HTTP status code ${code} while polling. This may indicate a server issue.`, code); } return false; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. const successStates = ["succeeded"]; const failureStates = ["failed", "canceled", "cancelled"]; // Copyright (c) Microsoft Corporation. function getProvisioningState(rawResponse) { var _a, _b; const { properties, provisioningState } = (_a = rawResponse.body) !== null && _a !== void 0 ? _a : {}; const state = (_b = properties === null || properties === void 0 ? void 0 : properties.provisioningState) !== null && _b !== void 0 ? _b : provisioningState; return typeof state === "string" ? state.toLowerCase() : "succeeded"; } function isBodyPollingDone(rawResponse) { const state = getProvisioningState(rawResponse); if (isUnexpectedPollingResponse(rawResponse) || failureStates.includes(state)) { throw new Error(`The long running operation has failed. The provisioning state: ${state}.`); } return successStates.includes(state); } /** * Creates a polling strategy based on BodyPolling which uses the provisioning state * from the result to determine the current operation state */ function processBodyPollingOperationResult(response) { return Object.assign(Object.assign({}, response), { done: isBodyPollingDone(response.rawResponse) }); } // Copyright (c) Microsoft Corporation. /** * The `@azure/logger` configuration for this package. * @internal */ const logger = logger$1.createClientLogger("core-lro"); // Copyright (c) Microsoft Corporation. function isPollingDone(rawResponse) { var _a; if (isUnexpectedPollingResponse(rawResponse) || rawResponse.statusCode === 202) { return false; } const { status } = (_a = rawResponse.body) !== null && _a !== void 0 ? _a : {}; const state = typeof status === "string" ? status.toLowerCase() : "succeeded"; if (isUnexpectedPollingResponse(rawResponse) || failureStates.includes(state)) { throw new Error(`The long running operation has failed. The provisioning state: ${state}.`); } return successStates.includes(state); } /** * Sends a request to the URI of the provisioned resource if needed. */ async function sendFinalRequest(lro, resourceLocation, lroResourceLocationConfig) { switch (lroResourceLocationConfig) { case "original-uri": return lro.sendPollRequest(lro.requestPath); case "azure-async-operation": return undefined; case "location": default: return lro.sendPollRequest(resourceLocation !== null && resourceLocation !== void 0 ? resourceLocation : lro.requestPath); } } function processLocationPollingOperationResult(lro, resourceLocation, lroResourceLocationConfig) { return (response) => { if (isPollingDone(response.rawResponse)) { if (resourceLocation === undefined) { return Object.assign(Object.assign({}, response), { done: true }); } else { return Object.assign(Object.assign({}, response), { done: false, next: async () => { const finalResponse = await sendFinalRequest(lro, resourceLocation, lroResourceLocationConfig); return Object.assign(Object.assign({}, (finalResponse !== null && finalResponse !== void 0 ? finalResponse : response)), { done: true }); } }); } } return Object.assign(Object.assign({}, response), { done: false }); }; } // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. function processPassthroughOperationResult(response) { return Object.assign(Object.assign({}, response), { done: true }); } // Copyright (c) Microsoft Corporation. /** * creates a stepping function that maps an LRO state to another. */ function createGetLroStatusFromResponse(lroPrimitives, config, lroResourceLocationConfig) { switch (config.mode) { case "Location": { return processLocationPollingOperationResult(lroPrimitives, config.resourceLocation, lroResourceLocationConfig); } case "Body": { return processBodyPollingOperationResult; } default: { return processPassthroughOperationResult; } } } /** * Creates a polling operation. */ function createPoll(lroPrimitives) { return async (path, pollerConfig, getLroStatusFromResponse) => { const response = await lroPrimitives.sendPollRequest(path); const retryAfter = response.rawResponse.headers["retry-after"]; if (retryAfter !== undefined) { // Retry-After header value is either in HTTP date format, or in seconds const retryAfterInSeconds = parseInt(retryAfter); pollerConfig.intervalInMs = isNaN(retryAfterInSeconds) ? calculatePollingIntervalFromDate(new Date(retryAfter), pollerConfig.intervalInMs) : retryAfterInSeconds * 1000; } return getLroStatusFromResponse(response); }; } function calculatePollingIntervalFromDate(retryAfterDate, defaultIntervalInMs) { const timeNow = Math.floor(new Date().getTime()); const retryAfterTime = retryAfterDate.getTime(); if (timeNow < retryAfterTime) { return retryAfterTime - timeNow; } return defaultIntervalInMs; } /** * Creates a callback to be used to initialize the polling operation state. * @param state - of the polling operation * @param operationSpec - of the LRO * @param callback - callback to be called when the operation is done * @returns callback that initializes the state of the polling operation */ function createInitializeState(state, requestPath, requestMethod) { return (response) => { if (isUnexpectedInitialResponse(response.rawResponse)) ; state.initialRawResponse = response.rawResponse; state.isStarted = true; state.pollingURL = getPollingUrl(state.initialRawResponse, requestPath); state.config = inferLroMode(requestPath, requestMethod, state.initialRawResponse); /** short circuit polling if body polling is done in the initial request */ if (state.config.mode === undefined || (state.config.mode === "Body" && isBodyPollingDone(state.initialRawResponse))) { state.result = response.flatResponse; state.isCompleted = true; } logger.verbose(`LRO: initial state: ${JSON.stringify(state)}`); return Boolean(state.isCompleted); }; } // Copyright (c) Microsoft Corporation. class GenericPollOperation { constructor(state, lro, lroResourceLocationConfig, processResult, updateState, isDone) { this.state = state; this.lro = lro; this.lroResourceLocationConfig = lroResourceLocationConfig; this.processResult = processResult; this.updateState = updateState; this.isDone = isDone; } setPollerConfig(pollerConfig) { this.pollerConfig = pollerConfig; } /** * General update function for LROPoller, the general process is as follows * 1. Check initial operation result to determine the strategy to use * - Strategies: Location, Azure-AsyncOperation, Original Uri * 2. Check if the operation result has a terminal state * - Terminal state will be determined by each strategy * 2.1 If it is terminal state Check if a final GET request is required, if so * send final GET request and return result from operation. If no final GET * is required, just return the result from operation. * - Determining what to call for final request is responsibility of each strategy * 2.2 If it is not terminal state, call the polling operation and go to step 1 * - Determining what to call for polling is responsibility of each strategy * - Strategies will always use the latest URI for polling if provided otherwise * the last known one */ async update(options) { var _a, _b, _c; const state = this.state; let lastResponse = undefined; if (!state.isStarted) { const initializeState = createInitializeState(state, this.lro.requestPath, this.lro.requestMethod); lastResponse = await this.lro.sendInitialRequest(); initializeState(lastResponse); } if (!state.isCompleted) { if (!this.poll || !this.getLroStatusFromResponse) { if (!state.config) { throw new Error("Bad state: LRO mode is undefined. Please check if the serialized state is well-formed."); } const isDone = this.isDone; this.getLroStatusFromResponse = isDone ? (response) => (Object.assign(Object.assign({}, response), { done: isDone(response.flatResponse, this.state) })) : createGetLroStatusFromResponse(this.lro, state.config, this.lroResourceLocationConfig); this.poll = createPoll(this.lro); } if (!state.pollingURL) { throw new Error("Bad state: polling URL is undefined. Please check if the serialized state is well-formed."); } const currentState = await this.poll(state.pollingURL, this.pollerConfig, this.getLroStatusFromResponse); logger.verbose(`LRO: polling response: ${JSON.stringify(currentState.rawResponse)}`); if (currentState.done) { state.result = this.processResult ? this.processResult(currentState.flatResponse, state) : currentState.flatResponse; state.isCompleted = true; } else { this.poll = (_a = currentState.next) !== null && _a !== void 0 ? _a : this.poll; state.pollingURL = getPollingUrl(currentState.rawResponse, state.pollingURL); } lastResponse = currentState; } logger.verbose(`LRO: current state: ${JSON.stringify(state)}`); if (lastResponse) { (_b = this.updateState) === null || _b === void 0 ? void 0 : _b.call(this, state, lastResponse === null || lastResponse === void 0 ? void 0 : lastResponse.rawResponse); } else { logger.error(`LRO: no response was received`); } (_c = options === null || options === void 0 ? void 0 : options.fireProgress) === null || _c === void 0 ? void 0 : _c.call(options, state); return this; } async cancel() { this.state.isCancelled = true; return this; } /** * Serializes the Poller operation. */ toString() { return JSON.stringify({ state: this.state, }); } } // Copyright (c) Microsoft Corporation. function deserializeState(serializedState) { try { return JSON.parse(serializedState).state; } catch (e) { throw new Error(`LroEngine: Unable to deserialize state: ${serializedState}`); } } /** * The LRO Engine, a class that performs polling. */ class LroEngine extends Poller { constructor(lro, options) { const { intervalInMs = 2000, resumeFrom } = options || {}; const state = resumeFrom ? deserializeState(resumeFrom) : {}; const operation = new GenericPollOperation(state, lro, options === null || options === void 0 ? void 0 : options.lroResourceLocationConfig, options === null || options === void 0 ? void 0 : options.processResult, options === null || options === void 0 ? void 0 : options.updateState, options === null || options === void 0 ? void 0 : options.isDone); super(operation); this.config = { intervalInMs: intervalInMs }; operation.setPollerConfig(this.config); } /** * The method used by the poller to wait before attempting to update its operation. */ delay() { return new Promise((resolve) => setTimeout(() => resolve(), this.config.intervalInMs)); } } exports.LroEngine = LroEngine; exports.Poller = Poller; exports.PollerCancelledError = PollerCancelledError; exports.PollerStoppedError = PollerStoppedError; //# sourceMappingURL=index.js.map