1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
|
odoo.define('web.concurrency', function (require) {
"use strict";
/**
* Concurrency Utils
*
* This file contains a short collection of useful helpers designed to help with
* everything concurrency related in Odoo.
*
* The basic concurrency primitives in Odoo JS are the callback, and the
* promises. Promises (promise) are more composable, so we usually use them
* whenever possible. We use the jQuery implementation.
*
* Those functions are really nothing special, but are simply the result of how
* we solved some concurrency issues, when we noticed that a pattern emerged.
*/
var Class = require('web.Class');
return {
/**
* Returns a promise resolved after 'wait' milliseconds
*
* @param {int} [wait=0] the delay in ms
* @return {Promise}
*/
delay: function (wait) {
return new Promise(function (resolve) {
setTimeout(resolve, wait);
});
},
/**
* The DropMisordered abstraction is useful for situations where you have
* a sequence of operations that you want to do, but if one of them
* completes after a subsequent operation, then its result is obsolete and
* should be ignored.
*
* Note that is is kind of similar to the DropPrevious abstraction, but
* subtly different. The DropMisordered operations will all resolves if
* they complete in the correct order.
*/
DropMisordered: Class.extend({
/**
* @constructor
*
* @param {boolean} [failMisordered=false] whether mis-ordered responses
* should be failed or just ignored
*/
init: function (failMisordered) {
// local sequence number, for requests sent
this.lsn = 0;
// remote sequence number, seqnum of last received request
this.rsn = -1;
this.failMisordered = failMisordered || false;
},
/**
* Adds a promise (usually an async request) to the sequencer
*
* @param {Promise} promise to ensure add
* @returns {Promise}
*/
add: function (promise) {
var self = this;
var seq = this.lsn++;
var res = new Promise(function (resolve, reject) {
promise.then(function (result) {
if (seq > self.rsn) {
self.rsn = seq;
resolve(result);
} else if (self.failMisordered) {
reject();
}
}).guardedCatch(function (result) {
reject(result);
});
});
return res;
},
}),
/**
* The DropPrevious abstraction is useful when you have a sequence of
* operations that you want to execute, but you only care of the result of
* the last operation.
*
* For example, let us say that we have a _fetch method on a widget which
* fetches data. We want to rerender the widget after. We could do this::
*
* this._fetch().then(function (result) {
* self.state = result;
* self.render();
* });
*
* Now, we have at least two problems:
*
* - if this code is called twice and the second _fetch completes before the
* first, the end state will be the result of the first _fetch, which is
* not what we expect
* - in any cases, the user interface will rerender twice, which is bad.
*
* Now, if we have a DropPrevious::
*
* this.dropPrevious = new DropPrevious();
*
* Then we can wrap the _fetch in a DropPrevious and have the expected
* result::
*
* this.dropPrevious
* .add(this._fetch())
* .then(function (result) {
* self.state = result;
* self.render();
* });
*/
DropPrevious: Class.extend({
/**
* Registers a new promise and rejects the previous one
*
* @param {Promise} promise the new promise
* @returns {Promise}
*/
add: function (promise) {
if (this.currentDef) {
this.currentDef.reject();
}
var rejection;
var res = new Promise(function (resolve, reject) {
rejection = reject;
promise.then(resolve).catch(function (reason) {
reject(reason);
});
});
this.currentDef = res;
this.currentDef.reject = rejection;
return res;
}
}),
/**
* A (Odoo) mutex is a primitive for serializing computations. This is
* useful to avoid a situation where two computations modify some shared
* state and cause some corrupted state.
*
* Imagine that we have a function to fetch some data _load(), which returns
* a promise which resolves to something useful. Now, we have some code
* looking like this::
*
* return this._load().then(function (result) {
* this.state = result;
* });
*
* If this code is run twice, but the second execution ends before the
* first, then the final state will be the result of the first call to
* _load. However, if we have a mutex::
*
* this.mutex = new Mutex();
*
* and if we wrap the calls to _load in a mutex::
*
* return this.mutex.exec(function() {
* return this._load().then(function (result) {
* this.state = result;
* });
* });
*
* Then, it is guaranteed that the final state will be the result of the
* second execution.
*
* A Mutex has to be a class, and not a function, because we have to keep
* track of some internal state.
*/
Mutex: Class.extend({
init: function () {
this.lock = Promise.resolve();
this.queueSize = 0;
this.unlockedProm = undefined;
this._unlock = undefined;
},
/**
* Add a computation to the queue, it will be executed as soon as the
* previous computations are completed.
*
* @param {function} action a function which may return a Promise
* @returns {Promise}
*/
exec: function (action) {
var self = this;
var currentLock = this.lock;
var result;
this.queueSize++;
this.unlockedProm = this.unlockedProm || new Promise(function (resolve) {
self._unlock = resolve;
});
this.lock = new Promise(function (unlockCurrent) {
currentLock.then(function () {
result = action();
var always = function (returnedResult) {
unlockCurrent();
self.queueSize--;
if (self.queueSize === 0) {
self.unlockedProm = undefined;
self._unlock();
}
return returnedResult;
};
Promise.resolve(result).then(always).guardedCatch(always);
});
});
return this.lock.then(function () {
return result;
});
},
/**
* @returns {Promise} resolved as soon as the Mutex is unlocked
* (directly if it is currently idle)
*/
getUnlockedDef: function () {
return this.unlockedProm || Promise.resolve();
},
}),
/**
* A MutexedDropPrevious is a primitive for serializing computations while
* skipping the ones that where executed between a current one and before
* the execution of a new one. This is useful to avoid useless RPCs.
*
* You can read the Mutex description to understand its role ; for the
* DropPrevious part of this abstraction, imagine the following situation:
* you have a code that call the server with a fixed argument and a list of
* operations that only grows after each call and you only care about the
* RPC result (the server code doesn't do anything). If this code is called
* three times (A B C) and C is executed before B has started, it's useless
* to make an extra RPC (B) if you know that it won't have an impact and you
* won't use its result.
*
* Note that the promise returned by the exec call won't be resolved if
* exec is called before the first exec call resolution ; only the promise
* returned by the last exec call will be resolved (the other are rejected);
*
* A MutexedDropPrevious has to be a class, and not a function, because we
* have to keep track of some internal state. The exec function takes as
* argument an action (and not a promise as DropPrevious for example)
* because it's the MutexedDropPrevious role to trigger the RPC call that
* returns a promise when it's time.
*/
MutexedDropPrevious: Class.extend({
init: function () {
this.locked = false;
this.currentProm = null;
this.pendingAction = null;
this.pendingProm = null;
},
/**
* @param {function} action a function which may return a promise
* @returns {Promise}
*/
exec: function (action) {
var self = this;
var resolution;
var rejection;
if (this.locked) {
this.pendingAction = action;
var oldPendingDef = this.pendingProm;
this.pendingProm = new Promise(function (resolve, reject) {
resolution = resolve;
rejection = reject;
if (oldPendingDef) {
oldPendingDef.reject();
}
self.currentProm.reject();
});
this.pendingProm.resolve = resolution;
this.pendingProm.reject = rejection;
return this.pendingProm;
} else {
this.locked = true;
this.currentProm = new Promise(function (resolve, reject) {
resolution = resolve;
rejection = reject;
function unlock() {
self.locked = false;
if (self.pendingAction) {
var action = self.pendingAction;
var prom = self.pendingProm;
self.pendingAction = null;
self.pendingProm = null;
self.exec(action)
.then(prom.resolve)
.guardedCatch(prom.reject);
}
}
Promise.resolve(action())
.then(function (result) {
resolve(result);
unlock();
})
.guardedCatch(function (reason) {
reject(reason);
unlock();
});
});
this.currentProm.resolve = resolution;
this.currentProm.reject = rejection;
return this.currentProm;
}
}
}),
/**
* Rejects a promise as soon as a reference promise is either resolved or
* rejected
*
* @param {Promise} [target_def] the promise to potentially reject
* @param {Promise} [reference_def] the reference target
* @returns {Promise}
*/
rejectAfter: function (target_def, reference_def) {
return new Promise(function (resolve, reject) {
target_def.then(resolve).guardedCatch(reject);
reference_def.then(reject).guardedCatch(reject);
});
}
};
});
|