##// END OF EJS Templates
added CancellationTests...
cin -
r18:a8dda6a00a16 propose cancellat...
parent child
Show More
@@ -0,0 +1,1
1 *.tgz No newline at end of file
@@ -0,0 +1,257
1 # Cancellations. Отмена асинхронных операций
2
3 Использование Promise позволяет организовать обработку результатов работы
4 асинхронных фукнций. Ключевые слова async/await позволяют работать с
5 асинхронными вызовами в стиле процедурного программирования, хотя по сути это
6 событиный подход. При всей своей красоте даннго подхода в нем умышленно
7 отсутсвует механизм отмены асинхронной операции, т.е. ее можно начать, но нельзя
8 отказаться от результатов ее выполнения, даже если это уже не требуется.
9
10 Примером того, когда может потребоваться отмена является загрузка большого
11 файла, при которой пользователю отображается окно хода операции с возможностью
12 ее отмены.
13
14 ```ts
15 // имеется некоторый HTTP клиент
16 let client = new HttpClient();
17
18 // загружается большой файл, с использованием медленного канала
19 let data = await client.getJsonAsync('http://host/large-file.json');
20
21 ```
22
23 Как поступить в данной ситуации, прежде всего нужно, чтобы сама операция
24 поддерживала возможность отмены, предположим, что для этого есть метод
25 `client.abort()`.
26
27 ```ts
28
29 // имеется некоторый HTTP клиент
30 let client = new HttpClient();
31
32 // отображаем окно с информацией о хоже операции
33 let progressView = showProgress("Downloading, please wait...");
34
35 // код оборачивается в try/finally поскольку созданную форму нужно закрыть
36 try {
37 // загружается большой файл, с использованием медленного канала
38 // здесь, в отличии от предыдущего примера, мы не дожидаемся результата,
39 // а запоминаем обещание в переменную downloadTask
40 let downloadTask = client.getJsonAsync('http://host/large-file.json');
41
42 // связываем нажатие кнопки с отменой загрузки
43 progressView.once('cancel', () => client.abort());
44
45 // ожидаем окончания загрузки данных
46
47 let data = await downloadTask;
48 } finally {
49 // независимот от результата закрываем форму
50 // при этом также происходит ануллирование подписок на события
51 progressView.close();
52 }
53
54 ```
55
56 Технические приведенное решение выглядит не плохо, но проблемы появляются, когда
57 требуется организовать отмену нескольких операций, особенно если они вложенные.
58
59 ```ts
60 // обновление информации о человеке на форме
61 async function updatePersonInfo(info) {
62 let client = new RestApiClient();
63
64 // выплнение нескольких асинхронных операций
65 let org = await client.getOrgAsync(info.orgId);
66 let city = await client.getCityAsync(info.cityId);
67
68 // обновление содержимого представления
69 renderContent({
70 person: info,
71 org: org,
72 city: city
73 });
74 }
75
76 ```
77
78 Чтобы реализовать возможность отмены такой операции требуется, чтобы в логике
79 самой операции была реализована поддержка отмены. Для реализации этого
80 потребуется чтобы у операции была информация о запросе отмены, причем данная
81 информация относится именно к текущей операции.
82
83 Информацию о состоянии запроса на отмену назовет **маркер отмены (cancellation
84 token)**. Поскольку маркер отмены тесно связан с операцией, его удобно
85 передавать в виде параметра, тогда код операции будет выглядеть так:
86
87 ```ts
88 // обновление информации о человеке на форме
89 // ct - маркер отмены
90 async function updatePersonInfo(info, ct) {
91 let client = new RestApiClient();
92
93 // выплнение нескольких асинхронных операций
94 // маркер отмены просто передается далее по цепочке вызовов, без
95 // дополнительных действий
96 let org = await client.getOrg(info.orgId, ct);
97 let city = await client.getCity(info.cityId, ct);
98
99 // обновление содержимого представления
100 renderContent({
101 person: info,
102 org: org,
103 city: city
104 });
105 }
106
107 ///////////////////////////////////////////////////////////////////////////////
108 // ... где-то в другом месте
109 ///////////////////////////////////////////////////////////////////////////////
110
111 // отображаем окно с информацией о хоже операции
112 let progressView = showProgress("Loading, please wait...");
113
114 // создаем маркер отмены для операции на основе события 'cancel'.
115 let ct = new Cancellation(cancel => progressView.on('cancel', cancel));
116
117 // код оборачивается в try/finally поскольку созданную форму нужно закрыть
118 try {
119 // асинхронно получаем информацию о человеке
120 let data = await getPersonInfo(personId, ct);
121
122 // асинхронно обновляем представление
123 await updatePersonInfo(data, ct);
124 } finally {
125 // независимот от результата закрываем форму
126 // при этом также происходит ануллирование подписок на события
127 progressView.close();
128 }
129
130 ```
131
132 Таким образом тот, кто начинает асинхронную операцию заранее определяет как эта
133 опреция будет отменена.
134
135 Важно понимать, что для реализации отмены операции
136 могут выделаться ресурсы требующие явного освобождения (DOM, таймеры, события),
137 об их освобождении по окончанию операции (успешном или нет) должен позаботиться
138 инициатор этой операции. `Cancellation` выступает только в роли посредника для
139 доставки события отмены операции до конечного получателя, он не отслеживает и
140 не освобождает ресурсы, кроме того, асинхронная операция может его попросту
141 проигнорировать.
142
143 ## `ICancellation` Маркер отмены операции
144
145 Интерфейс маркера отмены операции. Используется асинхронными операциями, чтобы
146 получить оповещение о требуемой отмене.
147
148 ### `isSupported(): boolean`
149
150 Определяет, может ли быть запрошена отмена операции через данный маркер.
151
152 ### `isRequested(): boolean`
153
154 Возвращает текущее состояние запроса на отмену.
155
156 ### `throwIfRequested(): void`
157
158 Если отмена была запрошена, бросает в качестве исключения причину отмены.
159
160 ### `register(cb: (e:any) => void): IDestroyable`
161
162 Метод, зарегистрировать обработчик на запрос отмены. Если отмена была запрошена
163 зарегистрированный обработчик будет вызван ровно один раз, независимо от того,
164 был ли он зарегистрирован до или после запроса отмены.
165
166 Если отмена уже была запрошена, обработчик будет вызван сразу при регистрации,
167 при этом исключения, которые могу возникнуть в обработчике не будут обработаны,
168 а передадуться наверх.
169
170 Вызов данного метода приводит к выделению ресурсов, поэтому операция,
171 зарегистрировавшая обработчик должна освободить подписку, которую вернет метод.
172
173 ```ts
174 async function getAsync(url: string, ct: ICancellation = Cancellation.none) {
175 // переменная в которой будет запомнена подписка на запрос отмены
176 let reg;
177 try {
178 // оборачиваем операцию загрузки в Promise
179 return await new Promise<string>((resolve, reject) => {
180 // объект Xhr
181 const xhr = new XMLHttpRequest();
182 xhr.open("GET", url);
183
184 // регистрируем обработчики Promise
185 xhr.onload = () => resolve(xhr.responseText);
186 xhr.onerror = () => reject(xhr.statusText);
187
188 // отправляем запрос
189 xhr.send();
190
191 // подписываемся на запрос отмены
192 reg = ct.register((e) => {
193 reject(e);
194 xhr.abort();
195 });
196 });
197 } finally {
198 if (reg)
199 reg.destroy();
200 }
201 }
202
203 ```
204
205 Использование метода `register()` предполагается для организации отмены операций
206 не поддерживающих маркеры отмены.
207
208 ## `Cancellation` Источник отмены
209
210 Класс используется для создания маркеров отмены. Позволяет создать маркер при
211 начале асинхронной операции и связать его, например, с событием DOM.
212
213 Также маркер можно создавать, когда требуется сложное условие отмены текущей и
214 всех нижележещих операций.
215
216 Как правило в большинстве операций достаточно маркера переданного в параметрах,
217 этот же маркер может передаваться ниже.
218
219 ### `constructor(exec: (cancel: (reason:any) => void ) => void )`
220
221 Создает новый маркер, при помощи параметра и инициализирует его при помощи
222 фукнции, переданной в параметре `exec`.
223
224 ```ts
225
226 let htimer;
227 let ct = new Cancellation(cancel => {
228 htimer = setTimeout(() => cancel("The request is timed out."), 1000);
229 });
230
231 try {
232 let text = await getAsync(url, ct);
233 } finally {
234 // инициатор должен освобождать ресурсы
235 // передача недействительного htimer не приводит ни к каким последствиям
236 clearTimeout(htimer);
237 }
238
239 ```
240
241 ## `Cancellation.none: ICancellation`
242
243 Статическое свойство только для чтения, в котором находится специальный токен
244 запроса отмены. Этот токен означает, что отмена никогда не может произойти.
245
246 Данный токен рекомендуется использовать как значение по-умолчанию для
247 параметров, принимающих токен отмены.
248
249 ```ts
250
251 async function load(url: string, ct: ICancellation = Cancellation.none) {
252 ct.throwIfRequested();
253
254 // ... the rest of method
255 }
256
257 ``` No newline at end of file
@@ -0,0 +1,97
1 import * as tape from 'tape';
2 import { Cancellation } from '@implab/core/Cancellation';
3 import { ICancellation } from '@implab/core/interfaces';
4 import { delay } from './TestTraits';
5
6 tape('standalone cancellation', async t => {
7
8 let doCancel: (e) => void;
9
10 let ct = new Cancellation(cancel => {
11 doCancel = cancel;
12 });
13
14 let counter = 0;
15 let reason = "BILL";
16
17 t.true(ct.isSupported(), "Cancellation must be supported");
18 t.false(ct.isRequested(), "Cancellation shouldn't be requested");
19 ct.throwIfRequested();
20 t.pass("The exception shouldn't be thrown unless the cancellation is requested");
21
22 ct.register(() => counter++);
23 t.equals(counter, 0, "counter should be zero");
24
25 ct.register(() => counter++).destroy();
26
27 doCancel(reason);
28
29 t.true(ct.isRequested(), "Cancellation should be requested");
30 t.equals(counter, 1, "The registered callback should be triggered");
31
32 ct.register(() => counter++);
33 t.equals(counter, 2, "The callback should be triggered immediately");
34
35 let msg;
36 ct.register((e) => msg = e);
37 t.equals(msg, reason, "The cancellation reason should be passed to callback");
38
39 try {
40 msg = null;
41 ct.throwIfRequested();
42 t.fail("The exception should be thrown");
43 } catch (e) {
44 msg = e;
45 }
46 t.equals(msg, reason, "The cancellation reason should be catched");
47
48 t.end();
49 });
50
51 tape('async cancellation', async t => {
52
53 let ct = new Cancellation(cancel => {
54 cancel("STOP!");
55 });
56
57 try {
58 await delay(0, ct);
59 t.fail("Should thow the exception");
60 } catch (e) {
61 t.equals(e, "STOP!", "Should throw the cancellation reason");
62 }
63
64 t.end();
65 });
66
67 tape('cancel with external event', async t => {
68 let ct = new Cancellation((cancel) => {
69 setTimeout(x => cancel('STOP!'), 0);
70 })
71
72 try {
73 await delay(10000, ct);
74 t.fail("Should thow the exception");
75 } catch (e) {
76 t.equals(e, "STOP!", "Should throw the cancellation reason");
77 }
78
79 t.end();
80 });
81
82 tape('operation normal flow', async t => {
83
84 let htimeout;
85 let ct = new Cancellation((cancel) => {
86 htimeout = setTimeout(() => cancel("STOP!"), 1000);
87 });
88
89 try {
90 await delay(0, ct);
91 t.pass("Should pass");
92 } finally {
93 clearTimeout(htimeout);
94 }
95
96 t.end();
97 }); No newline at end of file
@@ -43,7 +43,7 task _buildTs(dependsOn: _npmInstall, ty
43 43 task _packageMeta(type: Copy) {
44 44 inputs.property("version", version)
45 45 from('.') {
46 include 'package.json', 'readme.md', 'license', 'history.md'
46 include 'package.json', '.npmignore', 'readme.md', 'license', 'history.md'
47 47 }
48 48 into distDir
49 49 doLast {
@@ -1,6 +1,11
1 import { ICancellation } from "./interfaces";
1 import { ICancellation, IDestroyable } from "./interfaces";
2 2 import { argumentNotNull } from "./safe";
3 3
4 const destroyed = {
5 destroy() {
6 }
7 };
8
4 9 export class Cancellation implements ICancellation {
5 10 private _reason: any;
6 11 private _cbs: Array<(e) => void>;
@@ -23,16 +28,32 export class Cancellation implements ICa
23 28 return !!this._reason;
24 29 }
25 30
26 register(cb: (e: any) => void): void {
31 register(cb: (e: any) => void): IDestroyable {
27 32 argumentNotNull(cb, "cb");
28 33
29 34 if (this._reason) {
30 35 cb(this._reason);
36 return destroyed;
31 37 } else {
32 38 if (!this._cbs)
33 39 this._cbs = [cb];
34 40 else
35 41 this._cbs.push(cb);
42
43 let me = this;
44 return {
45 destroy() {
46 me._unregister(cb);
47 }
48 };
49 }
50 }
51
52 private _unregister(cb) {
53 if(this._cbs) {
54 let i = this._cbs.indexOf(cb);
55 if ( i>=0 )
56 this._cbs.splice(i,1);
36 57 }
37 58 }
38 59
@@ -61,7 +82,8 export class Cancellation implements ICa
61 82 return false;
62 83 },
63 84
64 register(_cb: (e: any) => void): void {
85 register(_cb: (e: any) => void): IDestroyable {
86 return destroyed;
65 87 }
66 88 };
67 89 } No newline at end of file
@@ -8,7 +8,7 interface Handler<T> {
8 8 }
9 9
10 10 interface Initializer<T> {
11 (notify: Handler<T>, error?: (e: any) => void, complete?: () => void): (() => void) | void;
11 (notify: Handler<T>, error?: (e: any) => void, complete?: () => void): void;
12 12 }
13 13
14 14 // TODO: think about to move this interfaces.ts and make it public
@@ -20,19 +20,21 interface IObserver<T> {
20 20 complete(): void
21 21 }
22 22
23 class Observable<T> implements IObservable<T>, IDestroyable {
23 const noop = () => {};
24
25 class Observable<T> implements IObservable<T> {
24 26 private _once = new Array<IObserver<T>>();
25 27
26 28 private _observers = new Array<IObserver<T>>();
27 29
28 private _cleanup: (() => void) | void;
29 30
30 31 private _complete: boolean
31 32
32 33 private _error: any
33 34
34 35 constructor(func?: Initializer<T>) {
35 this._cleanup = func && func(
36 if (func)
37 func(
36 38 this._notifyNext.bind(this),
37 39 this._notifyError.bind(this),
38 40 this._notifyCompleted.bind(this)
@@ -56,16 +58,8 class Observable<T> implements IObservab
56 58
57 59 let observer: IObserver<T> & IDestroyable = {
58 60 next: next,
59
60 error(e: any) {
61 if (error)
62 error(e);
63 },
64
65 complete() {
66 if (complete)
67 complete();
68 },
61 error: error ? error.bind(null) : noop,
62 complete: complete ? complete.bind(null) : noop,
69 63
70 64 destroy() {
71 65 me._removeObserver(this);
@@ -133,30 +127,19 class Observable<T> implements IObservab
133 127 return true;
134 128 }
135 129
136 destroy() {
137 if (this._complete)
138 this._notifyCompleted();
139
140 let cleanup = this._cleanup;
141 if (cleanup) {
142 this._cleanup = null;
143 cleanup();
144 }
145 }
146
147 130 protected onObserverException(e: any) {
148 131 }
149 132
150 133 private _removeOnce(d: IObserver<T>) {
151 134 let i = this._once.indexOf(d);
152 135 if (i >= 0)
153 this._once.splice(i);
136 this._once.splice(i, 1);
154 137 }
155 138
156 139 private _removeObserver(d: IObserver<T>) {
157 140 let i = this._observers.indexOf(d);
158 141 if (i >= 0)
159 this._observers.splice(i);
142 this._observers.splice(i, 1);
160 143 }
161 144
162 145 private _notify(guard: (observer: IObserver<T>) => void) {
@@ -6,7 +6,7 export interface ICancellation {
6 6 throwIfRequested(): void;
7 7 isRequested(): boolean;
8 8 isSupported(): boolean;
9 register(cb: (e: any) => void): void;
9 register(cb: (e: any) => void): IDestroyable;
10 10 }
11 11
12 12 /**
@@ -1,1 +1,2
1 define(["./ActivatableTests", "./trace-test", "./TraceSourceTests"]); No newline at end of file
1 define(["./ActivatableTests", "./trace-test", "./TraceSourceTests", "./CancellationTests"]);
2 //define(["./CancellationTests"]); No newline at end of file
@@ -36,4 +36,28 export class TapeWriter implements IDest
36 36 destroy() {
37 37 this._subscriptions.forEach(x => x.destroy());
38 38 }
39 }
40
41 export async function delay(timeout: number, ct: ICancellation = Cancellation.none) {
42 let un: IDestroyable;
43
44 try {
45 await new Promise((resolve, reject) => {
46 if (ct.isRequested()) {
47 un = ct.register(reject);
48 } else {
49 let ht = setTimeout(() => {
50 resolve();
51 }, timeout);
52
53 un = ct.register(e => {
54 clearTimeout(ht);
55 reject(e);
56 });
57 }
58 });
59 } finally {
60 if(un)
61 un.destroy();
62 };
39 63 } No newline at end of file
@@ -1,6 +1,5
1 import * as TraceSource from '../../build/dist/log/TraceSource'
1 import * as TraceSource from '@implab/core/log/TraceSource'
2 2 import * as tape from 'tape';
3 import * as ConsoleWriter from '../../build/dist/log/writers/ConsoleWriter';
4 3 import { TapeWriter } from './TestTraits';
5 4
6 5 const sourceId = 'test/TraceSourceTests';
General Comments 0
You need to be logged in to leave comments. Login now