@eclipse-scout/core
Version:
Eclipse Scout runtime
182 lines (159 loc) • 6.02 kB
text/typescript
/*
* Copyright (c) 2010, 2025 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 {AjaxSettings, RemoteRequest, RemoteResponse, Session} from '../index';
export class ResponseQueue {
session: Session;
queue: RemoteResponse[];
lastProcessedSequenceNo: number;
nextExpectedSequenceNo: number;
force: boolean;
forceTimeoutId: number;
constructor(session: Session) {
this.session = session;
this.queue = [];
this.lastProcessedSequenceNo = 0;
this.nextExpectedSequenceNo = 1;
this.force = false;
this.forceTimeoutId = null;
}
/**
* Timeout in milliseconds after which the response queue is processed even when a sequence number is still missing.
* This should prevent an infinitely blocked UI, but (optimistically) assumes that the missed response was not important.
*
* This value should be slightly larger than the {@link Session#POLLING_GRACE_PERIOD} to give the retry mechanism a
* chance to retrieve a missed response after a network interruption.
*/
static FORCE_TIMEOUT = (Session.POLLING_GRACE_PERIOD + 5) * 1000;
add(response?: RemoteResponse) {
let sequenceNo = response && response['#'];
// Ignore responses that were already processed (duplicate detection)
if (sequenceNo && sequenceNo <= this.lastProcessedSequenceNo) {
return;
}
// "Fast-forward" the expected sequence no. when a combined response is received
if (sequenceNo && response.combined) {
this.lastProcessedSequenceNo = Math.max(sequenceNo - 1, this.lastProcessedSequenceNo);
this.nextExpectedSequenceNo = Math.max(sequenceNo, this.nextExpectedSequenceNo);
}
if (!sequenceNo || this.queue.length === 0) { // Handle messages without sequenceNo in the order they were received
this.queue.push(response);
} else {
// Insert at correct position (ascending order)
let newQueue = [];
let responseToInsert = response;
for (let i = 0; i < this.queue.length; i++) {
let el = this.queue[i];
if (el['#']) {
if (responseToInsert && el['#'] > sequenceNo) {
// insert at position
newQueue.push(response);
responseToInsert = null;
}
if (el['#'] <= this.lastProcessedSequenceNo) {
// skip obsolete elements (may happen when a combined response is added to the queue)
continue;
}
}
newQueue.push(el);
}
if (responseToInsert) {
// no element with bigger seqNo found -> insert as last element
newQueue.push(responseToInsert);
}
this.queue = newQueue;
}
}
process(response?: RemoteResponse): boolean {
if (response) {
this.add(response);
}
// Process the queue in ascending order
let responseSuccess = true;
let missingResponse = false;
let nonProcessedResponses = [];
for (let i = 0; i < this.queue.length; i++) {
let el = this.queue[i];
let sequenceNo = el['#'];
// For elements with a sequence number, check if they are in the expected order
if (sequenceNo) {
if (this.nextExpectedSequenceNo && !this.force && !missingResponse) {
missingResponse = this._checkMissingResponse(sequenceNo);
}
if (missingResponse) {
// Sequence is not complete, process those messages later
nonProcessedResponses.push(el);
continue;
}
}
// Handle the element
let success = this._handleResponse(el);
// Only return success value of the response that was passed to the process() call
if (response && el === response) {
responseSuccess = success;
}
// Update the expected next sequenceNo
if (sequenceNo) {
this.lastProcessedSequenceNo = sequenceNo;
this.nextExpectedSequenceNo = sequenceNo + 1;
}
}
// Keep non-processed events (because they are not in sequence) in the queue
this.queue = nonProcessedResponses;
this._checkTimeout();
return responseSuccess;
}
size(): number {
return this.queue.length;
}
protected _handleResponse(response: RemoteResponse): boolean {
return this.session.processJsonResponseInternal(response);
}
protected _checkMissingResponse(sequenceNo: number): boolean {
return this.nextExpectedSequenceNo !== sequenceNo;
}
protected _checkTimeout() {
// If there are non-processed elements, schedule a job that forces the processing of those
// elements after a certain timeout to prevent the "blocked forever syndrome" if a response
// was lost on the network.
if (this.queue.length === 0) {
clearTimeout(this.forceTimeoutId);
this.forceTimeoutId = null;
} else if (!this.forceTimeoutId) {
this.forceTimeoutId = setTimeout(() => {
try {
this._logTimeout();
} catch (error) {
// nop
}
this.force = true;
try {
this.process();
} finally {
this.force = false;
this.forceTimeoutId = null;
}
}, ResponseQueue.FORCE_TIMEOUT);
}
}
protected _logTimeout() {
this.session.sendLogRequest('Expected response #' + this.nextExpectedSequenceNo + ' still missing after ' +
ResponseQueue.FORCE_TIMEOUT + ' ms. Forcing response queue to process ' + this.size() + ' elements: ' + this.queueToString());
}
prepareRequest(request: RemoteRequest) {
request['#ACK'] = this.lastProcessedSequenceNo;
}
prepareHttpRequest(ajaxOptions: AjaxSettings) {
ajaxOptions.headers = ajaxOptions.headers || {};
ajaxOptions.headers['X-Scout-#ACK'] = this.lastProcessedSequenceNo + '';
}
queueToString(): string {
return '[' + this.queue.map(el => '#' + el['#']).join(', ') + ']';
}
}