UNPKG

@eclipse-emfcloud/model-service-theia

Version:
173 lines 6.41 kB
"use strict"; // ***************************************************************************** // Copyright (C) 2024 STMicroelectronics. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: MIT License which is // available at https://opensource.org/licenses/MIT. // // SPDX-License-Identifier: EPL-2.0 OR MIT // ***************************************************************************** Object.defineProperty(exports, "__esModule", { value: true }); exports.timeout = timeout; exports.retryUntilFulfilled = retryUntilFulfilled; const core_1 = require("@theia/core"); const promise_util_1 = require("@theia/core/lib/common/promise-util"); const DEFAULT_TIMEOUT = 30_000 * 4; const stateProp = Symbol('state'); class TimeoutError extends Error { constructor(message) { super(message ?? 'Promise timed out.'); } } /** * Wrap a promise in a timeout that will reject if the promised value * is not provided within a given number of milliseconds. * * @param promise the promise to wrap * @param an optional timeout, in milliseconds. If not specified, the default is 30 seconds * @param callback an optional call-back to be invoked on completion of the wrapped provider * either by success, failure, or timeout. If it returns a string, it will be used for the * message of the returned promise's rejection error (if applicable) */ function timeout(promise, timeout, callback) { const provided = new core_1.Emitter(); const timeoutMillis = timeout ?? DEFAULT_TIMEOUT; if (timeoutMillis <= 0) { // Already timed out return Promise.reject(new TimeoutError(callback?.('timeout'))); } const result = setState((0, promise_util_1.waitForEvent)(provided.event, timeoutMillis) .then( /* completed on time or failed */ (outcome) => { invokeCallback(result, // whether failing by error or completing normally, it didn't time out outcome); if (outcome instanceof Error) { throw outcome; } setState(result, { state: 'completed', result: outcome }); return outcome; }) .catch((error) => { /* timed out or failed */ if (!(error instanceof core_1.CancellationError)) { // Initialization failed setState(result, { state: 'failed', reason: error }); throw error; } // Timed out const message = invokeCallback(result, 'timeout'); setState(result, { state: 'timeout' }); throw new TimeoutError(message); }) .finally(() => provided.dispose()), { state: 'pending', promise, timeoutMillis, callback, }); // Attempt to await the provided value promise .then((result) => provided.fire(result)) .catch((error) => provided.fire(error)); return result; } function isTimeoutPromise(promise) { return stateProp in promise; } async function retryUntilFulfilled(fn) { const timeoutPromise = wrap(fn()); if (timeoutPromise[stateProp].state !== 'pending') { // If it has already completed, there's nothing to retry return timeoutPromise; } const { promise, timeoutMillis, callback } = timeoutPromise[stateProp]; // Don't let the then/catch clauses attached by the timeout() function // invoke the call-back because we'll do it, ourselves timeoutPromise[stateProp].callback = undefined; const retryDelay = retryInterval(timeoutMillis); let nonTimeoutFailure; let remainingTimeout = timeoutMillis; let nextTry = promise; for (;;) { try { const result = await timeout(nextTry, remainingTimeout); callback?.(result); return result; } catch (error) { if (error instanceof TimeoutError) { // Timed out. Give up if (nonTimeoutFailure !== undefined) { callback?.(nonTimeoutFailure); throw nonTimeoutFailure; } else { throw new TimeoutError(callback?.('timeout')); } } nonTimeoutFailure = error; // Try again remainingTimeout -= retryDelay; await (0, promise_util_1.wait)(retryDelay); nextTry = unwrap(fn()); } } } // re-try three times per second-or-less function retryInterval(timeoutMillis) { return timeoutMillis > 1000 ? 300 : timeoutMillis / 3.5; } /** Wrap a promise as a timeout promise if it isn't already one. */ function wrap(promise) { return isTimeoutPromise(promise) ? promise : timeout(promise); } /** Unwrap a timeout promise to retrieve the underlying promise. */ function unwrap(promise) { if (!isTimeoutPromise(promise)) { return promise; } const state = promise[stateProp]; switch (state.state) { case 'pending': // Don't let the then/catch clauses attached by the timeout() function // invoke the call-back because we'll do it, ourselves state.callback = undefined; return state.promise; case 'failed': return Promise.reject(state.reason); case 'completed': return Promise.resolve(state.result); default: return Promise.reject(new TimeoutError()); } } /** * Invoke the call-function attached to a timeout `promise`, if it has one. * * @param promise the timeout promise on which to invoke the call-back * @param args the arguments to the call-back function * @returns the return result of the call-back, if there is one */ function invokeCallback(promise, ...args) { return promise[stateProp].callback?.(...args); } /** * Update the state of a timeout-promise. * * @param promise the promise on which to update the timeout state * @param state the state to set * @return the updated `promise` */ function setState(promise, state) { return Object.assign(promise, { [stateProp]: state }); } //# sourceMappingURL=promise-util.js.map