mirror of
https://github.com/oven-sh/setup-bun.git
synced 2025-02-24 03:00:39 +08:00
1172 lines
44 KiB
JavaScript
1172 lines
44 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
var logger$1 = require('@azure/logger');
|
|
var abortController = require('@azure/abort-controller');
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
/**
|
|
* The `@azure/logger` configuration for this package.
|
|
* @internal
|
|
*/
|
|
const logger = logger$1.createClientLogger("core-lro");
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
/**
|
|
* The default time interval to wait before sending the next polling request.
|
|
*/
|
|
const POLL_INTERVAL_IN_MS = 2000;
|
|
/**
|
|
* The closed set of terminal states.
|
|
*/
|
|
const terminalStates = ["succeeded", "canceled", "failed"];
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
/**
|
|
* Deserializes the state
|
|
*/
|
|
function deserializeState(serializedState) {
|
|
try {
|
|
return JSON.parse(serializedState).state;
|
|
}
|
|
catch (e) {
|
|
throw new Error(`Unable to deserialize input state: ${serializedState}`);
|
|
}
|
|
}
|
|
function setStateError(inputs) {
|
|
const { state, stateProxy, isOperationError } = inputs;
|
|
return (error) => {
|
|
if (isOperationError(error)) {
|
|
stateProxy.setError(state, error);
|
|
stateProxy.setFailed(state);
|
|
}
|
|
throw error;
|
|
};
|
|
}
|
|
function processOperationStatus(result) {
|
|
const { state, stateProxy, status, isDone, processResult, response, setErrorAsResult } = result;
|
|
switch (status) {
|
|
case "succeeded": {
|
|
stateProxy.setSucceeded(state);
|
|
break;
|
|
}
|
|
case "failed": {
|
|
stateProxy.setError(state, new Error(`The long-running operation has failed`));
|
|
stateProxy.setFailed(state);
|
|
break;
|
|
}
|
|
case "canceled": {
|
|
stateProxy.setCanceled(state);
|
|
break;
|
|
}
|
|
}
|
|
if ((isDone === null || isDone === void 0 ? void 0 : isDone(response, state)) ||
|
|
(isDone === undefined &&
|
|
["succeeded", "canceled"].concat(setErrorAsResult ? [] : ["failed"]).includes(status))) {
|
|
stateProxy.setResult(state, buildResult({
|
|
response,
|
|
state,
|
|
processResult,
|
|
}));
|
|
}
|
|
}
|
|
function buildResult(inputs) {
|
|
const { processResult, response, state } = inputs;
|
|
return processResult ? processResult(response, state) : response;
|
|
}
|
|
/**
|
|
* Initiates the long-running operation.
|
|
*/
|
|
async function initOperation(inputs) {
|
|
const { init, stateProxy, processResult, getOperationStatus, withOperationLocation, setErrorAsResult, } = inputs;
|
|
const { operationLocation, resourceLocation, metadata, response } = await init();
|
|
if (operationLocation)
|
|
withOperationLocation === null || withOperationLocation === void 0 ? void 0 : withOperationLocation(operationLocation, false);
|
|
const config = {
|
|
metadata,
|
|
operationLocation,
|
|
resourceLocation,
|
|
};
|
|
logger.verbose(`LRO: Operation description:`, config);
|
|
const state = stateProxy.initState(config);
|
|
const status = getOperationStatus({ response, state, operationLocation });
|
|
processOperationStatus({ state, status, stateProxy, response, setErrorAsResult, processResult });
|
|
return state;
|
|
}
|
|
async function pollOperationHelper(inputs) {
|
|
const { poll, state, stateProxy, operationLocation, getOperationStatus, getResourceLocation, isOperationError, options, } = inputs;
|
|
const response = await poll(operationLocation, options).catch(setStateError({
|
|
state,
|
|
stateProxy,
|
|
isOperationError,
|
|
}));
|
|
const status = getOperationStatus(response, state);
|
|
logger.verbose(`LRO: Status:\n\tPolling from: ${state.config.operationLocation}\n\tOperation status: ${status}\n\tPolling status: ${terminalStates.includes(status) ? "Stopped" : "Running"}`);
|
|
if (status === "succeeded") {
|
|
const resourceLocation = getResourceLocation(response, state);
|
|
if (resourceLocation !== undefined) {
|
|
return {
|
|
response: await poll(resourceLocation).catch(setStateError({ state, stateProxy, isOperationError })),
|
|
status,
|
|
};
|
|
}
|
|
}
|
|
return { response, status };
|
|
}
|
|
/** Polls the long-running operation. */
|
|
async function pollOperation(inputs) {
|
|
const { poll, state, stateProxy, options, getOperationStatus, getResourceLocation, getOperationLocation, isOperationError, withOperationLocation, getPollingInterval, processResult, updateState, setDelay, isDone, setErrorAsResult, } = inputs;
|
|
const { operationLocation } = state.config;
|
|
if (operationLocation !== undefined) {
|
|
const { response, status } = await pollOperationHelper({
|
|
poll,
|
|
getOperationStatus,
|
|
state,
|
|
stateProxy,
|
|
operationLocation,
|
|
getResourceLocation,
|
|
isOperationError,
|
|
options,
|
|
});
|
|
processOperationStatus({
|
|
status,
|
|
response,
|
|
state,
|
|
stateProxy,
|
|
isDone,
|
|
processResult,
|
|
setErrorAsResult,
|
|
});
|
|
if (!terminalStates.includes(status)) {
|
|
const intervalInMs = getPollingInterval === null || getPollingInterval === void 0 ? void 0 : getPollingInterval(response);
|
|
if (intervalInMs)
|
|
setDelay(intervalInMs);
|
|
const location = getOperationLocation === null || getOperationLocation === void 0 ? void 0 : getOperationLocation(response, state);
|
|
if (location !== undefined) {
|
|
const isUpdated = operationLocation !== location;
|
|
state.config.operationLocation = location;
|
|
withOperationLocation === null || withOperationLocation === void 0 ? void 0 : withOperationLocation(location, isUpdated);
|
|
}
|
|
else
|
|
withOperationLocation === null || withOperationLocation === void 0 ? void 0 : withOperationLocation(operationLocation, false);
|
|
}
|
|
updateState === null || updateState === void 0 ? void 0 : updateState(state, response);
|
|
}
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
function getOperationLocationPollingUrl(inputs) {
|
|
const { azureAsyncOperation, operationLocation } = inputs;
|
|
return operationLocation !== null && operationLocation !== void 0 ? operationLocation : azureAsyncOperation;
|
|
}
|
|
function getLocationHeader(rawResponse) {
|
|
return rawResponse.headers["location"];
|
|
}
|
|
function getOperationLocationHeader(rawResponse) {
|
|
return rawResponse.headers["operation-location"];
|
|
}
|
|
function getAzureAsyncOperationHeader(rawResponse) {
|
|
return rawResponse.headers["azure-asyncoperation"];
|
|
}
|
|
function findResourceLocation(inputs) {
|
|
const { location, requestMethod, requestPath, resourceLocationConfig } = inputs;
|
|
switch (requestMethod) {
|
|
case "PUT": {
|
|
return requestPath;
|
|
}
|
|
case "DELETE": {
|
|
return undefined;
|
|
}
|
|
default: {
|
|
switch (resourceLocationConfig) {
|
|
case "azure-async-operation": {
|
|
return undefined;
|
|
}
|
|
case "original-uri": {
|
|
return requestPath;
|
|
}
|
|
case "location":
|
|
default: {
|
|
return location;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function inferLroMode(inputs) {
|
|
const { rawResponse, requestMethod, requestPath, resourceLocationConfig } = inputs;
|
|
const operationLocation = getOperationLocationHeader(rawResponse);
|
|
const azureAsyncOperation = getAzureAsyncOperationHeader(rawResponse);
|
|
const pollingUrl = getOperationLocationPollingUrl({ operationLocation, azureAsyncOperation });
|
|
const location = getLocationHeader(rawResponse);
|
|
const normalizedRequestMethod = requestMethod === null || requestMethod === void 0 ? void 0 : requestMethod.toLocaleUpperCase();
|
|
if (pollingUrl !== undefined) {
|
|
return {
|
|
mode: "OperationLocation",
|
|
operationLocation: pollingUrl,
|
|
resourceLocation: findResourceLocation({
|
|
requestMethod: normalizedRequestMethod,
|
|
location,
|
|
requestPath,
|
|
resourceLocationConfig,
|
|
}),
|
|
};
|
|
}
|
|
else if (location !== undefined) {
|
|
return {
|
|
mode: "ResourceLocation",
|
|
operationLocation: location,
|
|
};
|
|
}
|
|
else if (normalizedRequestMethod === "PUT" && requestPath) {
|
|
return {
|
|
mode: "Body",
|
|
operationLocation: requestPath,
|
|
};
|
|
}
|
|
else {
|
|
return undefined;
|
|
}
|
|
}
|
|
function transformStatus(inputs) {
|
|
const { status, statusCode } = inputs;
|
|
if (typeof status !== "string" && status !== undefined) {
|
|
throw new Error(`Polling was unsuccessful. Expected status to have a string value or no value but it has instead: ${status}. This doesn't necessarily indicate the operation has failed. Check your Azure subscription or resource status for more information.`);
|
|
}
|
|
switch (status === null || status === void 0 ? void 0 : status.toLocaleLowerCase()) {
|
|
case undefined:
|
|
return toOperationStatus(statusCode);
|
|
case "succeeded":
|
|
return "succeeded";
|
|
case "failed":
|
|
return "failed";
|
|
case "running":
|
|
case "accepted":
|
|
case "started":
|
|
case "canceling":
|
|
case "cancelling":
|
|
return "running";
|
|
case "canceled":
|
|
case "cancelled":
|
|
return "canceled";
|
|
default: {
|
|
logger.warning(`LRO: unrecognized operation status: ${status}`);
|
|
return status;
|
|
}
|
|
}
|
|
}
|
|
function getStatus(rawResponse) {
|
|
var _a;
|
|
const { status } = (_a = rawResponse.body) !== null && _a !== void 0 ? _a : {};
|
|
return transformStatus({ status, statusCode: rawResponse.statusCode });
|
|
}
|
|
function getProvisioningState(rawResponse) {
|
|
var _a, _b;
|
|
const { properties, provisioningState } = (_a = rawResponse.body) !== null && _a !== void 0 ? _a : {};
|
|
const status = (_b = properties === null || properties === void 0 ? void 0 : properties.provisioningState) !== null && _b !== void 0 ? _b : provisioningState;
|
|
return transformStatus({ status, statusCode: rawResponse.statusCode });
|
|
}
|
|
function toOperationStatus(statusCode) {
|
|
if (statusCode === 202) {
|
|
return "running";
|
|
}
|
|
else if (statusCode < 300) {
|
|
return "succeeded";
|
|
}
|
|
else {
|
|
return "failed";
|
|
}
|
|
}
|
|
function parseRetryAfter({ rawResponse }) {
|
|
const retryAfter = rawResponse.headers["retry-after"];
|
|
if (retryAfter !== undefined) {
|
|
// Retry-After header value is either in HTTP date format, or in seconds
|
|
const retryAfterInSeconds = parseInt(retryAfter);
|
|
return isNaN(retryAfterInSeconds)
|
|
? calculatePollingIntervalFromDate(new Date(retryAfter))
|
|
: retryAfterInSeconds * 1000;
|
|
}
|
|
return undefined;
|
|
}
|
|
function calculatePollingIntervalFromDate(retryAfterDate) {
|
|
const timeNow = Math.floor(new Date().getTime());
|
|
const retryAfterTime = retryAfterDate.getTime();
|
|
if (timeNow < retryAfterTime) {
|
|
return retryAfterTime - timeNow;
|
|
}
|
|
return undefined;
|
|
}
|
|
function getStatusFromInitialResponse(inputs) {
|
|
const { response, state, operationLocation } = inputs;
|
|
function helper() {
|
|
var _a;
|
|
const mode = (_a = state.config.metadata) === null || _a === void 0 ? void 0 : _a["mode"];
|
|
switch (mode) {
|
|
case undefined:
|
|
return toOperationStatus(response.rawResponse.statusCode);
|
|
case "Body":
|
|
return getOperationStatus(response, state);
|
|
default:
|
|
return "running";
|
|
}
|
|
}
|
|
const status = helper();
|
|
return status === "running" && operationLocation === undefined ? "succeeded" : status;
|
|
}
|
|
/**
|
|
* Initiates the long-running operation.
|
|
*/
|
|
async function initHttpOperation(inputs) {
|
|
const { stateProxy, resourceLocationConfig, processResult, lro, setErrorAsResult } = inputs;
|
|
return initOperation({
|
|
init: async () => {
|
|
const response = await lro.sendInitialRequest();
|
|
const config = inferLroMode({
|
|
rawResponse: response.rawResponse,
|
|
requestPath: lro.requestPath,
|
|
requestMethod: lro.requestMethod,
|
|
resourceLocationConfig,
|
|
});
|
|
return Object.assign({ response, operationLocation: config === null || config === void 0 ? void 0 : config.operationLocation, resourceLocation: config === null || config === void 0 ? void 0 : config.resourceLocation }, ((config === null || config === void 0 ? void 0 : config.mode) ? { metadata: { mode: config.mode } } : {}));
|
|
},
|
|
stateProxy,
|
|
processResult: processResult
|
|
? ({ flatResponse }, state) => processResult(flatResponse, state)
|
|
: ({ flatResponse }) => flatResponse,
|
|
getOperationStatus: getStatusFromInitialResponse,
|
|
setErrorAsResult,
|
|
});
|
|
}
|
|
function getOperationLocation({ rawResponse }, state) {
|
|
var _a;
|
|
const mode = (_a = state.config.metadata) === null || _a === void 0 ? void 0 : _a["mode"];
|
|
switch (mode) {
|
|
case "OperationLocation": {
|
|
return getOperationLocationPollingUrl({
|
|
operationLocation: getOperationLocationHeader(rawResponse),
|
|
azureAsyncOperation: getAzureAsyncOperationHeader(rawResponse),
|
|
});
|
|
}
|
|
case "ResourceLocation": {
|
|
return getLocationHeader(rawResponse);
|
|
}
|
|
case "Body":
|
|
default: {
|
|
return undefined;
|
|
}
|
|
}
|
|
}
|
|
function getOperationStatus({ rawResponse }, state) {
|
|
var _a;
|
|
const mode = (_a = state.config.metadata) === null || _a === void 0 ? void 0 : _a["mode"];
|
|
switch (mode) {
|
|
case "OperationLocation": {
|
|
return getStatus(rawResponse);
|
|
}
|
|
case "ResourceLocation": {
|
|
return toOperationStatus(rawResponse.statusCode);
|
|
}
|
|
case "Body": {
|
|
return getProvisioningState(rawResponse);
|
|
}
|
|
default:
|
|
throw new Error(`Internal error: Unexpected operation mode: ${mode}`);
|
|
}
|
|
}
|
|
function getResourceLocation({ flatResponse }, state) {
|
|
if (typeof flatResponse === "object") {
|
|
const resourceLocation = flatResponse.resourceLocation;
|
|
if (resourceLocation !== undefined) {
|
|
state.config.resourceLocation = resourceLocation;
|
|
}
|
|
}
|
|
return state.config.resourceLocation;
|
|
}
|
|
function isOperationError(e) {
|
|
return e.name === "RestError";
|
|
}
|
|
/** Polls the long-running operation. */
|
|
async function pollHttpOperation(inputs) {
|
|
const { lro, stateProxy, options, processResult, updateState, setDelay, state, setErrorAsResult, } = inputs;
|
|
return pollOperation({
|
|
state,
|
|
stateProxy,
|
|
setDelay,
|
|
processResult: processResult
|
|
? ({ flatResponse }, inputState) => processResult(flatResponse, inputState)
|
|
: ({ flatResponse }) => flatResponse,
|
|
updateState,
|
|
getPollingInterval: parseRetryAfter,
|
|
getOperationLocation,
|
|
getOperationStatus,
|
|
isOperationError,
|
|
getResourceLocation,
|
|
options,
|
|
/**
|
|
* The expansion here is intentional because `lro` could be an object that
|
|
* references an inner this, so we need to preserve a reference to it.
|
|
*/
|
|
poll: async (location, inputOptions) => lro.sendPollRequest(location, inputOptions),
|
|
setErrorAsResult,
|
|
});
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
/**
|
|
* Map an optional value through a function
|
|
* @internal
|
|
*/
|
|
const maybemap = (value, f) => value === undefined ? undefined : f(value);
|
|
const INTERRUPTED = new Error("The poller is already stopped");
|
|
/**
|
|
* A promise that delays resolution until a certain amount of time (in milliseconds) has passed, with facilities for
|
|
* robust cancellation.
|
|
*
|
|
* ### Example:
|
|
*
|
|
* ```javascript
|
|
* let toCancel;
|
|
*
|
|
* // Wait 20 seconds, and optionally allow the function to be cancelled.
|
|
* await delayMs(20000, (cancel) => { toCancel = cancel });
|
|
*
|
|
* // ... if `toCancel` is called before the 20 second timer expires, then the delayMs promise will reject.
|
|
* ```
|
|
*
|
|
* @internal
|
|
* @param ms - the number of milliseconds to wait before resolving
|
|
* @param cb - a callback that can provide the caller with a cancellation function
|
|
*/
|
|
function delayMs(ms) {
|
|
let aborted = false;
|
|
let toReject;
|
|
return Object.assign(new Promise((resolve, reject) => {
|
|
let token;
|
|
toReject = () => {
|
|
maybemap(token, clearTimeout);
|
|
reject(INTERRUPTED);
|
|
};
|
|
// In the rare case that the operation is _already_ aborted, we will reject instantly. This could happen, for
|
|
// example, if the user calls the cancellation function immediately without yielding execution.
|
|
if (aborted) {
|
|
toReject();
|
|
}
|
|
else {
|
|
token = setTimeout(resolve, ms);
|
|
}
|
|
}), {
|
|
cancel: () => {
|
|
aborted = true;
|
|
toReject === null || toReject === void 0 ? void 0 : toReject();
|
|
},
|
|
});
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
const createStateProxy$1 = () => ({
|
|
/**
|
|
* The state at this point is created to be of type OperationState<TResult>.
|
|
* It will be updated later to be of type TState when the
|
|
* customer-provided callback, `updateState`, is called during polling.
|
|
*/
|
|
initState: (config) => ({ status: "running", config }),
|
|
setCanceled: (state) => (state.status = "canceled"),
|
|
setError: (state, error) => (state.error = error),
|
|
setResult: (state, result) => (state.result = result),
|
|
setRunning: (state) => (state.status = "running"),
|
|
setSucceeded: (state) => (state.status = "succeeded"),
|
|
setFailed: (state) => (state.status = "failed"),
|
|
getError: (state) => state.error,
|
|
getResult: (state) => state.result,
|
|
isCanceled: (state) => state.status === "canceled",
|
|
isFailed: (state) => state.status === "failed",
|
|
isRunning: (state) => state.status === "running",
|
|
isSucceeded: (state) => state.status === "succeeded",
|
|
});
|
|
/**
|
|
* Returns a poller factory.
|
|
*/
|
|
function buildCreatePoller(inputs) {
|
|
const { getOperationLocation, getStatusFromInitialResponse, getStatusFromPollResponse, isOperationError, getResourceLocation, getPollingInterval, resolveOnUnsuccessful, } = inputs;
|
|
return async ({ init, poll }, options) => {
|
|
const { processResult, updateState, withOperationLocation: withOperationLocationCallback, intervalInMs = POLL_INTERVAL_IN_MS, restoreFrom, } = options || {};
|
|
const stateProxy = createStateProxy$1();
|
|
const withOperationLocation = withOperationLocationCallback
|
|
? (() => {
|
|
let called = false;
|
|
return (operationLocation, isUpdated) => {
|
|
if (isUpdated)
|
|
withOperationLocationCallback(operationLocation);
|
|
else if (!called)
|
|
withOperationLocationCallback(operationLocation);
|
|
called = true;
|
|
};
|
|
})()
|
|
: undefined;
|
|
const state = restoreFrom
|
|
? deserializeState(restoreFrom)
|
|
: await initOperation({
|
|
init,
|
|
stateProxy,
|
|
processResult,
|
|
getOperationStatus: getStatusFromInitialResponse,
|
|
withOperationLocation,
|
|
setErrorAsResult: !resolveOnUnsuccessful,
|
|
});
|
|
let resultPromise;
|
|
let cancelJob;
|
|
const abortController$1 = new abortController.AbortController();
|
|
const handlers = new Map();
|
|
const handleProgressEvents = async () => handlers.forEach((h) => h(state));
|
|
const cancelErrMsg = "Operation was canceled";
|
|
let currentPollIntervalInMs = intervalInMs;
|
|
const poller = {
|
|
getOperationState: () => state,
|
|
getResult: () => state.result,
|
|
isDone: () => ["succeeded", "failed", "canceled"].includes(state.status),
|
|
isStopped: () => resultPromise === undefined,
|
|
stopPolling: () => {
|
|
abortController$1.abort();
|
|
cancelJob === null || cancelJob === void 0 ? void 0 : cancelJob();
|
|
},
|
|
toString: () => JSON.stringify({
|
|
state,
|
|
}),
|
|
onProgress: (callback) => {
|
|
const s = Symbol();
|
|
handlers.set(s, callback);
|
|
return () => handlers.delete(s);
|
|
},
|
|
pollUntilDone: (pollOptions) => (resultPromise !== null && resultPromise !== void 0 ? resultPromise : (resultPromise = (async () => {
|
|
const { abortSignal: inputAbortSignal } = pollOptions || {};
|
|
const { signal: abortSignal } = inputAbortSignal
|
|
? new abortController.AbortController([inputAbortSignal, abortController$1.signal])
|
|
: abortController$1;
|
|
if (!poller.isDone()) {
|
|
await poller.poll({ abortSignal });
|
|
while (!poller.isDone()) {
|
|
const delay = delayMs(currentPollIntervalInMs);
|
|
cancelJob = delay.cancel;
|
|
await delay;
|
|
await poller.poll({ abortSignal });
|
|
}
|
|
}
|
|
if (resolveOnUnsuccessful) {
|
|
return poller.getResult();
|
|
}
|
|
else {
|
|
switch (state.status) {
|
|
case "succeeded":
|
|
return poller.getResult();
|
|
case "canceled":
|
|
throw new Error(cancelErrMsg);
|
|
case "failed":
|
|
throw state.error;
|
|
case "notStarted":
|
|
case "running":
|
|
throw new Error(`Polling completed without succeeding or failing`);
|
|
}
|
|
}
|
|
})().finally(() => {
|
|
resultPromise = undefined;
|
|
}))),
|
|
async poll(pollOptions) {
|
|
if (resolveOnUnsuccessful) {
|
|
if (poller.isDone())
|
|
return;
|
|
}
|
|
else {
|
|
switch (state.status) {
|
|
case "succeeded":
|
|
return;
|
|
case "canceled":
|
|
throw new Error(cancelErrMsg);
|
|
case "failed":
|
|
throw state.error;
|
|
}
|
|
}
|
|
await pollOperation({
|
|
poll,
|
|
state,
|
|
stateProxy,
|
|
getOperationLocation,
|
|
isOperationError,
|
|
withOperationLocation,
|
|
getPollingInterval,
|
|
getOperationStatus: getStatusFromPollResponse,
|
|
getResourceLocation,
|
|
processResult,
|
|
updateState,
|
|
options: pollOptions,
|
|
setDelay: (pollIntervalInMs) => {
|
|
currentPollIntervalInMs = pollIntervalInMs;
|
|
},
|
|
setErrorAsResult: !resolveOnUnsuccessful,
|
|
});
|
|
await handleProgressEvents();
|
|
if (!resolveOnUnsuccessful) {
|
|
switch (state.status) {
|
|
case "canceled":
|
|
throw new Error(cancelErrMsg);
|
|
case "failed":
|
|
throw state.error;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
return poller;
|
|
};
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
/**
|
|
* Creates a poller that can be used to poll a long-running operation.
|
|
* @param lro - Description of the long-running operation
|
|
* @param options - options to configure the poller
|
|
* @returns an initialized poller
|
|
*/
|
|
async function createHttpPoller(lro, options) {
|
|
const { resourceLocationConfig, intervalInMs, processResult, restoreFrom, updateState, withOperationLocation, resolveOnUnsuccessful = false, } = options || {};
|
|
return buildCreatePoller({
|
|
getStatusFromInitialResponse,
|
|
getStatusFromPollResponse: getOperationStatus,
|
|
isOperationError,
|
|
getOperationLocation,
|
|
getResourceLocation,
|
|
getPollingInterval: parseRetryAfter,
|
|
resolveOnUnsuccessful,
|
|
})({
|
|
init: async () => {
|
|
const response = await lro.sendInitialRequest();
|
|
const config = inferLroMode({
|
|
rawResponse: response.rawResponse,
|
|
requestPath: lro.requestPath,
|
|
requestMethod: lro.requestMethod,
|
|
resourceLocationConfig,
|
|
});
|
|
return Object.assign({ response, operationLocation: config === null || config === void 0 ? void 0 : config.operationLocation, resourceLocation: config === null || config === void 0 ? void 0 : config.resourceLocation }, ((config === null || config === void 0 ? void 0 : config.mode) ? { metadata: { mode: config.mode } } : {}));
|
|
},
|
|
poll: lro.sendPollRequest,
|
|
}, {
|
|
intervalInMs,
|
|
withOperationLocation,
|
|
restoreFrom,
|
|
updateState,
|
|
processResult: processResult
|
|
? ({ flatResponse }, state) => processResult(flatResponse, state)
|
|
: ({ flatResponse }) => flatResponse,
|
|
});
|
|
}
|
|
|
|
// Copyright (c) Microsoft Corporation.
|
|
const createStateProxy = () => ({
|
|
initState: (config) => ({ config, isStarted: true }),
|
|
setCanceled: (state) => (state.isCancelled = true),
|
|
setError: (state, error) => (state.error = error),
|
|
setResult: (state, result) => (state.result = result),
|
|
setRunning: (state) => (state.isStarted = true),
|
|
setSucceeded: (state) => (state.isCompleted = true),
|
|
setFailed: () => {
|
|
/** empty body */
|
|
},
|
|
getError: (state) => state.error,
|
|
getResult: (state) => state.result,
|
|
isCanceled: (state) => !!state.isCancelled,
|
|
isFailed: (state) => !!state.error,
|
|
isRunning: (state) => !!state.isStarted,
|
|
isSucceeded: (state) => Boolean(state.isCompleted && !state.isCancelled && !state.error),
|
|
});
|
|
class GenericPollOperation {
|
|
constructor(state, lro, setErrorAsResult, lroResourceLocationConfig, processResult, updateState, isDone) {
|
|
this.state = state;
|
|
this.lro = lro;
|
|
this.setErrorAsResult = setErrorAsResult;
|
|
this.lroResourceLocationConfig = lroResourceLocationConfig;
|
|
this.processResult = processResult;
|
|
this.updateState = updateState;
|
|
this.isDone = isDone;
|
|
}
|
|
setPollerConfig(pollerConfig) {
|
|
this.pollerConfig = pollerConfig;
|
|
}
|
|
async update(options) {
|
|
var _a;
|
|
const stateProxy = createStateProxy();
|
|
if (!this.state.isStarted) {
|
|
this.state = Object.assign(Object.assign({}, this.state), (await initHttpOperation({
|
|
lro: this.lro,
|
|
stateProxy,
|
|
resourceLocationConfig: this.lroResourceLocationConfig,
|
|
processResult: this.processResult,
|
|
setErrorAsResult: this.setErrorAsResult,
|
|
})));
|
|
}
|
|
const updateState = this.updateState;
|
|
const isDone = this.isDone;
|
|
if (!this.state.isCompleted && this.state.error === undefined) {
|
|
await pollHttpOperation({
|
|
lro: this.lro,
|
|
state: this.state,
|
|
stateProxy,
|
|
processResult: this.processResult,
|
|
updateState: updateState
|
|
? (state, { rawResponse }) => updateState(state, rawResponse)
|
|
: undefined,
|
|
isDone: isDone
|
|
? ({ flatResponse }, state) => isDone(flatResponse, state)
|
|
: undefined,
|
|
options,
|
|
setDelay: (intervalInMs) => {
|
|
this.pollerConfig.intervalInMs = intervalInMs;
|
|
},
|
|
setErrorAsResult: this.setErrorAsResult,
|
|
});
|
|
}
|
|
(_a = options === null || options === void 0 ? void 0 : options.fireProgress) === null || _a === void 0 ? void 0 : _a.call(options, this.state);
|
|
return this;
|
|
}
|
|
async cancel() {
|
|
logger.error("`cancelOperation` is deprecated because it wasn't implemented");
|
|
return this;
|
|
}
|
|
/**
|
|
* Serializes the Poller operation.
|
|
*/
|
|
toString() {
|
|
return JSON.stringify({
|
|
state: this.state,
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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 the operation is cancelled, 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<TResult>`,
|
|
* 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<MyOperationState, MyResult> {
|
|
* 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<MyOperationState, MyResult> = 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<TState, TResult>`.
|
|
*
|
|
* 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<MyOperationState, string> {
|
|
* 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<MyOperationState, string> {
|
|
* 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<State, TResult>`.
|
|
*/
|
|
constructor(operation) {
|
|
/** controls whether to throw an error if the operation failed or was canceled. */
|
|
this.resolveOnUnsuccessful = false;
|
|
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(pollOptions = {}) {
|
|
if (this.stopped) {
|
|
this.stopped = false;
|
|
}
|
|
while (!this.isStopped() && !this.isDone()) {
|
|
await this.poll(pollOptions);
|
|
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 = {}) {
|
|
if (!this.isDone()) {
|
|
this.operation = await this.operation.update({
|
|
abortSignal: options.abortSignal,
|
|
fireProgress: this.fireProgress.bind(this),
|
|
});
|
|
}
|
|
this.processUpdatedState();
|
|
}
|
|
/**
|
|
* 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.
|
|
*/
|
|
async cancelOnce(options = {}) {
|
|
this.operation = await this.operation.cancel(options);
|
|
}
|
|
/**
|
|
* 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;
|
|
}
|
|
processUpdatedState() {
|
|
if (this.operation.state.error) {
|
|
this.stopped = true;
|
|
if (!this.resolveOnUnsuccessful) {
|
|
this.reject(this.operation.state.error);
|
|
throw this.operation.state.error;
|
|
}
|
|
}
|
|
if (this.operation.state.isCancelled) {
|
|
this.stopped = true;
|
|
if (!this.resolveOnUnsuccessful) {
|
|
const error = new PollerCancelledError("Operation was canceled");
|
|
this.reject(error);
|
|
throw error;
|
|
}
|
|
}
|
|
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.getResult());
|
|
}
|
|
}
|
|
/**
|
|
* Returns a promise that will resolve once the underlying operation is completed.
|
|
*/
|
|
async pollUntilDone(pollOptions = {}) {
|
|
if (this.stopped) {
|
|
this.startPolling(pollOptions).catch(this.reject);
|
|
}
|
|
// This is needed because the state could have been updated by
|
|
// `cancelOperation`, e.g. the operation is canceled or an error occurred.
|
|
this.processUpdatedState();
|
|
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.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<ResultType> {
|
|
* 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<ResultType> {
|
|
* publicProperty?: string;
|
|
* }
|
|
*
|
|
* // Then, we define our Poller as follows:
|
|
* export class MyPoller extends Poller<MyOperationState, ResultType> {
|
|
* // ... More content is needed here ...
|
|
*
|
|
* public getOperationState(): PublicState {
|
|
* const state: PublicState = this.operation.state;
|
|
* return {
|
|
* // Properties from PollOperationState<TResult>
|
|
* 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.
|
|
/**
|
|
* The LRO Engine, a class that performs polling.
|
|
*/
|
|
class LroEngine extends Poller {
|
|
constructor(lro, options) {
|
|
const { intervalInMs = POLL_INTERVAL_IN_MS, resumeFrom, resolveOnUnsuccessful = false, isDone, lroResourceLocationConfig, processResult, updateState, } = options || {};
|
|
const state = resumeFrom
|
|
? deserializeState(resumeFrom)
|
|
: {};
|
|
const operation = new GenericPollOperation(state, lro, !resolveOnUnsuccessful, lroResourceLocationConfig, processResult, updateState, isDone);
|
|
super(operation);
|
|
this.resolveOnUnsuccessful = resolveOnUnsuccessful;
|
|
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;
|
|
exports.createHttpPoller = createHttpPoller;
|
|
//# sourceMappingURL=index.js.map
|