[IMP] mail: convert timer utils into model
Better integration with the rest of the code. Task-2831082 closes odoo/odoo#90726 Signed-off-by: Sébastien Theys (seb) <seb@odoo.com>
This commit is contained in:
@@ -107,6 +107,20 @@ function insertAndReplace(data = {}) {
|
||||
return new FieldCommand('insert-and-replace', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a insert-and-unlink command to give to the model manager at create/update.
|
||||
* `insertAndUnlink` command can be used for relation fields.
|
||||
* - Create new record(s) from data if the record(s) do not exist;
|
||||
* - or update the record(s) if they can be found from identifying data;
|
||||
* - and then unlink the record(s) from the relation field (if they were present).
|
||||
*
|
||||
* @param {Object|Object[]} [data={}] - data object or data objects array to insert and unlink record(s).
|
||||
* @returns {FieldCommand}
|
||||
*/
|
||||
export function insertAndUnlink(data = {}) {
|
||||
return new FieldCommand('insert-and-unlink', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link command to give to the model manager at create/update.
|
||||
* `link` command can be used for relation fields.
|
||||
|
||||
@@ -560,7 +560,7 @@ registerModel({
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this.messaging.currentPartner) {
|
||||
if (this.messaging.currentPartner) { // not supported for guest
|
||||
composer.thread.unregisterCurrentPartnerIsTyping({ immediateNotify: true });
|
||||
}
|
||||
const escapedAndCompactContent = escapeAndCompactTextContent(composer.textInputContent);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { one } from '@mail/model/model_field';
|
||||
import { insertAndReplace } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'OtherMemberLongTypingInThreadTimer',
|
||||
identifyingFields: [
|
||||
'partner',
|
||||
'thread',
|
||||
],
|
||||
recordMethods: {
|
||||
onOtherMemberLongTypingTimeout() {
|
||||
this.thread.unregisterOtherMemberTypingMember(this.partner);
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
partner: one('Partner', {
|
||||
inverse: 'otherMemberLongTypingInThreadTimers',
|
||||
readonly: true,
|
||||
required: true,
|
||||
}),
|
||||
thread: one('Thread', {
|
||||
inverse: 'otherMembersLongTypingTimers',
|
||||
readonly: true,
|
||||
required: true,
|
||||
}),
|
||||
timer: one('Timer', {
|
||||
default: insertAndReplace(),
|
||||
inverse: 'otherMemberLongTypingInThreadTimerOwner',
|
||||
isCausal: true,
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -456,6 +456,10 @@ registerModel({
|
||||
nameOrDisplayName: attr({
|
||||
compute: '_computeNameOrDisplayName',
|
||||
}),
|
||||
otherMemberLongTypingInThreadTimers: many('OtherMemberLongTypingInThreadTimer', {
|
||||
inverse: 'partner',
|
||||
isCausal: true,
|
||||
}),
|
||||
rtcSessions: many('RtcSession', {
|
||||
inverse: 'partner',
|
||||
}),
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, many, one } from '@mail/model/model_field';
|
||||
import { clear, insert, insertAndReplace, link, replace, unlink } from '@mail/model/model_field_command';
|
||||
import { clear, insert, insertAndReplace, insertAndUnlink, link, replace, unlink } from '@mail/model/model_field_command';
|
||||
import { OnChange } from '@mail/model/model_onchange';
|
||||
import throttle from '@mail/utils/throttle';
|
||||
import Timer from '@mail/utils/timer';
|
||||
import { cleanSearchTerm } from '@mail/utils/utils';
|
||||
import * as mailUtils from '@mail/js/utils';
|
||||
|
||||
@@ -27,12 +26,7 @@ registerModel({
|
||||
identifyingFields: ['model', 'id'],
|
||||
lifecycleHooks: {
|
||||
_willDelete() {
|
||||
this.currentPartnerInactiveTypingTimer.clear();
|
||||
this.currentPartnerLongTypingTimer.clear();
|
||||
this.throttleNotifyCurrentPartnerTypingStatus.clear();
|
||||
for (const timer of this.otherMembersLongTypingTimers.values()) {
|
||||
timer.clear();
|
||||
}
|
||||
if (this.isTemporary) {
|
||||
for (const message of this.messages) {
|
||||
message.delete();
|
||||
@@ -888,7 +882,7 @@ registerModel({
|
||||
* Refresh the typing status of the current partner.
|
||||
*/
|
||||
refreshCurrentPartnerIsTyping() {
|
||||
this.currentPartnerInactiveTypingTimer.reset();
|
||||
this.update({ currentPartnerInactiveTypingTimer: [clear(), insertAndReplace()] });
|
||||
},
|
||||
/**
|
||||
* Called to refresh a registered other member partner that is typing
|
||||
@@ -897,7 +891,12 @@ registerModel({
|
||||
* @param {Partner} partner
|
||||
*/
|
||||
refreshOtherMemberTypingMember(partner) {
|
||||
this.otherMembersLongTypingTimers.get(partner).reset();
|
||||
this.update({
|
||||
otherMembersLongTypingTimers: [
|
||||
insertAndUnlink({ partner: replace(partner) }),
|
||||
insert({ partner: replace(partner) }),
|
||||
],
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Called when current partner is inserting some input in composer.
|
||||
@@ -906,8 +905,10 @@ registerModel({
|
||||
*/
|
||||
async registerCurrentPartnerIsTyping() {
|
||||
// Handling of typing timers.
|
||||
this.currentPartnerInactiveTypingTimer.start();
|
||||
this.currentPartnerLongTypingTimer.start();
|
||||
this.update({
|
||||
currentPartnerInactiveTypingTimer: insertAndReplace(),
|
||||
currentPartnerLongTypingTimer: insertAndReplace(),
|
||||
});
|
||||
// Manage typing member relation.
|
||||
const currentPartner = this.messaging.currentPartner;
|
||||
const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
|
||||
@@ -927,13 +928,7 @@ registerModel({
|
||||
* @param {Partner} partner
|
||||
*/
|
||||
registerOtherMemberTypingMember(partner) {
|
||||
const timer = new Timer(
|
||||
this.messaging,
|
||||
() => this._onOtherMemberLongTypingTimeout(partner),
|
||||
60 * 1000
|
||||
);
|
||||
this.otherMembersLongTypingTimers.set(partner, timer);
|
||||
timer.start();
|
||||
this.update({ otherMembersLongTypingTimers: insert({ partner: replace(partner) }) });
|
||||
const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
|
||||
.filter(localId => localId !== partner.localId);
|
||||
newOrderedTypingMemberLocalIds.push(partner.localId);
|
||||
@@ -999,10 +994,12 @@ registerModel({
|
||||
* status of current partner is immediately notified and doesn't
|
||||
* consume throttling at all.
|
||||
*/
|
||||
async unregisterCurrentPartnerIsTyping({ immediateNotify = false } = {}) {
|
||||
unregisterCurrentPartnerIsTyping({ immediateNotify = false } = {}) {
|
||||
// Handling of typing timers.
|
||||
this.currentPartnerInactiveTypingTimer.clear();
|
||||
this.currentPartnerLongTypingTimer.clear();
|
||||
this.update({
|
||||
currentPartnerInactiveTypingTimer: clear(),
|
||||
currentPartnerLongTypingTimer: clear(),
|
||||
});
|
||||
// Manage typing member relation.
|
||||
const currentPartner = this.messaging.currentPartner;
|
||||
const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
|
||||
@@ -1015,7 +1012,7 @@ registerModel({
|
||||
if (immediateNotify) {
|
||||
this.throttleNotifyCurrentPartnerTypingStatus.clear();
|
||||
}
|
||||
await this.throttleNotifyCurrentPartnerTypingStatus({ isTyping: false });
|
||||
this.throttleNotifyCurrentPartnerTypingStatus({ isTyping: false });
|
||||
},
|
||||
/**
|
||||
* Called to unregister an other member partner that is no longer typing
|
||||
@@ -1024,8 +1021,7 @@ registerModel({
|
||||
* @param {Partner} partner
|
||||
*/
|
||||
unregisterOtherMemberTypingMember(partner) {
|
||||
this.otherMembersLongTypingTimers.get(partner).clear();
|
||||
this.otherMembersLongTypingTimers.delete(partner);
|
||||
this.update({ otherMembersLongTypingTimers: insertAndUnlink({ partner: replace(partner) }) });
|
||||
const newOrderedTypingMemberLocalIds = this.orderedTypingMemberLocalIds
|
||||
.filter(localId => localId !== partner.localId);
|
||||
this.update({
|
||||
@@ -1091,17 +1087,6 @@ registerModel({
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Timer}
|
||||
*/
|
||||
_computeCurrentPartnerLongTypingTimer() {
|
||||
return new Timer(
|
||||
this.messaging,
|
||||
() => this._onCurrentPartnerLongTypingTimeout(),
|
||||
50 * 1000
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {FieldCommand}
|
||||
@@ -1274,21 +1259,6 @@ registerModel({
|
||||
follower.partner && follower.partner === this.messaging.currentPartner
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Timer}
|
||||
*/
|
||||
_computeCurrentPartnerInactiveTypingTimer() {
|
||||
return new Timer(
|
||||
this.messaging,
|
||||
() => {
|
||||
if (this.messaging.currentPartner) {
|
||||
return this._onCurrentPartnerInactiveTypingTimeout();
|
||||
}
|
||||
},
|
||||
5 * 1000
|
||||
);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {boolean}
|
||||
@@ -1547,13 +1517,6 @@ registerModel({
|
||||
.filter(member => !!member),
|
||||
]];
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Map}
|
||||
*/
|
||||
_computeOtherMembersLongTypingTimers() {
|
||||
return new Map();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {Activity[]}
|
||||
@@ -1680,8 +1643,8 @@ registerModel({
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isTyping && this.currentPartnerLongTypingTimer.isRunning) {
|
||||
this.currentPartnerLongTypingTimer.reset();
|
||||
if (isTyping && this.currentPartnerLongTypingTimer) {
|
||||
this.update({ currentPartnerLongTypingTimer: [clear(), insertAndReplace()] });
|
||||
}
|
||||
}
|
||||
this.update({
|
||||
@@ -1821,34 +1784,18 @@ registerModel({
|
||||
});
|
||||
this.update({ members });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
async _onCurrentPartnerInactiveTypingTimeout() {
|
||||
await this.unregisterCurrentPartnerIsTyping();
|
||||
onCurrentPartnerInactiveTypingTimeout() {
|
||||
this.unregisterCurrentPartnerIsTyping();
|
||||
},
|
||||
/**
|
||||
* Called when current partner has been typing for a very long time.
|
||||
* Immediately notify other members that he/she is still typing.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _onCurrentPartnerLongTypingTimeout() {
|
||||
async onCurrentPartnerLongTypingTimeout() {
|
||||
this.update({ forceNotifyNextCurrentPartnerTypingStatus: true });
|
||||
this.throttleNotifyCurrentPartnerTypingStatus.clear();
|
||||
await this.throttleNotifyCurrentPartnerTypingStatus({ isTyping: true });
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @param {Partner} partner
|
||||
*/
|
||||
async _onOtherMemberLongTypingTimeout(partner) {
|
||||
if (!this.typingMembers.includes(partner)) {
|
||||
this.otherMembersLongTypingTimers.delete(partner);
|
||||
return;
|
||||
}
|
||||
this.unregisterOtherMemberTypingMember(partner);
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
/**
|
||||
@@ -1914,8 +1861,9 @@ registerModel({
|
||||
* partner has stopped typing something, due to making no changes
|
||||
* on the composer for some time.
|
||||
*/
|
||||
currentPartnerInactiveTypingTimer: attr({
|
||||
compute: '_computeCurrentPartnerInactiveTypingTimer',
|
||||
currentPartnerInactiveTypingTimer: one('Timer', {
|
||||
inverse: 'threadAsCurrentPartnerInactiveTypingTimerOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
/**
|
||||
* Last 'is_typing' status of current partner that has been notified
|
||||
@@ -1941,8 +1889,9 @@ registerModel({
|
||||
* is still typing something, so that they should not assume he/she
|
||||
* has stopped typing something.
|
||||
*/
|
||||
currentPartnerLongTypingTimer: attr({
|
||||
compute: '_computeCurrentPartnerLongTypingTimer',
|
||||
currentPartnerLongTypingTimer: one('Timer', {
|
||||
inverse: 'threadAsCurrentPartnerLongTypingTimerOwner',
|
||||
isCausal: true,
|
||||
}),
|
||||
custom_channel_name: attr(),
|
||||
/**
|
||||
@@ -2323,8 +2272,9 @@ registerModel({
|
||||
* @see registerOtherMemberTypingMember
|
||||
* @see unregisterOtherMemberTypingMember
|
||||
*/
|
||||
otherMembersLongTypingTimers: attr({
|
||||
compute: '_computeOtherMembersLongTypingTimers',
|
||||
otherMembersLongTypingTimers: many('OtherMemberLongTypingInThreadTimer', {
|
||||
inverse: 'thread',
|
||||
isCausal: true,
|
||||
}),
|
||||
/**
|
||||
* States the `Activity` that belongs to `this` and that are
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registerModel } from '@mail/model/model_core';
|
||||
import { attr, one } from '@mail/model/model_field';
|
||||
import { clear } from '@mail/model/model_field_command';
|
||||
|
||||
registerModel({
|
||||
name: 'Timer',
|
||||
identifyingFields: [[
|
||||
'otherMemberLongTypingInThreadTimerOwner',
|
||||
'threadAsCurrentPartnerInactiveTypingTimerOwner',
|
||||
'threadAsCurrentPartnerLongTypingTimerOwner',
|
||||
]],
|
||||
lifecycleHooks: {
|
||||
_willDelete() {
|
||||
this.messaging.browser.clearTimeout(this.timeoutId);
|
||||
},
|
||||
},
|
||||
recordMethods: {
|
||||
/**
|
||||
* @private
|
||||
* @returns {integer|FieldCommand}
|
||||
*/
|
||||
_computeDuration() {
|
||||
if (this.threadAsCurrentPartnerInactiveTypingTimerOwner) {
|
||||
return 5 * 1000;
|
||||
}
|
||||
if (this.threadAsCurrentPartnerLongTypingTimerOwner) {
|
||||
return 50 * 1000;
|
||||
}
|
||||
if (this.otherMemberLongTypingInThreadTimerOwner) {
|
||||
return 60 * 1000;
|
||||
}
|
||||
return clear();
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_computeTimeoutId() {
|
||||
if (this.duration === undefined) {
|
||||
return; // ensure duration is computed first
|
||||
}
|
||||
if (this.timeoutId) {
|
||||
return;
|
||||
}
|
||||
return this.messaging.browser.setTimeout(this._onTimeout, this.duration);
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onTimeout() {
|
||||
this._onTimeoutOwner();
|
||||
if (this.exists()) { // owner might have deleted the timer itself
|
||||
this.delete();
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
_onTimeoutOwner() {
|
||||
if (this.threadAsCurrentPartnerInactiveTypingTimerOwner) {
|
||||
this.threadAsCurrentPartnerInactiveTypingTimerOwner.onCurrentPartnerInactiveTypingTimeout();
|
||||
return;
|
||||
}
|
||||
if (this.threadAsCurrentPartnerLongTypingTimerOwner) {
|
||||
this.threadAsCurrentPartnerLongTypingTimerOwner.onCurrentPartnerLongTypingTimeout();
|
||||
return;
|
||||
}
|
||||
if (this.otherMemberLongTypingInThreadTimerOwner) {
|
||||
this.otherMemberLongTypingInThreadTimerOwner.onOtherMemberLongTypingTimeout();
|
||||
return;
|
||||
}
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
/**
|
||||
* Duration, in milliseconds, until timer times out and calls the
|
||||
* timeout function.
|
||||
*/
|
||||
duration: attr({
|
||||
compute: '_computeDuration',
|
||||
readonly: true,
|
||||
required: true,
|
||||
}),
|
||||
otherMemberLongTypingInThreadTimerOwner: one('OtherMemberLongTypingInThreadTimer', {
|
||||
inverse: 'timer',
|
||||
isCausal: true,
|
||||
readonly: true,
|
||||
}),
|
||||
threadAsCurrentPartnerInactiveTypingTimerOwner: one('Thread', {
|
||||
inverse: 'currentPartnerInactiveTypingTimer',
|
||||
readonly: true,
|
||||
}),
|
||||
threadAsCurrentPartnerLongTypingTimerOwner: one('Thread', {
|
||||
inverse: 'currentPartnerLongTypingTimer',
|
||||
readonly: true,
|
||||
}),
|
||||
/**
|
||||
* Internal reference of `setTimeout()` that is used to invoke function
|
||||
* when timer times out. Useful to clear it when timer is cleared/reset.
|
||||
*/
|
||||
timeoutId: attr({
|
||||
compute: '_computeTimeoutId',
|
||||
readonly: true,
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
@@ -1,162 +0,0 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { makeDeferred } from '@mail/utils/deferred';
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Errors
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* List of Timer errors.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Error when timer has been cleared with `.clear()` or `.reset()`. Used to
|
||||
* let know caller of timer that the countdown has been aborted, which
|
||||
* means the inner function will not be called. Usually caller should just
|
||||
* accept it and kindly treated this error as a polite warning.
|
||||
*/
|
||||
class TimerClearedError extends Error {
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
constructor(timerId, ...args) {
|
||||
super(...args);
|
||||
this.name = 'TimerClearedError';
|
||||
this.timerId = timerId;
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Private
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* This class creates a timer which, when times out, calls a function.
|
||||
* Note that the timer is not started on initialization (@see start method).
|
||||
*/
|
||||
class Timer {
|
||||
|
||||
/**
|
||||
* @param {Messaging} messaging the messaging record.
|
||||
* @param {function} onTimeout
|
||||
* @param {integer} duration
|
||||
* @param {Object} [param3={}]
|
||||
* @param {boolean} [param3.silentCancelationErrors=true] if unset, caller
|
||||
* of timer will observe some errors that come from current timer calls
|
||||
* that has been cleared with `.clear()` or `.reset()`.
|
||||
* @see TimerClearedError for when timer has been aborted from `.clear()`
|
||||
* or `.reset()`.
|
||||
*/
|
||||
constructor(messaging, onTimeout, duration, { silentCancelationErrors = true } = {}) {
|
||||
this.messaging = messaging;
|
||||
/**
|
||||
* Determine whether the timer has a pending timeout.
|
||||
*/
|
||||
this.isRunning = false;
|
||||
/**
|
||||
* Duration, in milliseconds, until timer times out and calls the
|
||||
* timeout function.
|
||||
*/
|
||||
this._duration = duration;
|
||||
/**
|
||||
* Determine whether the caller of timer `.start()` and `.reset()`
|
||||
* should observe cancelation errors from `.clear()` or `.reset()`.
|
||||
*/
|
||||
this._hasSilentCancelationErrors = silentCancelationErrors;
|
||||
/**
|
||||
* The function that is called when the timer times out.
|
||||
*/
|
||||
this._onTimeout = onTimeout;
|
||||
/**
|
||||
* Deferred of a currently pending invocation to inner function on
|
||||
* timeout.
|
||||
*/
|
||||
this._timeoutDeferred = undefined;
|
||||
/**
|
||||
* Internal reference of `setTimeout()` that is used to invoke function
|
||||
* when timer times out. Useful to clear it when timer is cleared/reset.
|
||||
*/
|
||||
this._timeoutId = undefined;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Clear the timer, which basically sets the state of timer as if it was
|
||||
* just instantiated, without being started. This function makes sense only
|
||||
* when this timer is running.
|
||||
*/
|
||||
clear() {
|
||||
this.messaging.browser.clearTimeout(this._timeoutId);
|
||||
this.isRunning = false;
|
||||
if (!this._timeoutDeferred) {
|
||||
return;
|
||||
}
|
||||
this._timeoutDeferred.reject(new TimerClearedError(this.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the timer, i.e. the pending timeout is refreshed with initial
|
||||
* duration. This function makes sense only when this timer is running.
|
||||
*/
|
||||
async reset() {
|
||||
this.clear();
|
||||
await this.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the timer, i.e. after a certain duration, it times out and calls
|
||||
* a function back. This function makes sense only when this timer is not
|
||||
* yet running.
|
||||
*
|
||||
* @throws {Error} in case the timer is already running.
|
||||
*/
|
||||
async start() {
|
||||
if (this.isRunning) {
|
||||
throw new Error("Cannot start a timer that is currently running.");
|
||||
}
|
||||
this.isRunning = true;
|
||||
const timeoutDeferred = makeDeferred();
|
||||
this._timeoutDeferred = timeoutDeferred;
|
||||
const timeoutId = this.messaging.browser.setTimeout(
|
||||
() => {
|
||||
this.isRunning = false;
|
||||
timeoutDeferred.resolve(this._onTimeout());
|
||||
},
|
||||
this._duration
|
||||
);
|
||||
this._timeoutId = timeoutId;
|
||||
let result;
|
||||
try {
|
||||
result = await timeoutDeferred;
|
||||
} catch (error) {
|
||||
if (
|
||||
!this._hasSilentCancelationErrors ||
|
||||
!(error instanceof TimerClearedError) ||
|
||||
error.timerId !== this.id
|
||||
) {
|
||||
// This branching should never happens.
|
||||
// Still defined in case of programming error.
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
this.messaging.browser.clearTimeout(timeoutId);
|
||||
this._timeoutDeferred = undefined;
|
||||
this.isRunning = false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Make external timer errors accessible from timer class.
|
||||
*/
|
||||
Object.assign(Timer, {
|
||||
TimerClearedError,
|
||||
});
|
||||
|
||||
export default Timer;
|
||||
@@ -34,5 +34,13 @@ registerModel({
|
||||
inverse: 'qunitTest',
|
||||
isCausal: true,
|
||||
}),
|
||||
timer1: one('Timer', {
|
||||
inverse: 'qunitTestOwner1',
|
||||
isCausal: true,
|
||||
}),
|
||||
timer2: one('Timer', {
|
||||
inverse: 'qunitTestOwner2',
|
||||
isCausal: true,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { addFields, patchIdentifyingFields, patchRecordMethods } from '@mail/model/model_core';
|
||||
import { one } from '@mail/model/model_field';
|
||||
// ensure that the model definition is loaded before the patch
|
||||
import '@mail/models/timer';
|
||||
|
||||
addFields('Timer', {
|
||||
qunitTestOwner1: one('QUnitTest', {
|
||||
inverse: 'timer1',
|
||||
readonly: true,
|
||||
}),
|
||||
qunitTestOwner2: one('QUnitTest', {
|
||||
inverse: 'timer2',
|
||||
readonly: true,
|
||||
}),
|
||||
});
|
||||
|
||||
patchIdentifyingFields('Timer', identifyingFields => {
|
||||
identifyingFields[0].push('qunitTestOwner1');
|
||||
identifyingFields[0].push('qunitTestOwner2');
|
||||
});
|
||||
|
||||
patchRecordMethods('Timer', {
|
||||
_computeDuration() {
|
||||
if (this.qunitTestOwner1) {
|
||||
return 0;
|
||||
}
|
||||
if (this.qunitTestOwner2) {
|
||||
return 1000 * 1000;
|
||||
}
|
||||
return this._super();
|
||||
},
|
||||
});
|
||||
@@ -1,393 +1,66 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { nextTick, start } from '@mail/../tests/helpers/test_utils';
|
||||
import Timer from '@mail/utils/timer';
|
||||
|
||||
const { TimerClearedError } = Timer;
|
||||
import { insertAndReplace } from '@mail/model/model_field_command';
|
||||
import { start } from '@mail/../tests/helpers/test_utils';
|
||||
|
||||
QUnit.module('mail', {}, function () {
|
||||
QUnit.module('utils', {}, function () {
|
||||
QUnit.module('timer', {}, function () {
|
||||
QUnit.module('timer_tests.js', {
|
||||
beforeEach() {
|
||||
this.timers = [];
|
||||
},
|
||||
afterEach() {
|
||||
// Important: tests should cleanly intercept cancelation errors that
|
||||
// may result from this teardown.
|
||||
for (const timer of this.timers) {
|
||||
timer.clear();
|
||||
}
|
||||
},
|
||||
});
|
||||
QUnit.module('timer_tests.js', {});
|
||||
|
||||
QUnit.test('timer does not timeout on initialization', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out on immediate initialization"
|
||||
);
|
||||
|
||||
await advanceTime(0);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out from initialization after 0ms"
|
||||
);
|
||||
|
||||
await advanceTime(1000 * 1000);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out from initialization after 1000s"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('timer start (duration: 0ms)', async function (assert) {
|
||||
QUnit.test('timer insert (duration: 0ms)', async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
const timer = messaging.models['Timer'].insert({
|
||||
qunitTestOwner1: insertAndReplace(),
|
||||
});
|
||||
assert.ok(
|
||||
timer.exists(),
|
||||
"timer should not have timed out immediately after insert"
|
||||
);
|
||||
|
||||
await advanceTime(0);
|
||||
assert.ok(
|
||||
hasTimedOut,
|
||||
"timer should have timed out on start after 0ms"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('timer start observe termination (duration: 0ms)', async function (assert) {
|
||||
assert.expect(6);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => {
|
||||
hasTimedOut = true;
|
||||
return 'timeout_result';
|
||||
},
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start()
|
||||
.then(result => {
|
||||
assert.strictEqual(
|
||||
result,
|
||||
'timeout_result',
|
||||
"value returned by start should be value returned by function on timeout"
|
||||
);
|
||||
assert.step('timeout');
|
||||
});
|
||||
await nextTick();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
);
|
||||
assert.verifySteps(
|
||||
[],
|
||||
"timer.start() should not have yet observed timeout"
|
||||
);
|
||||
|
||||
await advanceTime(0);
|
||||
assert.ok(
|
||||
hasTimedOut,
|
||||
"timer should have timed out on start after 0ms"
|
||||
);
|
||||
assert.verifySteps(
|
||||
['timeout'],
|
||||
"timer.start() should have observed timeout after 0ms"
|
||||
timer.exists(),
|
||||
"timer should have timed out on insert after 0ms"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('timer start (duration: 1000s)', async function (assert) {
|
||||
QUnit.test('timer insert (duration: 1000s)', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
1000 * 1000
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
const timer = messaging.models['Timer'].insert({
|
||||
qunitTestOwner2: insertAndReplace(),
|
||||
});
|
||||
assert.ok(
|
||||
timer.exists(),
|
||||
"timer should not have timed out immediately after insert"
|
||||
);
|
||||
|
||||
await advanceTime(0);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out on start after 0ms"
|
||||
assert.ok(
|
||||
timer.exists(),
|
||||
"timer should not have timed out on insert after 0ms"
|
||||
);
|
||||
|
||||
await advanceTime(1000);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out on start after 1000ms"
|
||||
assert.ok(
|
||||
timer.exists(),
|
||||
"timer should not have timed out on insert after 1000ms"
|
||||
);
|
||||
|
||||
await advanceTime(998 * 1000 + 999);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out on start after 9999ms"
|
||||
);
|
||||
|
||||
await advanceTime(1);
|
||||
assert.ok(
|
||||
hasTimedOut,
|
||||
"timer should have timed out on start after 10s"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('[no cancelation intercept] timer start then immediate clear (duration: 0ms)', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
0
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
);
|
||||
|
||||
this.timers[0].clear();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start and clear"
|
||||
);
|
||||
|
||||
await advanceTime(0);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 0ms of clear"
|
||||
);
|
||||
|
||||
await advanceTime(1000);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 1s of clear"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('[no cancelation intercept] timer start then clear before timeout (duration: 1000ms)', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
1000
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
);
|
||||
|
||||
await advanceTime(999);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after 999ms of start"
|
||||
);
|
||||
|
||||
this.timers[0].clear();
|
||||
await advanceTime(1);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 1ms of clear that happens 999ms after start (globally 1s await)"
|
||||
);
|
||||
|
||||
await advanceTime(1000);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 1001ms after clear (timer fully cleared)"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('[no cancelation intercept] timer start then reset before timeout (duration: 1000ms)', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
1000
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start();
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
);
|
||||
|
||||
await advanceTime(999);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 999ms of start"
|
||||
);
|
||||
|
||||
this.timers[0].reset();
|
||||
await advanceTime(1);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 1ms of reset which happens 999ms after start"
|
||||
);
|
||||
|
||||
await advanceTime(998);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 999ms of reset"
|
||||
timer.exists(),
|
||||
"timer should not have timed out on insert after 9999ms"
|
||||
);
|
||||
|
||||
await advanceTime(1);
|
||||
assert.ok(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out after 1s of reset"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('[with cancelation intercept] timer start then immediate clear (duration: 0ms)', async function (assert) {
|
||||
assert.expect(5);
|
||||
|
||||
const { messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
0,
|
||||
{ silentCancelationErrors: false }
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start()
|
||||
.then(() => {
|
||||
throw new Error("timer.start() should not be resolved (should have been canceled by clear)");
|
||||
})
|
||||
.catch(error => {
|
||||
assert.ok(
|
||||
error instanceof TimerClearedError,
|
||||
"Should generate a Timer cleared error (from `.clear()`)"
|
||||
);
|
||||
assert.step('timer_cleared');
|
||||
});
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps([], "should not have observed cleared timer (timer not yet cleared)");
|
||||
|
||||
this.timers[0].clear();
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
['timer_cleared'],
|
||||
"timer.start() should have observed it has been cleared"
|
||||
);
|
||||
});
|
||||
|
||||
QUnit.test('[with cancelation intercept] timer start then immediate reset (duration: 0ms)', async function (assert) {
|
||||
assert.expect(9);
|
||||
|
||||
const { advanceTime, messaging } = await start({ hasTimeControl: true });
|
||||
|
||||
let hasTimedOut = false;
|
||||
this.timers.push(
|
||||
new Timer(
|
||||
messaging,
|
||||
() => hasTimedOut = true,
|
||||
0,
|
||||
{ silentCancelationErrors: false }
|
||||
)
|
||||
);
|
||||
|
||||
this.timers[0].start()
|
||||
.then(() => {
|
||||
throw new Error("timer.start() should not observe a timeout");
|
||||
})
|
||||
.catch(error => {
|
||||
assert.ok(error instanceof TimerClearedError, "Should generate a Timer cleared error (from `.reset()`)");
|
||||
assert.step('timer_cleared');
|
||||
});
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after start"
|
||||
);
|
||||
await nextTick();
|
||||
assert.verifySteps([], "should not have observed cleared timer (timer not yet cleared)");
|
||||
|
||||
this.timers[0].reset()
|
||||
.then(() => assert.step('timer_reset_timeout'));
|
||||
await nextTick();
|
||||
assert.verifySteps(
|
||||
['timer_cleared'],
|
||||
"timer.start() should have observed it has been cleared"
|
||||
);
|
||||
assert.notOk(
|
||||
hasTimedOut,
|
||||
"timer should not have timed out immediately after reset"
|
||||
);
|
||||
|
||||
await advanceTime(0);
|
||||
assert.ok(
|
||||
hasTimedOut,
|
||||
"timer should have timed out after reset timeout"
|
||||
);
|
||||
assert.verifySteps(
|
||||
['timer_reset_timeout'],
|
||||
"timer.reset() should have observed it has timed out"
|
||||
timer.exists(),
|
||||
"timer should have timed out on insert after 10s"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user