@eclipse-emfcloud/model-service-theia
Version:
Model service Theia
173 lines • 6.41 kB
JavaScript
;
// *****************************************************************************
// 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