UNPKG

@eclipse-scout/core

Version:
260 lines (225 loc) 8.63 kB
/* * Copyright (c) 2010, 2024 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {arrays, CallModel, InitModelOf, objects, ObjectWithType, scout, strings} from '../index'; import $ from 'jquery'; /** * Represents a robust "call" that, when it fails, is retried automatically for a specific * number of times, before failing ultimately. The call implementation must be provided * by a subclass by overriding the _callImpl() method. */ export abstract class Call implements CallModel, ObjectWithType { declare model: CallModel; objectType: string; minCallDuration: number; callCounter: number; retryIntervals: number[]; maxRetries: number; defaultRetryInterval: number; deferred: JQuery.Deferred<any>; aborted: boolean; initialized: boolean; pendingCall: JQuery.Promise<any>; callTimeoutId: number; callStartTimestamp: number; type: string; name: string; uniqueName: string; logPrefix: string; result: any; constructor() { this.initialized = false; this.retryIntervals = []; this.maxRetries = null; // automatically set to the length of retryInternals in init(), unless specified explicitly this.minCallDuration = 500; this.defaultRetryInterval = 300; /** * Counts how many times this call was actually performed (normally, only 1 try is expected) */ this.callCounter = 0; this.deferred = $.Deferred(); this.aborted = false; this.pendingCall = null; this.callTimeoutId = null; this.callStartTimestamp = null; this.type = null; this.name = null; /** * Unique identifier of this call instance for logging and debugging purposes */ this.uniqueName = null; this.logPrefix = ''; this.result = null; } static GLOBAL_SEQ = 0; init(model: InitModelOf<this>) { $.extend(this, model); this.retryIntervals = (this.retryIntervals ? this.retryIntervals.slice() : []); // Do not modify the passed value -> create a copy if (objects.isNullOrUndefined(this.maxRetries)) { this.maxRetries = this.retryIntervals.length; } // Assign a unique name to the call to help distinguish different calls in the log this.uniqueName = scout.nvl(this.type, 'call') + '-' + (Call.GLOBAL_SEQ++) + strings.box(' ', this.name, ''); this.initialized = true; } protected _checkInitialized() { if (!this.initialized) { throw new Error('Not initialized'); } } protected _updateLogPrefix() { this.logPrefix = this.callCounter + '/' + (this.maxRetries + 1) + ' [' + this.uniqueName + '] '; } protected _resolve() { $.log.isTraceEnabled() && $.log.trace(this.logPrefix + '[RESOLVE]'); this.deferred.resolve(...arrays.ensure(this.result)); } protected _reject() { $.log.isTraceEnabled() && $.log.trace(this.logPrefix + '[REJECT]'); this.deferred.reject(...arrays.ensure(this.result)); } protected _setResult(...args: any[]) { this.result = args; } // ================================================================================== /** * Performs the call with retries. * * Returns a promise that is ... * ... RESOLVED when the call was successful (possibly after some retries). * ... REJECTED when the call failed and no more retries are possible. * * * | (promise) * | ^ * v | * +--------+ +---------+ .---------------. (yes) * | call() | . . . . . | _call() | ------> < success? > ------> [RESOLVE] * +--------+ +---------+ '---------------' * ^ |(no) * | | * | v * | .---------------. (yes) * | < aborted? > ------> [REJECT] * | '---------------' * | |(no) * | | * | v * | .---------------. (no) * | < retry possible? > ------> [REJECT] * | '---------------' * | |(yes) * | sleep | * +-------- %%% ----------+ */ call(): JQuery.Promise<any> { this._checkInitialized(); this._call(); return this.deferred.promise(); } /** * Aborts the call. If the request is currently running, it is aborted (interrupted). * If a retry is scheduled, that retry is cancelled. * * The promise returned by call() is REJECTED. */ abort() { this._checkInitialized(); this._abort(); } // ================================================================================== protected _call() { if (this.aborted) { throw new Error('Call is aborted'); } this.callTimeoutId = null; this.callStartTimestamp = Date.now(); this.callCounter++; this._updateLogPrefix(); this.pendingCall = this._callImpl() .always(() => { this.pendingCall = null; }) .done(this._setResultDone.bind(this)) .done(this._onCallDone.bind(this)) .fail(this._setResultFail.bind(this)) .fail(this._onCallFail.bind(this)); } /** * Performs the actual request. */ protected abstract _callImpl(): JQuery.Promise<any>; protected _setResultDone(...args: any[]) { this._setResult(...args); } protected _setResultFail(...args: any[]) { this._setResult(...args); } protected _onCallDone(...args: any[]) { // Call successful -> RESOLVE this._resolve(); } protected _onCallFail(...args: any[]) { // Aborted? -> REJECT if (this.aborted) { $.log.isTraceEnabled() && $.log.trace(this.logPrefix + 'Call aborted'); this._reject(); return; } // Retry impossible? -> REJECT let nextInterval = this._nextRetryImpl(...args); if (typeof nextInterval !== 'number') { $.log.isTraceEnabled() && $.log.trace(this.logPrefix + 'No retries remaining'); this._reject(); return; } // Retry let callDuration = Date.now() - this.callStartTimestamp; let additionalDelay = Math.max(this.minCallDuration - callDuration, 0); let retryInterval = nextInterval + additionalDelay; $.log.isTraceEnabled() && $.log.trace(this.logPrefix + 'Try again in ' + retryInterval + ' ms...'); this.callTimeoutId = setTimeout(this._call.bind(this), retryInterval); } /** * Checks if the call can be retried. If a number is returned, a retry is performed with a delay of the corresponding amount of milliseconds. * All other values indicate that no retry must be performed. (It is recommended to return 'false' or 'null' in this case.) * This method MAY be overridden by a subclass. */ protected _nextRetryImpl(...args: any[]): number | boolean { if (this.maxRetries >= 0 && this.callCounter > this.maxRetries) { return false; // no more retries } if (this.retryIntervals.length) { // there are more intervals: consume the next let nextRetryInterval = this.retryIntervals.shift(); this.defaultRetryInterval = nextRetryInterval; // keep the last interval for next retries (in case maxRetries > retryIntervals.length) return nextRetryInterval; } // there should be more retries, but no intervals are available (anymore). // use the default interval for all subsequent retries. return this.defaultRetryInterval; } // ================================================================================== protected _abort() { this.aborted = true; // Abort while waiting for the next retry (there is no running call) if (this.callTimeoutId) { $.log.isTraceEnabled() && $.log.trace(this.logPrefix + 'Cancelled scheduled retry'); clearTimeout(this.callTimeoutId); this.callTimeoutId = null; this._reject(); return; } // Abort a running call if necessary this._abortImpl(); } protected _abortImpl() { // This method MAY be overridden by a subclass. } }