diff --git a/src/ts/Cancellation.ts b/src/ts/Cancellation.ts new file mode 100644 --- /dev/null +++ b/src/ts/Cancellation.ts @@ -0,0 +1,33 @@ +import { ICancellation } from "./interfaces"; + +export class Cancellation implements ICancellation { + isSupported(): boolean { + return false; + } + throwIfRequested(): void { + } + + isRequested(): boolean { + return false; + } + + register(_cb: (e:any) => void): void { + + } + + static readonly none : Cancellation = { + isSupported(): boolean { + return false; + }, + + throwIfRequested(): void { + }, + + isRequested(): boolean { + return false; + }, + + register(_cb: (e:any) => void): void { + } + }; +} \ No newline at end of file diff --git a/src/ts/EmptyCancellation.ts b/src/ts/EmptyCancellation.ts deleted file mode 100644 --- a/src/ts/EmptyCancellation.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ICancellation } from "./ICancellation"; - -export class EmptyCancellation implements ICancellation { - isSupported(): boolean { - return false; - } - throwIfRequested(): void { - } - - isRequested(): boolean { - return false; - } - - register(_cb: () => void): void { - - } - - static readonly default : EmptyCancellation = new EmptyCancellation(); - -} \ No newline at end of file diff --git a/src/ts/ICancellation.ts b/src/ts/ICancellation.ts deleted file mode 100644 --- a/src/ts/ICancellation.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface ICancellation { - throwIfRequested(): void; - isRequested(): boolean; - isSupported(): boolean; - register(cb: () => void): void; -} \ No newline at end of file diff --git a/src/ts/components/ActivatableMixin.ts b/src/ts/components/ActivatableMixin.ts --- a/src/ts/components/ActivatableMixin.ts +++ b/src/ts/components/ActivatableMixin.ts @@ -1,8 +1,6 @@ -import { IActivationController } from './IActivationController'; -import { IActivatable } from './IActivatable'; +import { IActivationController, IActivatable, ICancellation } from '../interfaces'; import { AsyncComponent } from './AsyncComponent'; -import { ICancellation } from '../ICancellation'; -import { EmptyCancellation } from '../EmptyCancellation'; +import { Cancellation } from '../Cancellation'; import * as TraceSource from '../log/TraceSource'; type Constructor = new (...args: any[]) => T; @@ -37,23 +35,21 @@ function ActivatableMixin; - - _deferred: { - resolve(): void - reject(reason: any): void - }; +export class AsyncComponent implements IAsyncComponent { + _completion: Promise = Promise.resolve(); getCompletion() { return this._completion }; - startOperation(ct: ICancellation = EmptyCancellation.default) { - if (this._deferred) - throw new Error("The async operation is already pending"); - - this._completion = new Promise((resolve, reject) => { - this._deferred = { - resolve: resolve, - reject: reject - } - }); - return ct; - } + runOperation(op: (ct: ICancellation) => any, ct: ICancellation = Cancellation.none) { + // TODO create cancellation source here + async function guard() { + await op(ct); + } - completeSuccess() { - this._deferred.resolve(); - this._deferred = null; - } - - completeFail(reason: any) { - this._deferred.reject(reason); - this._deferred = null; - } - - async runOperation(cb: (ct: ICancellation) => Promise, ct: ICancellation = EmptyCancellation.default) { - //safe.argumentNotNull(cb, "cb") - ct = this.startOperation(ct); - try { - await cb(ct); - this.completeSuccess(); - } catch(e) { - this.completeFail(e); - } + return this._completion = guard(); } } \ No newline at end of file diff --git a/src/ts/components/IActivatable.ts b/src/ts/components/IActivatable.ts deleted file mode 100644 --- a/src/ts/components/IActivatable.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { IActivationController } from "./IActivationController"; -import { ICancellation } from "../ICancellation"; - -/** - * Интерфейс поддерживающий асинхронную активацию - */ -export interface IActivatable { - /** - * @returns Boolean indicates the current state - */ - isActive(): boolean; - - /** - * Starts the component activation - * @param ct cancellation token for this operation - */ - activate(ct?: ICancellation): Promise; - - /** - * Starts the component deactivation - * @param ct cancellation token for this operation - */ - deactivate(ct?: ICancellation): Promise; - - /** - * Sets the activation controller for this component - * @param controller The activation controller - * - * Activation controller checks whether this component - * can be activated and manages the active state of the - * component - */ - setActivationController(controller: IActivationController); - - /** - * Gets the current activation controller for this component - */ - getActivationController(): IActivationController; -} \ No newline at end of file diff --git a/src/ts/components/IActivationController.ts b/src/ts/components/IActivationController.ts deleted file mode 100644 --- a/src/ts/components/IActivationController.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { IActivatable } from './IActivatable'; -import { ICancellation } from '../ICancellation'; -import { EmptyCancellation } from '../EmptyCancellation'; - -export interface IActivationController { - activating(component: IActivatable, ct?: ICancellation): Promise; - - activated(component: IActivatable, ct?: ICancellation): Promise; - - deactivating(component: IActivatable, ct?: ICancellation): Promise; - - deactivated(component: IActivatable, ct?: ICancellation): Promise; - - deactivate(ct?: ICancellation): Promise; - - activate(component: IActivatable, ct?: ICancellation): Promise; - - getActive(): IActivatable; -} \ No newline at end of file diff --git a/src/ts/components/Observable.ts b/src/ts/components/Observable.ts new file mode 100644 --- /dev/null +++ b/src/ts/components/Observable.ts @@ -0,0 +1,85 @@ +import { IObservable, IObserver, IDestroyable, ICancellation } from '../interfaces'; +import { Cancellation } from '../Cancellation' +import * as TraceSource from '../log/TraceSource' +import { argumentNotNull } from '../safe'; + +const trace = TraceSource.get('@implab/core/components/Observable'); + + +class Observable implements IObservable { + private _once = new Array>(); + + private readonly _observers = new Array>(); + + constructor(func: (notify: IObserver) => void) { + argumentNotNull(func, "func"); + + func(this._notify.bind(this)); + } + + on(observer: IObserver): IDestroyable { + argumentNotNull(observer, "observer"); + + this._observers.push(observer); + + let me = this; + return { + destroy() { + me._removeObserver(observer); + } + } + } + + wait(ct: ICancellation = Cancellation.none): Promise { + return new Promise((resolve, reject) => { + this._once.push(resolve); + if (ct.isSupported()) { + ct.register((e) => { + this._removeOnce(resolve); + reject(e); + }); + } + }); + } + + onObserverException(e: any) { + trace.error("Unhandled exception in the observer: {0}", e); + } + + private _removeOnce(d: IObserver) { + let i = this._once.indexOf(d); + if (i >= 0) + this._once.splice(i); + } + + private _removeObserver(d: IObserver) { + let i = this._observers.indexOf(d); + if (i >= 0) + this._observers.splice(i); + } + + private _notify(evt: T) { + let guard = (observer: IObserver) => { + try { + observer(evt); + } catch (e) { + this.onObserverException(e); + } + } + + if (this._once.length) { + for (let i = 0; i < this._once.length; i++) + guard(this._once[i]); + this._once = []; + } + + for (let i = 0; i < this._observers.length; i++) + guard(this._observers[i]); + } +} + +namespace Observable { + export const traceSource = trace; +} + +export = Observable; \ No newline at end of file diff --git a/src/ts/interfaces.ts b/src/ts/interfaces.ts new file mode 100644 --- /dev/null +++ b/src/ts/interfaces.ts @@ -0,0 +1,83 @@ +import { watchFile } from "fs"; + +export interface IDestroyable { + destroy(); +} + +export interface ICancellation { + throwIfRequested(): void; + isRequested(): boolean; + isSupported(): boolean; + register(cb: (e: any) => void): void; +} + +/** + * Интерфейс поддерживающий асинхронную активацию + */ +export interface IActivatable { + /** + * @returns Boolean indicates the current state + */ + isActive(): boolean; + + /** + * Starts the component activation + * @param ct cancellation token for this operation + */ + activate(ct?: ICancellation) : Promise; + + /** + * Starts the component deactivation + * @param ct cancellation token for this operation + */ + deactivate(ct?: ICancellation) : Promise; + + /** + * Sets the activation controller for this component + * @param controller The activation controller + * + * Activation controller checks whether this component + * can be activated and manages the active state of the + * component + */ + setActivationController(controller: IActivationController); + + /** + * Gets the current activation controller for this component + */ + getActivationController(): IActivationController; +} + +export interface IActivationController { + activating(component: IActivatable, ct?: ICancellation): Promise; + + activated(component: IActivatable, ct?: ICancellation): Promise; + + deactivating(component: IActivatable, ct?: ICancellation): Promise; + + deactivated(component: IActivatable, ct?: ICancellation): Promise; + + deactivate(ct?: ICancellation): Promise; + + activate(component: IActivatable, ct?: ICancellation): Promise; + + getActive(): IActivatable; +} + +export interface IAsyncComponent { + getCompletion(): Promise; +} + +export interface ICancellable { + cancel(reason?: any): void; +} + +export interface IObserver { + (x:T): void; +} + +export interface IObservable { + on(observer: IObserver): IDestroyable; + + wait(ct?: ICancellation) : Promise; +} \ No newline at end of file diff --git a/test/ts/ActivatableTests.ts b/test/ts/ActivatableTests.ts --- a/test/ts/ActivatableTests.ts +++ b/test/ts/ActivatableTests.ts @@ -78,15 +78,15 @@ tape('controller activation', async func t.comment("Active the component through the controller"); await c.activate(a); t.true(a.isActive(), "The component should successfully activate"); - t.assert(c.getActive() == a, "The controller should point to the activated component"); - t.assert(a.getActivationController() == c, "The component should point to the controller"); + t.equal(c.getActive(), a, "The controller should point to the activated component"); + t.equal(a.getActivationController(), c, "The component should point to the controller"); t.comment("Deactive the component throug the controller"); await c.deactivate(); t.false(a.isActive(), "The component should successfully deactivate"); - t.assert(c.getActive() == null, "The controller shouldn't point to any component"); - t.assert(a.getActivationController() == c, "The componet should point to it's controller"); + t.equal(c.getActive(), null, "The controller shouldn't point to any component"); + t.equal(a.getActivationController(), c, "The componet should point to it's controller"); t.end(); }); diff --git a/test/ts/TraceSourceTests.ts b/test/ts/TraceSourceTests.ts --- a/test/ts/TraceSourceTests.ts +++ b/test/ts/TraceSourceTests.ts @@ -3,7 +3,7 @@ import * as tape from 'tape'; const sourceId = 'test/TraceSourceTests'; -tape('', t => { +tape('trace message', t => { let trace = TraceSource.get(sourceId); trace.level = TraceSource.DebugLevel; @@ -11,7 +11,7 @@ tape('', t => { let h = trace.on((sender,level,msg) => { t.equal(sender, trace, "sender should be the current trace source"); t.equal(TraceSource.DebugLevel, level, "level should be debug level"); - t.equal(msg, "Hello, World!", "The message should be formatted correctly"); + t.equal(msg, "Hello, World!", "The message should be a formatted message"); t.end(); }); @@ -19,4 +19,26 @@ tape('', t => { trace.debug("Hello, {0}!", "World"); h.destroy(); +}); + +tape('trace event', t => { + let trace = TraceSource.get(sourceId); + + trace.level = TraceSource.DebugLevel; + + let event = { + name: "custom event" + }; + + let h = trace.on((sender,level,msg) => { + t.equal(sender, trace, "sender should be the current trace source"); + t.equal(TraceSource.DebugLevel, level, "level should be debug level"); + t.equal(msg, event, "The message should be the specified object"); + + t.end(); + }); + + trace.traceEvent(TraceSource.DebugLevel, event); + + h.destroy(); }); \ No newline at end of file