##// END OF EJS Templates
Merged in propose observables (pull request #2)...
m407 -
r29:e0b5fc764f84 merge default
parent child
Show More
@@ -0,0 +1,135
1 # Observable
2
3 Универсальный способ организации потока сообщений. Данный механизм может
4 использоваться для оповещения об изменениях состояний объектов или для доставки
5 самостоятельных событий, например, связанных с действиями пользователя.
6
7 Является реализацией классического шаблона наблюдателя с возможность сообщить
8 о коце потока событий. Данная реализация не содержит никаких дополнительных
9 функций, таких как фильтрация, канал с состоянием, преобразования сообщений и
10 т.п. Это сделано специально, чтобы реализация оставалась максимально простой.
11
12 Пример того, как можно создать последовательность из 10 событий:
13
14 ```ts
15 var events = new Observable(async (notify, error, complete) => {
16 // цикл в котором возникает событие
17 for(let i = 0; i < 10; i++) {
18 await delay(1000);
19 // в качестве данных передается номер события
20 notify(i);
21 }
22 // по окончании последовательности информируем, что событий больше не будет
23 compelte();
24 });
25
26 // создаем окно с отображением хода событий
27 var progress = showProgress({ min: 0, max: 9, current: 0});
28
29 // подписываемся на события
30 events.on(
31 // обработчик очередного события
32 msg => {
33 progress.setValue(msg);
34 }.
35 // обработчик ошибки
36 e => {
37 progress.showError(e);
38 },
39 // обработчик конца потока
40 () => {
41 progress.close();
42 }
43 );
44
45 // ожидание следующего события
46 let firstEvent = await events.next();
47 ```
48
49 `Observable` можно создавать из событий другого объекта, например, виджета:
50
51 ```ts
52 // клсс
53 class Canvas {
54 readonly mouseMove: IObservable<[number,number]>
55
56 postCreate() {
57 // превращаем события виджета в Observable
58 this.mouseMove = new Observable<[number,number]>((notify) => {
59 this.mousePad.on('mousemove',(e) => notify([e.clientX, e.clientY]) );
60 });
61 }
62 }
63
64 ```
65
66 Если объект инкапсулирует в себе `Observable`, он также может сохранить методы
67 для оповещения подписчиков для дальнейшего их использования внутри класса.
68
69 ```ts
70 // класс, который будет генерировать события местоположения
71 class PositionTracker implements IDestroyable {
72 // _nextPosition и _complete будут связаны с position при создании
73 // экземпляра PositionTracker.
74 _nextPosition: (pos: Position) => void
75 _complete: () => void
76
77 readonly position: IObservable<Position>
78
79 // конструктор
80 constructor(...args: any[]) {
81 super(args);
82
83 // создаем Observable
84 this.position = new Observable<Position>((notify, error, complete) => {
85 // сохраняем методы для оповещения о новом местоположении
86 this._nextPosition = notify;
87 // метод об оповещении конца потока событий
88 this._complete = complete
89 });
90 }
91
92 // метод для очистки ресурсов
93 destroy() {
94 this._complete();
95
96 super();
97 }
98 }
99
100 ```
101
102 ## Observable и последовательности
103
104 Можно сичтать, что `Observable` это некоторая аналогия итератора только в
105 парадигме событийного (или реактивного) программировния. Следует также понимать,
106 что при переходе от синхронного процедурного программирования к событийному так
107 же меняется и направление управления (Inverse Of Control), что означает
108 следующее:
109
110 * при работе с итераторами клиенты сами определяют момент чтения следующего
111 элемента последовательности.
112 * при работе с `Observable` клиенты вынуждены обрабатывать эти события по мере
113 их поступления и не могут на это повлиять.
114
115 Последний пункт можно изменить применив, например, буффер или канал с
116 состоянием, т.е. очередь, но данные механизмы выходят за рамки простого шаблона
117 наблюдателя.
118
119 ```ts
120 while(1) {
121 // ожидаем следующее событие, по сути это подписка только на одно событие
122 let next = await events.next();
123
124 // такой цикл может пропускать сообщения, поскольку асинхронная операция
125 // позволит возобновить создание новых событий, на которые мы не подписаны
126 await processEvent(next);
127
128 // не только асинхронные операции могут привести к пропуску события
129 // например вызов метода, который приводит к созданию события так же
130 // приведет к тому, что созданное событие не будет обработано в текущем
131 // цикле
132 doSmthAndRiseEvent();
133 }
134
135 ``` No newline at end of file
@@ -0,0 +1,186
1 # Observable
2
3 Универсальный способ организации потока сообщений. Данный механизм может
4 использоваться для оповещения об изменениях состояний объектов или для доставки
5 самостоятельных событий, например, связанных с действиями пользователя.
6
7 Является реализацией классического шаблона наблюдателя с возможность сообщить
8 о коце потока событий. Данная реализация не содержит никаких дополнительных
9 функций, таких как фильтрация, канал с состоянием, преобразования сообщений и
10 т.п. Это сделано специально, чтобы реализация оставалась максимально простой.
11
12 Пример того, как можно создать последовательность из 10 событий:
13
14 ```ts
15 var events = new Observable(async (notify, error, complete) => {
16 // цикл в котором возникает событие
17 for(let i = 0; i < 10; i++) {
18 await delay(1000);
19 // в качестве данных передается номер события
20 notify(i);
21 }
22 // по окончании последовательности информируем, что событий больше не будет
23 compelte();
24 });
25
26 // создаем окно с отображением хода событий
27 var progress = showProgress({ min: 0, max: 9, current: 0});
28
29 // подписываемся на события
30 events.on(
31 // обработчик очередного события
32 msg => {
33 progress.setValue(msg);
34 }.
35 // обработчик ошибки
36 e => {
37 progress.showError(e);
38 },
39 // обработчик конца потока
40 () => {
41 progress.close();
42 }
43 );
44
45 // ожидание следующего события
46 let firstEvent = await events.next();
47 ```
48
49 `Observable` можно создавать из событий другого объекта, например, виджета:
50
51 ```ts
52 // клсс
53 class Canvas {
54 readonly mouseMove: IObservable<[number,number]>
55
56 postCreate() {
57 // превращаем события виджета в Observable
58 this.mouseMove = new Observable<[number,number]>((notify) => {
59 this.mousePad.on('mousemove',(e) => notify([e.clientX, e.clientY]) );
60 });
61 }
62 }
63
64 ```
65
66 Если объект инкапсулирует в себе `Observable`, он также может сохранить методы
67 для оповещения подписчиков для дальнейшего их использования внутри класса.
68
69 ```ts
70 // класс, который будет генерировать события местоположения
71 class PositionTracker implements IDestroyable {
72 // _nextPosition и _complete будут связаны с position при создании
73 // экземпляра PositionTracker.
74 _nextPosition: (pos: Position) => void
75 _complete: () => void
76
77 readonly position: IObservable<Position>
78
79 // конструктор
80 constructor(...args: any[]) {
81 super(args);
82
83 // создаем Observable
84 this.position = new Observable<Position>((notify, error, complete) => {
85 // сохраняем методы для оповещения о новом местоположении
86 this._nextPosition = notify;
87 // метод об оповещении конца потока событий
88 this._complete = complete
89 });
90 }
91
92 // метод для очистки ресурсов
93 destroy() {
94 this._complete();
95
96 super();
97 }
98 }
99 ```
100
101 Существует также несколько варинатов получения сообщений
102
103 ```ts
104 // регистрация метода для получений событий
105 let subscription = pushEvents.on((msg) => {
106 displayPopup(msg);
107 });
108
109 // подписку можно отменить, после чего обработчики больше не будут вызываться
110 subcription.destroy();
111
112 // если требуется получить только одно сообщение можно использовать
113 // асинхронный метод next(ct?: ICancellation)
114
115 let msg = await pushEvents.next();
116
117 // пример метода для получения координат с карты, который использует
118 // событие нажатия мышью для определения координат.
119
120 class Map {
121 /**
122
123 Получает координаты по щелчку мыши.
124
125 @async
126
127 @returns [lon,lat]
128
129 */
130 async peekCoordinates(ct: ICancellation = Cancellation.none) {
131 // получаем событие клика
132 let evt = this.viewport.click.next(ct);
133
134 // преобразуем позицию на экране в координаты карты
135 return this.clientToCoodinates([evt.clientx,evt.clientY]);
136 }
137 }
138
139
140 let map : Map; // где-то объявлено
141
142 // пример получения координат с карты
143 let coords = await map.peekCoordinates();
144
145 ```
146
147 ## Observable и последовательности
148
149 Можно сичтать, что `Observable` это некоторая аналогия итератора только в
150 парадигме событийного (или реактивного) программировния. Следует также понимать,
151 что при переходе от синхронного процедурного программирования к событийному так
152 же меняется и направление управления (Inverse Of Control), что означает
153 следующее:
154
155 * при работе с итераторами клиенты сами определяют момент чтения следующего
156 элемента последовательности.
157 * при работе с `Observable` клиенты вынуждены обрабатывать эти события по мере
158 их поступления и не могут на это повлиять.
159
160 Последний пункт можно изменить применив, например, буффер или канал с
161 состоянием, т.е. очередь, но данные механизмы выходят за рамки простого шаблона
162 наблюдателя.
163
164 ```ts
165 // обработка в цикле не гарантирует получения всех сообщений
166 while(1) {
167 // ожидаем следующее событие, по сути это подписка только на одно событие
168 let next = await events.next();
169
170 // такой цикл может пропускать сообщения, поскольку асинхронная операция
171 // позволит возобновить создание новых событий, на которые мы не подписаны
172 await processEvent(next);
173
174 // не только асинхронные операции могут привести к пропуску события
175 // например вызов метода, который приводит к созданию события так же
176 // приведет к тому, что созданное событие не будет обработано в текущем
177 // цикле
178 doSmthAndRiseEvent();
179 }
180
181 // для получения всех сообщений нужно регистрировать подписчика
182 events.on((data) => {
183 // будет вызван для всех сообщений
184 processEvent(data);
185 });
186 ``` No newline at end of file
@@ -0,0 +1,195
1 import { IObservable, IDestroyable, ICancellation } from './interfaces';
2 import { Cancellation } from './Cancellation'
3 import { argumentNotNull } from './safe';
4
5
6 interface Handler<T> {
7 (x: T): void
8 }
9
10 interface Initializer<T> {
11 (notify: Handler<T>, error?: (e: any) => void, complete?: () => void): void;
12 }
13
14 // TODO: think about to move this interfaces.ts and make it public
15 interface IObserver<T> {
16 next(event: T): void
17
18 error(e: any): void
19
20 complete(): void
21 }
22
23 const noop = () => {};
24
25 export class Observable<T> implements IObservable<T> {
26 private _once = new Array<IObserver<T>>();
27
28 private _observers = new Array<IObserver<T>>();
29
30
31 private _complete: boolean
32
33 private _error: any
34
35 constructor(func?: Initializer<T>) {
36 if (func)
37 func(
38 this._notifyNext.bind(this),
39 this._notifyError.bind(this),
40 this._notifyCompleted.bind(this)
41 );
42 }
43
44 /**
45 * Registers handlers for the current observable object.
46 *
47 * @param next the handler for events
48 * @param error the handler for a error
49 * @param complete the handler for a completion
50 * @returns {IDestroyable} the handler for the current subscription, this
51 * handler can be used to unsubscribe from events.
52 *
53 */
54 on(next: Handler<T>, error?: Handler<any>, complete?: () => void): IDestroyable {
55 argumentNotNull(next, "next");
56
57 let me = this;
58
59 let observer: IObserver<T> & IDestroyable = {
60 next: next,
61 error: error ? error.bind(null) : noop,
62 complete: complete ? complete.bind(null) : noop,
63
64 destroy() {
65 me._removeObserver(this);
66 }
67 };
68
69 this._addObserver(observer);
70
71
72 return observer;
73 }
74
75 private _addObserver(observer: IObserver<T>) {
76 if (this._complete) {
77 try {
78 if (this._error)
79 observer.error(this._error);
80 else
81 observer.complete();
82 } catch (e) {
83 this.onObserverException(e);
84 }
85 } else {
86 this._observers.push(observer);
87 }
88 }
89
90 /**
91 * Waits for the next event. This method can't be used to read messages
92 * as a sequence since it can skip some messages between calls.
93 *
94 * @param ct a cancellation token
95 */
96 next(ct: ICancellation = Cancellation.none): Promise<T> {
97 return new Promise<T>((resolve, reject) => {
98 let observer: IObserver<T> = {
99 next: resolve,
100 error: reject,
101 complete: () => reject("No more events are available")
102 };
103
104 if (this._addOnce(observer) && ct.isSupported()) {
105 ct.register((e) => {
106 this._removeOnce(observer);
107 reject(e);
108 });
109 }
110 });
111 }
112
113 private _addOnce(observer: IObserver<T>) {
114 if (this._complete) {
115 try {
116 if (this._error)
117 observer.error(this._error);
118 else
119 observer.complete();
120 } catch (e) {
121 this.onObserverException(e);
122 }
123 return false;
124 }
125
126 this._once.push(observer);
127 return true;
128 }
129
130 protected onObserverException(e: any) {
131 }
132
133 private _removeOnce(d: IObserver<T>) {
134 let i = this._once.indexOf(d);
135 if (i >= 0)
136 this._once.splice(i, 1);
137 }
138
139 private _removeObserver(d: IObserver<T>) {
140 let i = this._observers.indexOf(d);
141 if (i >= 0)
142 this._observers.splice(i, 1);
143 }
144
145 private _notify(guard: (observer: IObserver<T>) => void) {
146 if (this._once.length) {
147 for (let i = 0; i < this._once.length; i++)
148 guard(this._once[i]);
149 this._once = [];
150 }
151
152 for (let i = 0; i < this._observers.length; i++)
153 guard(this._observers[i]);
154 }
155
156 protected _notifyNext(evt: T) {
157 let guard = (observer: IObserver<T>) => {
158 try {
159 observer.next(evt);
160 } catch (e) {
161 this.onObserverException(e);
162 }
163 }
164
165 this._notify(guard);
166 }
167
168 protected _notifyError(e: any) {
169 let guard = (observer: IObserver<T>) => {
170 try {
171 observer.error(e);
172 } catch (e) {
173 this.onObserverException(e);
174 }
175 }
176
177 this._notify(guard);
178 this._observers = [];
179 this._complete = true;
180 }
181
182 protected _notifyCompleted() {
183 let guard = (observer: IObserver<T>) => {
184 try {
185 observer.complete();
186 } catch (e) {
187 this.onObserverException(e);
188 }
189 }
190
191 this._notify(guard);
192 this._observers = [];
193 this._complete = true;
194 }
195 } No newline at end of file
@@ -0,0 +1,74
1 import { TraceSource, DebugLevel } from '@implab/core/log/TraceSource'
2 import * as tape from 'tape';
3 import { TapeWriter, delay } from './TestTraits';
4 import { Observable } from '@implab/core/Observable';
5 import { IObservable } from '@implab/core/interfaces';
6
7 let trace = TraceSource.get("ObservableTests");
8
9 tape('events sequence example', async t => {
10
11
12 let events: IObservable<number>
13
14 let done = new Promise<void>((resolve) => {
15 events = new Observable<number>(async (notify, fail, complete) => {
16 for (let i = 0; i < 10; i++) {
17 await delay(0);
18 notify(i);
19 }
20 complete();
21 resolve();
22 });
23 });
24
25 let count = 0;
26 let complete = false;
27 events.on(x => count = count + x, null, () => complete = true);
28
29 let first = await events.next();
30
31 t.equals(first, 0, "the first event");
32 t.false(complete, "the sequence is not complete");
33
34 await done;
35
36 t.equals(count, 45, "the summ of the evetns");
37 t.true(complete, "the sequence is complete");
38
39 t.end();
40 });
41
42 tape('event sequence termination', async t => {
43 let events: IObservable<number>
44
45 let done = new Promise<void>((resolve) => {
46 events = new Observable<number>(async (notify, fail, complete) => {
47 await delay(0);
48 notify(1);
49 complete();
50 notify(2);
51 complete();
52 fail("Sequence terminated");
53 resolve();
54 });
55 });
56
57 let count = 0;
58 events.on(() => {}, (e) => count++, () => count++);
59
60 let first = await events.next();
61 t.equals(first, 1, "the first message");
62 try {
63 await events.next();
64 t.fail("shoud throw an exception");
65 } catch(e) {
66 t.pass("the sequence is terminated");
67 }
68
69 await done;
70
71 t.equals(count, 1, "the sequence must be terminated once");
72
73 t.end();
74 }); No newline at end of file
1 NO CONTENT: file renamed from docs/cancellations.ru.md to docs/en/cancellations.md
1 NO CONTENT: file copied from docs/cancellations.ru.md to docs/ru/cancellations.md
@@ -1,50 +1,52
1 define(["./TraceSource"], function (TraceSource) {
1 define(["./TraceSource"], function (TraceSource_1) {
2 2 'use strict';
3 3
4 var TraceSource = TraceSource_1.TraceSource;
5
4 6 return {
5 7
6 8 on: function (filter, cb) {
7 9 if (arguments.length == 1) {
8 10 cb = filter;
9 11 filter = undefined;
10 12 }
11 13 var test;
12 14 if (filter instanceof RegExp) {
13 15 test = function (chId) {
14 16 return filter.test(chId);
15 17 };
16 18 } else if (filter instanceof Function) {
17 19 test = filter;
18 20 } else if (filter) {
19 21 test = function (chId) {
20 22 return chId == filter;
21 23 };
22 24 }
23 25
24 26 if (test) {
25 27 TraceSource.on(function (source) {
26 28 if (test(source.id))
27 29 source.on(cb);
28 30 });
29 31 } else {
30 32 TraceSource.on(function (source) {
31 33 source.on(cb);
32 34 });
33 35 }
34 36 },
35 37
36 38 load: function (id, require, cb) {
37 39 if (id) {
38 40 cb(TraceSource.get(id));
39 41 } else if (require.module && require.module.mid) {
40 42 cb(TraceSource.get(require.module.mid));
41 43 } else {
42 44 require(['module'], function (module) {
43 45 cb(TraceSource.get(module && module.id));
44 46 });
45 47 }
46 48 },
47 49
48 50 dynamic: true,
49 51 };
50 52 }); No newline at end of file
@@ -1,87 +1,83
1 1 import { IActivationController, IActivatable, ICancellation } from '../interfaces';
2 2 import { AsyncComponent } from './AsyncComponent';
3 3 import { Cancellation } from '../Cancellation';
4 import * as TraceSource from '../log/TraceSource';
4 import { TraceSource } from '../log/TraceSource';
5 5
6 6 type Constructor<T = {}> = new (...args: any[]) => T;
7 7
8 8 const log = TraceSource.get('@implab/core/components/ActivatableMixin');
9 9
10 function ActivatableMixin<TBase extends Constructor<AsyncComponent>>(Base: TBase) {
10 export function ActivatableMixin<TBase extends Constructor<AsyncComponent>>(Base: TBase) {
11 11 return class extends Base implements IActivatable {
12 12 _controller: IActivationController;
13 13
14 14 _active: boolean;
15 15
16 16 isActive() {
17 17 return this._active;
18 18 }
19 19
20 20 getActivationController() {
21 21 return this._controller;
22 22 }
23 23
24 24 setActivationController(controller: IActivationController) {
25 25 this._controller = controller;
26 26 }
27 27
28 28 async onActivating(ct: ICancellation) {
29 29 if (this._controller)
30 30 await this._controller.activating(this, ct);
31 31 }
32 32
33 33 async onActivated(ct: ICancellation) {
34 34 if (this._controller)
35 35 await this._controller.activated(this, ct);
36 36 }
37 37
38 38 activate(ct: ICancellation = Cancellation.none) {
39 39 return this.runOperation(this._activateAsync.bind(this), ct);
40 40 }
41 41
42 42 async _activateAsync(ct: ICancellation) {
43 43 if (this.isActive())
44 44 return;
45 45
46 46 await this.onActivating(ct);
47 47 this._active = true;
48 48 try {
49 49 await this.onActivated(ct);
50 50 } catch (e) {
51 51 log.error("Suppressed onActivated error: {0}", e);
52 52 }
53 53 }
54 54
55 55 async onDeactivating(ct: ICancellation) {
56 56 if (this._controller)
57 57 await this._controller.deactivating(this, ct);
58 58 }
59 59
60 60 async onDeactivated(ct: ICancellation) {
61 61 if (this._controller)
62 62 await this._controller.deactivated(this, ct);
63 63 }
64 64
65 65 deactivate(ct: ICancellation = Cancellation.none) {
66 66 return this.runOperation(this._deactivateAsync.bind(this), ct);
67 67 }
68 68
69 69 async _deactivateAsync(ct: ICancellation) {
70 70 if (!this.isActive())
71 71 return;
72 72 await this.onDeactivating(ct);
73 73 this._active = false;
74 74 try {
75 75 await this.onDeactivated(ct);
76 76 } catch (e) {
77 77 log.error("Suppressed onDeactivated error: {0}", e);
78 78 }
79 79 }
80 80 }
81 81 }
82 82
83 namespace ActivatableMixin {
84 export const traceSource = log;
85 }
86
87 export = ActivatableMixin; No newline at end of file
83 export const traceSource = log; No newline at end of file
@@ -1,17 +1,40
1 1 import { Cancellation } from "../Cancellation";
2 import { IAsyncComponent, ICancellation } from "../interfaces";
2 import { IAsyncComponent, ICancellation, ICancellable, IDestroyable } from "../interfaces";
3 import { destroy } from "../safe";
3 4
4 export class AsyncComponent implements IAsyncComponent {
5 export class AsyncComponent implements IAsyncComponent, ICancellable {
6 _cancel: (e) => void;
7
5 8 _completion: Promise<void> = Promise.resolve();
6 9
7 10 getCompletion() { return this._completion };
8 11
9 12 runOperation(op: (ct: ICancellation) => any, ct: ICancellation = Cancellation.none) {
13 // create inner cancellation bound to the passed cancellation token
14 let h: IDestroyable;
15 let inner = new Cancellation(cancel => {
16
17 this._cancel = cancel;
18 h = ct.register(cancel);
19 });
20
10 21 // TODO create cancellation source here
11 async function guard() {
12 await op(ct);
22 let guard = async () => {
23 try {
24 await op(inner);
25 } finally {
26 // after the operation is complete we need to cleanup the
27 // resources
28 destroy(h);
29 this._cancel = null;
30 }
13 31 }
14 32
15 33 return this._completion = guard();
16 34 }
35
36 cancel(reason) {
37 if (this._cancel)
38 this._cancel(reason);
39 }
17 40 } No newline at end of file
@@ -1,171 +1,188
1 1 import * as format from '../text/format'
2 2 import { argumentNotNull } from '../safe';
3 import * as Observable from '../components/Observable'
3 import { Observable } from '../Observable'
4 4 import { IDestroyable } from '../interfaces';
5 import * as TraceEvent from './TraceEvent'
5
6 export const DebugLevel = 400;
7
8 export const LogLevel = 300;
9
10 export const WarnLevel = 200;
11
12 export const ErrorLevel = 100;
13
14 export const SilentLevel = 0;
15
16 export class TraceEvent {
17 readonly source: TraceSource;
18
19 readonly level: Number;
20
21 readonly arg: any;
22
23 constructor(source: TraceSource, level: Number, arg: any) {
24 this.source = source;
25 this.level = level;
26 this.arg = arg;
27 }
28 }
6 29
7 30 class Registry {
8 31 static readonly instance = new Registry();
9 32
10 33 private _registry: object = new Object();
11 34 private _listeners: object = new Object();
12 35 private _nextCookie: number = 1;
13 36
14 37 get(id: any): TraceSource {
15 38 argumentNotNull(id, "id");
16 39
17 40 if (this._registry[id])
18 41 return this._registry[id];
19 42
20 43 var source = new TraceSource(id);
21 44 this._registry[id] = source;
22 45 this._onNewSource(source);
23 46
24 47 return source;
25 48 }
26 49
27 50 add(id: any, source: TraceSource) {
28 51 argumentNotNull(id, "id");
29 52 argumentNotNull(source, "source");
30 53
31 54 this._registry[id] = source;
32 55 this._onNewSource(source);
33 56 }
34 57
35 58 _onNewSource(source: TraceSource) {
36 59 for (let i in this._listeners)
37 60 this._listeners[i].call(null, source);
38 61 }
39 62
40 63 on(handler: (source: TraceSource) => void): IDestroyable {
41 64 argumentNotNull(handler, "handler");
42 65 var me = this;
43 66
44 67 var cookie = this._nextCookie++;
45 68
46 69 this._listeners[cookie] = handler;
47 70
48 71 for (let i in this._registry)
49 72 handler(this._registry[i]);
50 73
51 74 return {
52 75 destroy() {
53 76 delete me._listeners[cookie];
54 77 }
55 78 };
56 79 }
57 80 }
58 81
59 class TraceSource extends Observable<TraceEvent> {
82 export class TraceSource {
60 83 readonly id: any
61 84
62 85 level: number
63 86
87 readonly events: Observable<TraceEvent>
88
89 _notifyNext: (arg: TraceEvent) => void
90
64 91 constructor(id: any) {
65 super();
92
66 93 this.id = id || new Object();
94 this.events = new Observable((next) => {
95 this._notifyNext = next;
96 })
67 97 }
68 98
69 99 protected emit(level: number, arg: any) {
70 100 this._notifyNext(new TraceEvent(this, level, arg));
71 101 }
72 102
73 103 isDebugEnabled() {
74 return this.level >= TraceSource.DebugLevel;
104 return this.level >= DebugLevel;
75 105 }
76 106
77 107 debug(msg: string, ...args: any[]) {
78 if (this.isEnabled(TraceSource.DebugLevel))
79 this.emit(TraceSource.DebugLevel, format(msg, args));
108 if (this.isEnabled(DebugLevel))
109 this.emit(DebugLevel, format(msg, args));
80 110 }
81 111
82 112 isLogEnabled() {
83 return this.level >= TraceSource.LogLevel;
113 return this.level >= LogLevel;
84 114 }
85 115
86 116 log(msg: string, ...args: any[]) {
87 if (this.isEnabled(TraceSource.LogLevel))
88 this.emit(TraceSource.LogLevel, format(msg, args));
117 if (this.isEnabled(LogLevel))
118 this.emit(LogLevel, format(msg, args));
89 119 }
90 120
91 121 isWarnEnabled() {
92 return this.level >= TraceSource.WarnLevel;
122 return this.level >= WarnLevel;
93 123 }
94 124
95 125 warn(msg: string, ...args: any[]) {
96 if (this.isEnabled(TraceSource.WarnLevel))
97 this.emit(TraceSource.WarnLevel, format(msg, args));
126 if (this.isEnabled(WarnLevel))
127 this.emit(WarnLevel, format(msg, args));
98 128 }
99 129
100 130 /**
101 131 * returns true if errors will be recorded.
102 132 */
103 133 isErrorEnabled() {
104 return this.level >= TraceSource.ErrorLevel;
134 return this.level >= ErrorLevel;
105 135 }
106 136
107 137 /**
108 138 * Traces a error.
109 139 *
110 140 * @param msg the message.
111 141 * @param args parameters which will be substituted in the message.
112 142 */
113 143 error(msg: string, ...args: any[]) {
114 if (this.isEnabled(TraceSource.ErrorLevel))
115 this.emit(TraceSource.ErrorLevel, format(msg, args));
144 if (this.isEnabled(ErrorLevel))
145 this.emit(ErrorLevel, format(msg, args));
116 146 }
117 147
118 148 /**
119 149 * Checks whether the specified level is enabled for this
120 150 * trace source.
121 151 *
122 152 * @param level the trace level which should be checked.
123 153 */
124 154 isEnabled(level: number) {
125 155 return (this.level >= level);
126 156 }
127 157
128 158 /**
129 159 * Traces a raw event, passing data as it is to the underlying listeners
130 160 *
131 161 * @param level the level of the event
132 162 * @param arg the data of the event, can be a simple string or any object.
133 163 */
134 164 traceEvent(level: number, arg: any) {
135 165 if (this.isEnabled(level))
136 166 this.emit(level, arg);
137 167 }
138 168
139 169 /**
140 170 * Register the specified handler to be called for every new and already
141 171 * created trace source.
142 172 *
143 173 * @param handler the handler which will be called for each trace source
144 174 */
145 175 static on(handler: (source: TraceSource) => void) {
146 176 return Registry.instance.on(handler);
147 177 }
148 178
149 179 /**
150 180 * Creates or returns already created trace source for the specified id.
151 181 *
152 182 * @param id the id for the trace source
153 183 */
154 184 static get(id: any) {
155 185 return Registry.instance.get(id);
156 186 }
157 187 }
158 188
159 namespace TraceSource {
160 export const DebugLevel = 400;
161
162 export const LogLevel = 300;
163
164 export const WarnLevel = 200;
165
166 export const ErrorLevel = 100;
167
168 export const SilentLevel = 0;
169 }
170
171 export = TraceSource; No newline at end of file
@@ -1,35 +1,29
1 1 import { IObservable, IDestroyable, ICancellation } from "../../interfaces";
2 import * as TraceEvent from '../TraceEvent';
3 2 import { Cancellation } from "../../Cancellation";
4 import * as TraceSource from "../TraceSource";
3 import { TraceEvent, LogLevel, WarnLevel } from "../TraceSource";
5 4
6 class ConsoleWriter implements IDestroyable {
5 export class ConsoleWriter implements IDestroyable {
7 6 readonly _subscriptions = new Array<IDestroyable>();
8 7
9 8 writeEvents(source: IObservable<TraceEvent>, ct: ICancellation = Cancellation.none) {
10 9 var subscription = source.on(this.writeEvent.bind(this));
11 10 if (ct.isSupported()) {
12 11 ct.register(subscription.destroy.bind(subscription));
13 12 }
14 13 this._subscriptions.push(subscription);
15 14 }
16 15
17 16 writeEvent(next: TraceEvent) {
18 if (next.level >= TraceSource.LogLevel) {
17 if (next.level >= LogLevel) {
19 18 console.log(next.source.id.toString(), next.arg);
20 } else if(next.level >= TraceSource.WarnLevel) {
19 } else if(next.level >= WarnLevel) {
21 20 console.warn(next.source.id.toString(), next.arg);
22 21 } else {
23 22 console.error(next.source.id.toString(), next.arg);
24 23 }
25 24 }
26 25
27 26 destroy() {
28 27 this._subscriptions.forEach(x => x.destroy());
29 28 }
30 }
31
32 namespace ConsoleWriter {
33 }
34
35 export = ConsoleWriter; No newline at end of file
29 } No newline at end of file
@@ -1,231 +1,236
1 1 export function argumentNotNull(arg, name) {
2 2 if (arg === null || arg === undefined)
3 3 throw new Error("The argument " + name + " can't be null or undefined");
4 4 }
5 5
6 6 export function argumentNotEmptyString(arg, name) {
7 7 if (typeof (arg) !== "string" || !arg.length)
8 8 throw new Error("The argument '" + name + "' must be a not empty string");
9 9 }
10 10
11 11 export function argumentNotEmptyArray(arg, name) {
12 12 if (!(arg instanceof Array) || !arg.length)
13 13 throw new Error("The argument '" + name + "' must be a not empty array");
14 14 }
15 15
16 16 export function argumentOfType(arg, type, name) {
17 17 if (!(arg instanceof type))
18 18 throw new Error("The argument '" + name + "' type doesn't match");
19 19 }
20 20
21 21 export function isNull(arg) {
22 22 return (arg === null || arg === undefined);
23 23 }
24 24
25 25 export function isPrimitive(arg) {
26 26 return (arg === null || arg === undefined || typeof (arg) === "string" ||
27 27 typeof (arg) === "number" || typeof (arg) === "boolean");
28 28 }
29 29
30 30 export function isInteger(arg) {
31 31 return parseInt(arg) == arg;
32 32 }
33 33
34 34 export function isNumber(arg) {
35 35 return parseFloat(arg) == arg;
36 36 }
37 37
38 38 export function isString(val) {
39 39 return typeof (val) == "string" || val instanceof String;
40 40 }
41 41
42 42 export function isNullOrEmptyString(str) {
43 43 if (str === null || str === undefined ||
44 44 ((typeof (str) == "string" || str instanceof String) && str.length === 0))
45 45 return true;
46 46 }
47 47
48 48 export function isNotEmptyArray(arg) {
49 49 return (arg instanceof Array && arg.length > 0);
50 50 }
51 51
52 52 /**
53 53 * Выполняет метод для каждого элемента массива, останавливается, когда
54 54 * либо достигнут конец массива, либо функция <c>cb</c> вернула
55 55 * значение.
56 56 *
57 57 * @param {Array | Object} obj массив элементов для просмотра
58 58 * @param {Function} cb функция, вызываемая для каждого элемента
59 59 * @param {Object} thisArg значение, которое будет передано в качестве
60 60 * <c>this</c> в <c>cb</c>.
61 61 * @returns Результат вызова функции <c>cb</c>, либо <c>undefined</c>
62 62 * если достигнут конец массива.
63 63 */
64 64 export function each(obj, cb, thisArg) {
65 65 argumentNotNull(cb, "cb");
66 66 var i, x;
67 67 if (obj instanceof Array) {
68 68 for (i = 0; i < obj.length; i++) {
69 69 x = cb.call(thisArg, obj[i], i);
70 70 if (x !== undefined)
71 71 return x;
72 72 }
73 73 } else {
74 74 var keys = Object.keys(obj);
75 75 for (i = 0; i < keys.length; i++) {
76 76 var k = keys[i];
77 77 x = cb.call(thisArg, obj[k], k);
78 78 if (x !== undefined)
79 79 return x;
80 80 }
81 81 }
82 82 }
83 83
84 84 /** Wraps the specified function to emulate an asynchronous execution.
85 85 * @param{Object} thisArg [Optional] Object which will be passed as 'this' to the function.
86 86 * @param{Function|String} fn [Required] Function wich will be wrapped.
87 87 */
88 88 export function async(_fn: (...args: any[]) => any, thisArg) : (...args: any[]) => PromiseLike<any> {
89 89 let fn = _fn;
90 90
91 91 if (arguments.length == 2 && !(fn instanceof Function))
92 92 fn = thisArg[fn];
93 93
94 94 if (fn == null)
95 95 throw new Error("The function must be specified");
96 96
97 97 function wrapresult(x, e?) : PromiseLike<any> {
98 98 if (e) {
99 99 return {
100 100 then: function (cb, eb) {
101 101 try {
102 102 return eb ? wrapresult(eb(e)) : this;
103 103 } catch (e2) {
104 104 return wrapresult(null, e2);
105 105 }
106 106 }
107 107 };
108 108 } else {
109 109 if (x && x.then)
110 110 return x;
111 111 return {
112 112 then: function (cb) {
113 113 try {
114 114 return cb ? wrapresult(cb(x)) : this;
115 115 } catch (e2) {
116 116 return wrapresult(e2);
117 117 }
118 118 }
119 119 };
120 120 }
121 121 }
122 122
123 123 return function () {
124 124 try {
125 125 return wrapresult(fn.apply(thisArg, arguments));
126 126 } catch (e) {
127 127 return wrapresult(null, e);
128 128 }
129 129 };
130 130 }
131 131
132 132 export function delegate(target, _method: (string | Function)) {
133 133 let method : Function;
134 134
135 135 if (!(_method instanceof Function)) {
136 136 argumentNotNull(target, "target");
137 137 method = target[_method];
138 138 } else {
139 139 method = _method;
140 140 }
141 141
142 142 if (!(method instanceof Function))
143 143 throw new Error("'method' argument must be a Function or a method name");
144 144
145 145 return function () {
146 146 return method.apply(target, arguments);
147 147 };
148 148 }
149 149
150 150 /**
151 151 * Для каждого элемента массива вызывает указанную функцию и сохраняет
152 152 * возвращенное значение в массиве результатов.
153 153 *
154 154 * @remarks cb может выполняться асинхронно, при этом одновременно будет
155 155 * только одна операция.
156 156 *
157 157 * @async
158 158 */
159 159 export function pmap(items, cb) {
160 160 argumentNotNull(cb, "cb");
161 161
162 162 if (items && items.then instanceof Function)
163 163 return items.then(function (data) {
164 164 return pmap(data, cb);
165 165 });
166 166
167 167 if (isNull(items) || !items.length)
168 168 return items;
169 169
170 170 var i = 0,
171 171 result = [];
172 172
173 173 function next() {
174 174 var r, ri;
175 175
176 176 function chain(x) {
177 177 result[ri] = x;
178 178 return next();
179 179 }
180 180
181 181 while (i < items.length) {
182 182 r = cb(items[i], i);
183 183 ri = i;
184 184 i++;
185 185 if (r && r.then) {
186 186 return r.then(chain);
187 187 } else {
188 188 result[ri] = r;
189 189 }
190 190 }
191 191 return result;
192 192 }
193 193
194 194 return next();
195 195 }
196 196
197 197 /**
198 198 * Выбирает первый элемент из последовательности, или обещания, если в
199 199 * качестве параметра используется обещание, оно должно вернуть массив.
200 200 *
201 201 * @param {Function} cb обработчик результата, ему будет передан первый
202 202 * элемент последовательности в случае успеха
203 203 * @param {Function} err обработчик исключения, если массив пустой, либо
204 204 * не массив
205 205 *
206 206 * @remarks Если не указаны ни cb ни err, тогда функция вернет либо
207 207 * обещание, либо первый элемент.
208 208 * @async
209 209 */
210 210 export function first(sequence: any, cb: Function, err: Function) {
211 211 if (sequence) {
212 212 if (sequence.then instanceof Function) {
213 213 return sequence.then(function (res) {
214 214 return first(res, cb, err);
215 215 }, err);
216 216 } else if (sequence && "length" in sequence) {
217 217 if (sequence.length === 0) {
218 218 if (err)
219 219 return err(new Error("The sequence is empty"));
220 220 else
221 221 throw new Error("The sequence is empty");
222 222 }
223 223 return cb ? cb(sequence[0]) : sequence[0];
224 224 }
225 225 }
226 226
227 227 if (err)
228 228 return err(new Error("The sequence is required"));
229 229 else
230 230 throw new Error("The sequence is required");
231 }
232
233 export function destroy(d: any) {
234 if (d && 'destroy' in d)
235 d.destroy();
231 236 } No newline at end of file
@@ -1,2 +1,3
1 define(["./ActivatableTests", "./trace-test", "./TraceSourceTests", "./CancellationTests"]);
2 //define(["./CancellationTests"]); No newline at end of file
1 //define(["./ActivatableTests", "./trace-test", "./TraceSourceTests", "./CancellationTests"]);
2 //define(["./CancellationTests"]);
3 define(["./ObservableTests"]); No newline at end of file
@@ -1,29 +1,30
1 1 define(["tape"], function(tape) {
2 2 "use strict";
3 3 var sourceId = '73a633f3-eab8-49b0-8601-07cae710f234';
4 4 var sourceId2 = '3ba9c7cd-ed77-437b-9a2f-1cbeb1226b5b';
5 5 tape('Load TraceSource for the module', function(t) {
6 require(["core/log/trace!" + sourceId, "core/log/TraceSource"], function(trace, TraceSource) {
6 require(["core/log/trace!" + sourceId, "core/log/TraceSource"], function(trace, TraceSource_1) {
7 var TraceSource = TraceSource_1.TraceSource;
7 8 t.equal(trace && trace.id, sourceId, "trace should be taken from the loader plugin parameter");
8 9
9 10 var count = 0;
10 11
11 12 var h = TraceSource.on(function(x) {
12 13 if(x.id == sourceId || x.id == sourceId2)
13 14 count++;
14 15 });
15 16
16 17 t.equal(count, 1, "should see created channel immediatelly");
17 18 t.equal(trace, TraceSource.get(sourceId), "should get same TraceSource from registry");
18 19 t.equal(count, 1);
19 20
20 21 TraceSource.get(sourceId2);
21 22
22 23 t.equal(count, 2);
23 24
24 25 h.destroy();
25 26
26 27 t.end();
27 28 });
28 29 });
29 30 }); No newline at end of file
@@ -1,108 +1,108
1 1 import * as tape from 'tape';
2 import * as ActivatableMixin from '@implab/core/components/ActivatableMixin';
2 import { ActivatableMixin} from '@implab/core/components/ActivatableMixin';
3 3 import { AsyncComponent } from '@implab/core/components/AsyncComponent';
4 4 import { IActivationController, IActivatable, ICancellation } from '@implab/core/interfaces';
5 5 import { Cancellation } from '@implab/core/Cancellation';
6 6
7 7 class SimpleActivatable extends ActivatableMixin(AsyncComponent) {
8 8
9 9 }
10 10
11 11 class MockActivationController implements IActivationController {
12 12
13 13 _active: IActivatable = null;
14 14
15 15
16 16 getActive() : IActivatable {
17 17 return this._active;
18 18 }
19 19
20 20 async deactivate() {
21 21 if (this._active)
22 22 await this._active.deactivate();
23 23 this._active = null;
24 24 }
25 25
26 26 async activate(component: IActivatable) {
27 27 if (!component || component.isActive())
28 28 return;
29 29 component.setActivationController(this);
30 30
31 31 await component.activate();
32 32 }
33 33
34 34 async activating(component: IActivatable, ct: ICancellation = Cancellation.none) {
35 35 if (component != this._active)
36 36 await this.deactivate();
37 37 }
38 38
39 39 async activated(component: IActivatable, ct: ICancellation = Cancellation.none) {
40 40 this._active = component;
41 41 }
42 42
43 43 async deactivating(component: IActivatable, ct: ICancellation = Cancellation.none) {
44 44
45 45 }
46 46
47 47 async deactivated(component: IActivatable, ct: ICancellation = Cancellation.none) {
48 48 if (this._active == component)
49 49 this._active = null;
50 50 }
51 51 }
52 52
53 53 tape('simple activation',async function(t){
54 54
55 55 let a = new SimpleActivatable();
56 56 t.false(a.isActive());
57 57
58 58 await a.activate();
59 59 t.true(a.isActive());
60 60
61 61 await a.deactivate();
62 62 t.false(a.isActive());
63 63
64 64 t.end();
65 65 });
66 66
67 67 tape('controller activation', async function(t) {
68 68
69 69 let a = new SimpleActivatable();
70 70 let c = new MockActivationController();
71 71
72 72 t.false(a.isActive(), "the component is not active by default");
73 73 t.assert(c.getActive() == null, "the activation controller doesn't have an active component by default");
74 74 t.assert(a.getActivationController() == null, "the component doesn't have an activation controller by default");
75 75
76 76 t.comment("Active the component through the controller");
77 77 await c.activate(a);
78 78 t.true(a.isActive(), "The component should successfully activate");
79 79 t.equal(c.getActive(), a, "The controller should point to the activated component");
80 80 t.equal(a.getActivationController(), c, "The component should point to the controller");
81 81
82 82 t.comment("Deactive the component throug the controller");
83 83 await c.deactivate();
84 84
85 85 t.false(a.isActive(), "The component should successfully deactivate");
86 86 t.equal(c.getActive(), null, "The controller shouldn't point to any component");
87 87 t.equal(a.getActivationController(), c, "The componet should point to it's controller");
88 88
89 89 t.end();
90 90 });
91 91
92 92 tape('handle error in onActivating', async function(t) {
93 93 let a = new SimpleActivatable();
94 94
95 95 a.onActivating = async function() {
96 96 throw "Should fail";
97 97 };
98 98
99 99 try {
100 100 await a.activate();
101 101 t.fail("activation should fail");
102 102 } catch {
103 103 }
104 104
105 105 t.false(a.isActive(), "the component should remain inactive");
106 106
107 107 t.end();
108 108 }); No newline at end of file
@@ -1,63 +1,62
1 import { IObservable, ICancellation, IDestroyable } from "../../build/dist/interfaces";
2 import * as TraceEvent from '../../build/dist/log/TraceEvent';
3 import { Cancellation } from "../../build/dist/Cancellation";
4 import * as TraceSource from "../../build/dist/log/TraceSource";
1 import { IObservable, ICancellation, IDestroyable } from "@implab/core/interfaces";
2 import { Cancellation } from "@implab/core/Cancellation";
3 import { TraceEvent, LogLevel, WarnLevel } from "@implab/core/log/TraceSource";
5 4 import * as tape from 'tape';
6 import { argumentNotNull } from "../../build/dist/safe";
5 import { argumentNotNull } from "@implab/core/safe";
7 6
8 7 export class TapeWriter implements IDestroyable {
9 8 readonly _tape: tape.Test
10 9
11 10 _subscriptions = new Array<IDestroyable>();
12 11
13 12 constructor(tape: tape.Test) {
14 13 argumentNotNull(tape, "tape");
15 14 this._tape = tape;
16 15 }
17 16
18 17 writeEvents(source: IObservable<TraceEvent>, ct: ICancellation = Cancellation.none) {
19 18 let subscription = source.on(this.writeEvent.bind(this));
20 19 if (ct.isSupported()) {
21 20 ct.register(subscription.destroy.bind(subscription));
22 21 }
23 22 this._subscriptions.push(subscription);
24 23 }
25 24
26 25 writeEvent(next: TraceEvent) {
27 if (next.level >= TraceSource.LogLevel) {
26 if (next.level >= LogLevel) {
28 27 this._tape.comment("LOG " + next.arg);
29 } else if (next.level >= TraceSource.WarnLevel) {
28 } else if (next.level >= WarnLevel) {
30 29 this._tape.comment("WARN " + next.arg);
31 30 } else {
32 31 this._tape.comment("ERROR " + next.arg);
33 32 }
34 33 }
35 34
36 35 destroy() {
37 36 this._subscriptions.forEach(x => x.destroy());
38 37 }
39 38 }
40 39
41 40 export async function delay(timeout: number, ct: ICancellation = Cancellation.none) {
42 41 let un: IDestroyable;
43 42
44 43 try {
45 44 await new Promise((resolve, reject) => {
46 45 if (ct.isRequested()) {
47 46 un = ct.register(reject);
48 47 } else {
49 48 let ht = setTimeout(() => {
50 49 resolve();
51 50 }, timeout);
52 51
53 52 un = ct.register(e => {
54 53 clearTimeout(ht);
55 54 reject(e);
56 55 });
57 56 }
58 57 });
59 58 } finally {
60 59 if(un)
61 60 un.destroy();
62 61 };
63 62 } No newline at end of file
@@ -1,69 +1,69
1 import * as TraceSource from '@implab/core/log/TraceSource'
1 import { TraceSource, DebugLevel } from '@implab/core/log/TraceSource'
2 2 import * as tape from 'tape';
3 3 import { TapeWriter } from './TestTraits';
4 4
5 5 const sourceId = 'test/TraceSourceTests';
6 6
7 7 tape('trace message', t => {
8 8 let trace = TraceSource.get(sourceId);
9 9
10 trace.level = TraceSource.DebugLevel;
10 trace.level = DebugLevel;
11 11
12 let h = trace.on((ev) => {
12 let h = trace.events.on((ev) => {
13 13 t.equal(ev.source, trace, "sender should be the current trace source");
14 t.equal(ev.level, TraceSource.DebugLevel, "level should be debug level");
14 t.equal(ev.level, DebugLevel, "level should be debug level");
15 15 t.equal(ev.arg, "Hello, World!", "The message should be a formatted message");
16 16
17 17 t.end();
18 18 });
19 19
20 20 trace.debug("Hello, {0}!", "World");
21 21
22 22 h.destroy();
23 23 });
24 24
25 25 tape('trace event', t => {
26 26 let trace = TraceSource.get(sourceId);
27 27
28 trace.level = TraceSource.DebugLevel;
28 trace.level = DebugLevel;
29 29
30 30 let event = {
31 31 name: "custom event"
32 32 };
33 33
34 let h = trace.on((ev) => {
34 let h = trace.events.on((ev) => {
35 35 t.equal(ev.source, trace, "sender should be the current trace source");
36 t.equal(ev.level, TraceSource.DebugLevel, "level should be debug level");
36 t.equal(ev.level, DebugLevel, "level should be debug level");
37 37 t.equal(ev.arg, event, "The message should be the specified object");
38 38
39 39 t.end();
40 40 });
41 41
42 trace.traceEvent(TraceSource.DebugLevel, event);
42 trace.traceEvent(DebugLevel, event);
43 43
44 44 h.destroy();
45 45 });
46 46
47 47 tape('tape comment writer', async t => {
48 48 let writer = new TapeWriter(t);
49 49
50 50 TraceSource.on(ts => {
51 writer.writeEvents(ts);
51 writer.writeEvents(ts.events);
52 52 });
53 53
54 54 let trace = TraceSource.get(sourceId);
55 trace.level = TraceSource.DebugLevel;
55 trace.level = DebugLevel;
56 56
57 57 trace.log("Hello, {0}!", 'World');
58 58 trace.log("Multi\n line");
59 59 trace.warn("Look at me!");
60 60 trace.error("DIE!");
61 61
62 62 writer.destroy();
63 63
64 64 trace.log("You shouldn't see it!");
65 65
66 66 t.comment("DONE");
67 67
68 68 t.end();
69 69 }); No newline at end of file
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now