diff --git a/.npmignore b/.npmignore new file mode 100644 --- /dev/null +++ b/.npmignore @@ -0,0 +1,1 @@ +*.tgz \ No newline at end of file diff --git a/build.gradle b/build.gradle --- a/build.gradle +++ b/build.gradle @@ -7,6 +7,7 @@ def testDir = "$buildDir/test" task clean { doLast { delete buildDir + delete 'node_modules/@implab' } } @@ -42,7 +43,7 @@ task _buildTs(dependsOn: _npmInstall, ty task _packageMeta(type: Copy) { inputs.property("version", version) from('.') { - include 'package.json', 'readme.md', 'license', 'history.md' + include 'package.json', '.npmignore', 'readme.md', 'license', 'history.md' } into distDir doLast { diff --git a/docs/ActivatableMixin/activate.puml b/docs/ActivatableMixin/activate.puml new file mode 100644 --- /dev/null +++ b/docs/ActivatableMixin/activate.puml @@ -0,0 +1,25 @@ +@startuml + +participant Component as a +participant Other as b + +[-> a : activate(ct) +activate a +<-- a : promise +a -> a : onActivating(ct) +activate a +a -> b : doAsyncWork(ct) +deactivate a +deactivate a +activate b + +[-> b : ct.cancel +b --> a : reject(Cancelled) +deactivate b +activate a + +a -> a : setFailState() + +[<-- a : reject(Cancelled) + +@enduml \ No newline at end of file diff --git a/docs/cancellations.ru.md b/docs/cancellations.ru.md new file mode 100644 --- /dev/null +++ b/docs/cancellations.ru.md @@ -0,0 +1,257 @@ +# Cancellations. Отмена асинхронных операций + +Использование Promise позволяет организовать обработку результатов работы +асинхронных фукнций. Ключевые слова async/await позволяют работать с +асинхронными вызовами в стиле процедурного программирования, хотя по сути это +событиный подход. При всей своей красоте даннго подхода в нем умышленно +отсутсвует механизм отмены асинхронной операции, т.е. ее можно начать, но нельзя +отказаться от результатов ее выполнения, даже если это уже не требуется. + +Примером того, когда может потребоваться отмена является загрузка большого +файла, при которой пользователю отображается окно хода операции с возможностью +ее отмены. + +```ts +// имеется некоторый HTTP клиент +let client = new HttpClient(); + +// загружается большой файл, с использованием медленного канала +let data = await client.getJsonAsync('http://host/large-file.json'); + +``` + +Как поступить в данной ситуации, прежде всего нужно, чтобы сама операция +поддерживала возможность отмены, предположим, что для этого есть метод +`client.abort()`. + +```ts + +// имеется некоторый HTTP клиент +let client = new HttpClient(); + +// отображаем окно с информацией о ходе операции +let progressView = showProgress("Downloading, please wait..."); + +// код оборачивается в try/finally поскольку созданную форму нужно закрыть +try { + // загружается большой файл, с использованием медленного канала + // здесь, в отличии от предыдущего примера, мы не дожидаемся результата, + // а запоминаем обещание в переменную downloadTask + let downloadTask = client.getJsonAsync('http://host/large-file.json'); + + // связываем событие нажатия кнопки с отменой загрузки + progressView.once('cancel', () => client.abort()); + + // ожидаем окончания загрузки данных + + let data = await downloadTask; +} finally { + // независимот от результата закрываем форму + // при этом также происходит ануллирование подписок на события + progressView.close(); +} + +``` + +Технические приведенное решение выглядит не плохо, но проблемы появляются, когда +требуется организовать отмену нескольких операций, особенно если они вложенные. + +```ts +// обновление информации о человеке на форме +async function updatePersonInfo(info) { + let client = new RestApiClient(); + + // выплнение нескольких асинхронных операций + let org = await client.getOrgAsync(info.orgId); + let city = await client.getCityAsync(info.cityId); + + // обновление содержимого представления + renderContent({ + person: info, + org: org, + city: city + }); +} + +``` + +Чтобы реализовать возможность отмены такой операции требуется, чтобы в логике +самой операции была реализована поддержка отмены. Для реализации этого +потребуется чтобы у операции была информация о запросе отмены, причем данная +информация относится именно к текущей операции. + +Информацию о состоянии запроса на отмену назовём **маркер отмены (cancellation +token)**. Поскольку маркер отмены тесно связан с операцией, его удобно +передавать в виде параметра, тогда код операции будет выглядеть так: + +```ts +// обновление информации о человеке на форме +// ct - маркер отмены +async function updatePersonInfo(info, ct) { + let client = new RestApiClient(); + + // выплнение нескольких асинхронных операций + // маркер отмены просто передается далее по цепочке вызовов, без + // дополнительных действий + let org = await client.getOrg(info.orgId, ct); + let city = await client.getCity(info.cityId, ct); + + // обновление содержимого представления + renderContent({ + person: info, + org: org, + city: city + }); +} + +/////////////////////////////////////////////////////////////////////////////// +// ... где-то в другом месте +/////////////////////////////////////////////////////////////////////////////// + +// отображаем окно с информацией о хоже операции +let progressView = showProgress("Loading, please wait..."); + +// создаем маркер отмены для операции на основе события 'cancel'. +let ct = new Cancellation(cancel => progressView.on('cancel', cancel)); + +// код оборачивается в try/finally поскольку созданную форму нужно закрыть +try { + // асинхронно получаем информацию о человеке + let data = await getPersonInfo(personId, ct); + + // асинхронно обновляем представление + await updatePersonInfo(data, ct); +} finally { + // независимот от результата закрываем форму + // при этом также происходит ануллирование подписок на события + progressView.close(); +} + +``` + +Таким образом тот, кто начинает асинхронную операцию заранее определяет как эта +опреция будет отменена. + +Важно понимать, что для реализации отмены операции +могут выделаться ресурсы требующие явного освобождения (DOM, таймеры, события), +об их освобождении по окончанию операции (успешном или нет) должен позаботиться +инициатор этой операции. `Cancellation` выступает только в роли посредника для +доставки события отмены операции до конечного получателя, он не отслеживает и +не освобождает ресурсы, кроме того, асинхронная операция может его попросту +проигнорировать. + +## `ICancellation` Маркер отмены операции + +Интерфейс маркера отмены операции. Используется асинхронными операциями, чтобы +получить оповещение о требуемой отмене. + +### `isSupported(): boolean` + +Определяет, может ли быть запрошена отмена операции через данный маркер. + +### `isRequested(): boolean` + +Возвращает текущее состояние запроса на отмену. + +### `throwIfRequested(): void` + +Если отмена была запрошена, бросает в качестве исключения причину отмены. + +### `register(cb: (e:any) => void): IDestroyable` + +Метод, зарегистрировать обработчик на запрос отмены. Если отмена была запрошена +зарегистрированный обработчик будет вызван ровно один раз, независимо от того, +был ли он зарегистрирован до или после запроса отмены. + +Если отмена уже была запрошена, обработчик будет вызван сразу при регистрации, +при этом исключения, которые могу возникнуть в обработчике не будут обработаны, +а передадуться наверх. + +Вызов данного метода приводит к выделению ресурсов, поэтому операция, +зарегистрировавшая обработчик должна освободить подписку, которую вернет метод. + +```ts +async function getAsync(url: string, ct: ICancellation = Cancellation.none) { + // переменная в которой будет запомнена подписка на запрос отмены + let reg; + try { + // оборачиваем операцию загрузки в Promise + return await new Promise((resolve, reject) => { + // объект Xhr + const xhr = new XMLHttpRequest(); + xhr.open("GET", url); + + // регистрируем обработчики Promise + xhr.onload = () => resolve(xhr.responseText); + xhr.onerror = () => reject(xhr.statusText); + + // отправляем запрос + xhr.send(); + + // подписываемся на запрос отмены + reg = ct.register((e) => { + reject(e); + xhr.abort(); + }); + }); + } finally { + if (reg) + reg.destroy(); + } +} + +``` + +Использование метода `register()` предполагается для организации отмены операций +не поддерживающих маркеры отмены. + +## `Cancellation` Источник отмены + +Класс используется для создания маркеров отмены. Позволяет создать маркер при +начале асинхронной операции и связать его, например, с событием DOM. + +Также маркер можно создавать, когда требуется сложное условие отмены текущей и +всех нижележещих операций. + +Как правило в большинстве операций достаточно маркера переданного в параметрах, +этот же маркер может передаваться ниже. + +### `constructor(exec: (cancel: (reason:any) => void ) => void )` + +Создает новый маркер, при помощи параметра и инициализирует его при помощи +фукнции, переданной в параметре `exec`. + +```ts + +let htimer; +let ct = new Cancellation(cancel => { + htimer = setTimeout(() => cancel("The request is timed out."), 1000); +}); + +try { + let text = await getAsync(url, ct); +} finally { + // инициатор должен освобождать ресурсы + // передача недействительного htimer не приводит ни к каким последствиям + clearTimeout(htimer); +} + +``` + +## `Cancellation.none: ICancellation` + +Статическое свойство только для чтения, в котором находится специальный токен +запроса отмены. Этот токен означает, что отмена никогда не может произойти. + +Данный токен рекомендуется использовать как значение по-умолчанию для +параметров, принимающих токен отмены. + +```ts + +async function load(url: string, ct: ICancellation = Cancellation.none) { + ct.throwIfRequested(); + + // ... the rest of method +} + +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json --- a/package-lock.json +++ b/package-lock.json @@ -133,7 +133,7 @@ }, "minimist": { "version": "0.0.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.5.tgz", "integrity": "sha1-16oye87PUY+RBqxrjwA/o7zqhWY=", "dev": true }, @@ -319,9 +319,9 @@ } }, "requirejs": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.5.tgz", - "integrity": "sha512-svnO+aNcR/an9Dpi44C7KSAy5fFGLtmPbaaCeQaklUz8BQhS64tWWIIlvEA5jrWICzlO/X9KSzSeXFnZdBu8nw==", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.6.tgz", + "integrity": "sha512-ipEzlWQe6RK3jkzikgCupiTbTvm4S0/CAU5GlgptkN5SO6F3u0UD0K18wy6ErDqiCyP4J4YYe1HuAShvsxePLg==", "dev": true }, "resolve": { @@ -413,9 +413,9 @@ } }, "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.0.3.tgz", + "integrity": "sha512-kk80vLW9iGtjMnIv11qyxLqZm20UklzuR2tL0QAnDIygIUIemcZMxlMWudl9OOt76H3ntVzcTiddQ1/pAAJMYg==", "dev": true }, "wrappy": { diff --git a/run-amd-tests.js b/run-amd-tests.js --- a/run-amd-tests.js +++ b/run-amd-tests.js @@ -14,6 +14,10 @@ requirejs.config({ { name: "test", location: "build/test" + }, + { + name: "dojo", + location: "node_modules/dojo" } ], nodeRequire: require diff --git a/src/js/components/ActivationController.js b/src/js/components/ActivationController.js deleted file mode 100644 --- a/src/js/components/ActivationController.js +++ /dev/null @@ -1,120 +0,0 @@ -define(["dojo/_base/declare", "../guard", "../safe", "../log/_LogMixin"], function (declare, guard, safe, _LogMixin) { - "use strict"; - return declare([_LogMixin], { - - _current: null, - - _pending: false, - - getCurrent: function () { - return this._current; - }, - - _start: function () { - if (this._pending) - throw new Error("The activation/decativation is already pending"); - this._pending = true; - }, - - _await: function (d) { - var me = this; - return d.then(function (x) { - me._pending = false; - return x; - }, function (e) { - me._pending = false; - throw e; - }); - }, - - activate: function (component) { - safe.argumentNotNull(component, "component"); - var me = this; - if (component.getController() !== this) - throw new Error("The specified component doesn't belong to this controller"); - - return me._await(guard(me, "_start").then(function () { - me._activate(component); - })); - }, - - _activate: function (component) { - var me = this; - if (me._current === component) - return guard(false); - - // before activation hook - return guard(me, "onActivating", [component]).then(function () { - // deactivate curent - if (me._current) - return me._current.deactivate(true).then(function () { - try { - me._current.onDeactivated(); - } catch (err) { - me.error(err); - } - // HACK raise deactivated event - try { - me.onDeactivated(me._current, component); - } catch (err) { - // deactivated shouldn't affect the process - me.error(err); - } - me._current = null; - - }); - }).then(function () { - return component.activate(true); - }).then(function () { - me._current = component; - try { - me.onActivated(component); - } catch (err) { - me.error(err); - } - - }); - - }, - - /** - * Деактивирует текущую компоненту. - * - * @async - * @returns true - компонента была деактивирована, либо нет активной - * компоненты. false - запрос на деактивацию - отклонен. - */ - deactivate: function () { - var me = this; - return me._await(guard(me, "_start").then(function () { - return me._deactivate(); - })); - }, - - _deactivate: function () { - var me = this; - if (!me._current) - return guard(false); - - return guard(me, "onDeactivating").then(function () { - return me._current.deactivate(true); - }).then(function () { - // HACK raise deactivated event - try { - me.onDeactivated(me._current); - } catch (err) { - me.error(err); - } - me._current = null; - }); - }, - - onActivating: function (component) {}, - - onDeactivating: function (component) {}, - - onDeactivated: function (component, next) {}, - - onActivated: function (component) {} - }); -}); \ No newline at end of file diff --git a/src/js/components/StateMachine.js b/src/js/components/StateMachine.js deleted file mode 100644 --- a/src/js/components/StateMachine.js +++ /dev/null @@ -1,34 +0,0 @@ -define([ "dojo/_base/declare", "../safe", "../text/format" ], function(declare, safe, format) { - return declare(null, { - states : null, - - current : null, - - constructor : function(opts) { - safe.argumentNotNull(opts, "opts"); - safe.argumentNotNull(opts.states, "opts.states"); - safe.argumentNotNull(opts.initial, "opts.initial"); - - this.states = opts.states; - this.current = opts.initial; - - if (safe.isNull(this.states[this.current])) - throw new Error("Invalid initial state " + this.current); - }, - - move : function(input, noThrow) { - safe.argumentNotNull(input, "input"); - - var next = this.states[this.current][input]; - if(safe.isNull(next)) { - if (noThrow) - return false; - else - throw new Error(format("Invalid transition {0}-{1}->?", this.current, input)); - } else { - this.current = next; - return true; - } - } - }); -}); \ No newline at end of file diff --git a/src/js/components/_ActivatableMixin.js b/src/js/components/_ActivatableMixin.js deleted file mode 100644 --- a/src/js/components/_ActivatableMixin.js +++ /dev/null @@ -1,153 +0,0 @@ -define(["dojo/_base/declare", "../guard", "./StateMachine", "../log/_LogMixin", ], function (declare, guard, StateMachine, _LogMixin) { - - var states = { - inactive: { - activate: "activating" - }, - activating: { - success: "active", - failed: "inactive" - }, - active: { - deactivate: "deactivating" - }, - deactivating: { - success: "inactive", - failed: "active" - } - }; - - return declare([_LogMixin], { - _controller: null, - - _active: null, - - constructor: function () { - this._active = new StateMachine({ - states: states, - initial: "inactive" - }); - }, - - /** - * @returns {Object} контроллер для активации текущей компоненты - */ - getController: function () { - return this._controller; - }, - - /** - * @param {Object} - * v Контроллер для активации текущей компоненты - */ - setController: function (v) { - this._controller = v; - }, - - /** - * @returns {Boolean} текущая компонента активна - */ - isActive: function () { - return this._active.current == "active"; - }, - - assertActive: function () { - if (!this.isActive()) - throw new Error("The object must be active to perform the operation"); - }, - - /** - * Активирует текущую компоненту, если у текущей компоненты задан - * контроллер, то активация будет осуществляться через него - * - * @async - * @param{Boolean} - * direct вызов должен осуществится напрямую, без участия - * контроллера. - * @return{Boolean} успешно/неуспешно - */ - activate: function (direct) { - var me = this; - if (!direct && this._controller) - return me._controller.activate(me).then(function () { - me.onActivated(); - }); - - me._active.move("activate"); - return guard(me, "onActivating").then(function () { - me.log("Activated"); - me._active.move("success"); - if (!me._controller) - me.onActivated(); - }, function (err) { - console.error(err); - me.error("Activation failed: {0}", err); - me._active.move("failed"); - throw err; - }); - }, - - /** - * Деактивирует текущую компоненту, если у компоненты задан контроллер, - * то деактивация будет осуществляться через него. - * - * @async - * @param{Boolean} direct вызов должен осуществится напрямую, без - * участия контроллера. - * - */ - deactivate: function (direct) { - var me = this; - if (!direct && me._controller) - return me._controller.deactivate(me).then(function () { - me.onDeactivated(); - }); - - me._active.move("deactivate"); - return guard(me, "onDeactivating").then(function () { - me.log("Deactivated"); - me._active.move("success"); - if (!me._controller) - me.onDeactivated(); - }, function (err) { - console.error(err); - me.error("Deactivation failed: {0}", err); - me.move("failed"); - throw err; - }); - - }, - - toogleActive: function () { - var me = this; - return (me.isActive() ? me.deactivate() : me.activate()).then(function () { - return me.isActive(); - }); - }, - - /** - * Событие вызывается перед активацией текущей компоненты - * - * @returns{Boolean|undefined} если false - активация будет отменена - */ - onActivating: function () {}, - - /** - * Событие вызывается перед деактивацией текущей компоненты - * - * @returns {Boolean|undefined} если false - деактивация будет отменена - */ - onDeactivating: function () {}, - - /** - * Событие вызывается после активации текущей компоненты - */ - onActivated: function () {}, - - /** - * Событие вызывается после деактивации текущей компоненты - */ - onDeactivated: function () {} - - }); -}); \ No newline at end of file diff --git a/src/js/di/Container.js b/src/js/di/Container.js --- a/src/js/di/Container.js +++ b/src/js/di/Container.js @@ -110,7 +110,7 @@ define([ if (typeof (config) === "string") { p = new Deferred(); if (!contextRequire) { - var shim = [config, new Uuid()].join(config.indexOf("/") != -1 ? "-" : "/"); + var shim = [config, Uuid()].join(config.indexOf("/") != -1 ? "-" : "/"); define(shim, ["require", config], function (ctx, data) { p.resolve([data, { contextRequire: ctx diff --git a/src/js/log/ConsoleLogChannel.js b/src/js/log/ConsoleLogChannel.js deleted file mode 100644 --- a/src/js/log/ConsoleLogChannel.js +++ /dev/null @@ -1,30 +0,0 @@ -define( - [ "dojo/_base/declare", "../text/format" ], - function(declare, format) { - return declare( - null, - { - name : null, - - constructor : function(name) { - this.name = name; - }, - - log : function() { - console.log(this._makeMsg(arguments)); - }, - - warn : function() { - console.warn(this._makeMsg(arguments)); - }, - - error : function() { - console.error(this._makeMsg(arguments)); - }, - - _makeMsg : function(args) { - return this.name ? this.name + " " + - format.apply(null, args) : format.apply(null, args); - } - }); - }); \ No newline at end of file diff --git a/src/js/log/_LogMixin.js b/src/js/log/_LogMixin.js deleted file mode 100644 --- a/src/js/log/_LogMixin.js +++ /dev/null @@ -1,67 +0,0 @@ -define([ "dojo/_base/declare" ], - -function(declare) { - var cls = declare(null, { - _logChannel : null, - - _logLevel : 1, - - constructor : function(opts) { - if (typeof opts == "object") { - if ("logChannel" in opts) - this._logChannel = opts.logChannel; - if ("logLevel" in opts) - this._logLevel = opts.logLevel; - } - }, - - getLogChannel : function() { - return this._logChannel; - }, - - setLogChannel : function(v) { - this._logChannel = v; - }, - - getLogLevel : function() { - return this._logLevel; - }, - - setLogLevel : function(v) { - this._logLevel = v; - }, - - log : function(format) { - if (this._logChannel && this._logLevel > 2) - this._logChannel.log.apply(this._logChannel, arguments); - }, - warn : function(format) { - if (this._logChannel && this._logLevel > 1) - this._logChannel.warn.apply(this._logChannel, arguments); - }, - error : function(format) { - if (this._logChannel && this._logLevel > 0) - this._logChannel.error.apply(this._logChannel, arguments); - }, - - /** - * Used to by widgets - */ - startup : function() { - var me = this, parent; - if (!me.getLogChannel()) { - parent = me; - while (parent = parent.getParent()) { - if (parent.getLogChannel) { - me.setLogChannel(parent.getLogChannel()); - if(parent.getLogLevel) - me.setLogLevel(parent.getLogLevel()); - break; - } - } - } - this.inherited(arguments); - } - }); - return cls; -}); \ No newline at end of file diff --git a/src/js/log/listeners/console.js b/src/js/log/listeners/console.js deleted file mode 100644 --- a/src/js/log/listeners/console.js +++ /dev/null @@ -1,25 +0,0 @@ -define([], function () { - if (console && console.log) - return function (ch, name, msg) { - - var args = [ch + ":"]; - - switch (name) { - case "warn": - case "error": - case "log": - break; - default: - args.push(name + ":"); - name = "log"; - } - - - if (msg instanceof Array) - args.push.apply(args, msg); - else - args.push(msg); - - console[name].apply(console, args); - }; -}); \ No newline at end of file diff --git a/src/js/log/trace.js b/src/js/log/trace.js --- a/src/js/log/trace.js +++ b/src/js/log/trace.js @@ -1,116 +1,50 @@ -define(["../text/format"], function (format) { +define(["./TraceSource"], function (TraceSource) { 'use strict'; - var listeners = []; - var channels = {}; - - var Trace = function (name) { - this.name = name; - this._subscribers = []; - }; - - Trace.prototype.debug = function () { - if (Trace.level >= 4) - this.notify("debug", format.apply(null, arguments)); - }; - - Trace.prototype.log = function () { - if (Trace.level >= 3) - this.notify("log", format.apply(null, arguments)); - }; - - Trace.prototype.warn = function () { - if (Trace.level >= 2) - this.notify("warn", format.apply(null, arguments)); - - }; + return { - Trace.prototype.error = function () { - if (Trace.level >= 1) - this.notify("error", format.apply(null, arguments)); - }; - - Trace.prototype.notify = function (name, msg) { - var me = this; - me._subscribers.forEach(function (cb) { - cb(me, name, msg); - }); - }; - - Trace.prototype.subscribe = function (cb) { - this._subscribers.push(cb); - }; - - Trace.prototype.toString = function () { - return this.name; - }; - - Trace.createChannel = function (type, name, cb) { - var chId = name; - if (channels[chId]) - return channels[chId]; - - var channel = new type(chId); - channels[chId] = channel; - - Trace._onNewChannel(chId, channel); - cb(channel); - }; + on: function (filter, cb) { + if (arguments.length == 1) { + cb = filter; + filter = undefined; + } + var test; + if (filter instanceof RegExp) { + test = function (chId) { + return filter.test(chId); + }; + } else if (filter instanceof Function) { + test = filter; + } else if (filter) { + test = function (chId) { + return chId == filter; + }; + } - Trace._onNewChannel = function (chId, ch) { - listeners.forEach(function (listener) { - listener(chId, ch); - }); - }; - - Trace.on = function (filter, cb) { - if (arguments.length == 1) { - cb = filter; - filter = undefined; - } - var d, test; - if (filter instanceof RegExp) { - test = function (chId) { - return filter.test(chId); - }; - } else if (filter instanceof Function) { - test = filter; - } else if (filter) { - test = function (chId) { - return chId == filter; - }; - } + if (test) { + TraceSource.on(function (source) { + if (test(source.id)) + source.on(cb); + }); + } else { + TraceSource.on(function (source) { + source.on(cb); + }); + } + }, - if (test) { - d = function(chId, ch) { - if(test(chId)) - ch.subscribe(cb); - }; - } else { - d = function(chId, ch) { - ch.subscribe(cb); - }; - } - listeners.push(d); - - for(var chId in channels) - d(chId,channels[chId]); - }; + load: function (id, require, cb) { + if (id) { + cb(TraceSource.get(id)); + } else if (require.module && require.module.mid) { + cb(TraceSource.get(require.module.mid)); + } else { + require(['module'], function (module) { + cb(TraceSource.get(module && module.id)); + }); + } + }, - Trace.load = function (id, require, cb) { - if (id) - Trace.createChannel(Trace, id, cb); - else if (require.module && require.module.mid) - Trace.createChannel(Trace, require.module.mid, cb); - else - require(['module'], function (module) { - Trace.createChannel(Trace, module && module.id, cb); - }); + dynamic: true, }; - - Trace.dynamic = true; - - Trace.level = 4; - - return Trace; }); \ No newline at end of file diff --git a/src/js/safe.js b/src/js/safe.js deleted file mode 100644 --- a/src/js/safe.js +++ /dev/null @@ -1,323 +0,0 @@ -define([], - - function () { - var _create = Object.create, - _keys = Object.keys; - - var safe = null; - safe = { - argumentNotNull: function (arg, name) { - if (arg === null || arg === undefined) - throw new Error("The argument " + name + " can't be null or undefined"); - }, - - argumentNotEmptyString: function (arg, name) { - if (typeof (arg) !== "string" || !arg.length) - throw new Error("The argument '" + name + "' must be a not empty string"); - }, - - argumentNotEmptyArray: function (arg, name) { - if (!(arg instanceof Array) || !arg.length) - throw new Error("The argument '" + name + "' must be a not empty array"); - }, - - argumentOfType: function (arg, type, name) { - if (!(arg instanceof type)) - throw new Error("The argument '" + name + "' type doesn't match"); - }, - - isNull: function (arg) { - return (arg === null || arg === undefined); - }, - - isPrimitive: function (arg) { - return (arg === null || arg === undefined || typeof (arg) === "string" || - typeof (arg) === "number" || typeof (arg) === "boolean"); - }, - - isInteger: function (arg) { - return parseInt(arg) == arg; - }, - - isNumber: function (arg) { - return parseFloat(arg) == arg; - }, - - isString: function (val) { - return typeof (val) == "string" || val instanceof String; - }, - - isNullOrEmptyString: function (str) { - if (str === null || str === undefined || - ((typeof (str) == "string" || str instanceof String) && str.length === 0)) - return true; - }, - - isNotEmptyArray: function (arg) { - return (arg instanceof Array && arg.length > 0); - }, - - /** - * Выполняет метод для каждого элемента массива, останавливается, когда - * либо достигнут конец массива, либо функция cb вернула - * значение. - * - * @param{Array | Object} obj массив элементов для просмотра - * @param{Function} cb функция, вызываемая для каждого элемента - * @param{Object} thisArg значение, которое будет передано в качестве - * this в cb. - * @returns Результат вызова функции cb, либо undefined - * если достигнут конец массива. - */ - each: function (obj, cb, thisArg) { - safe.argumentNotNull(cb, "cb"); - var i, x; - if (obj instanceof Array) { - for (i = 0; i < obj.length; i++) { - x = cb.call(thisArg, obj[i], i); - if (x !== undefined) - return x; - } - } else { - var keys = _keys(obj); - for (i = 0; i < keys.length; i++) { - var k = keys[i]; - x = cb.call(thisArg, obj[k], k); - if (x !== undefined) - return x; - } - } - }, - - /** - * Копирует свойства одного объекта в другой. - * - * @param{Any} dest объект в который нужно скопировать значения - * @param{Any} src источник из которого будут копироваться значения - * @tmpl{Object|Array} tmpl шаблон по которому будет происходить - * копирование. Если шаблон является массивом - * (список свойств), тогда значения этого массива - * являются именами свойсвт которые будут - * скопированы. Если шаблон является объектом (карта - * преобразования имен свойств src->dst), тогда - * копирование будет осуществляться только - * собственных свойств источника, присутсвующих в - * шаблоне, при этом значение свойства шаблона - * является именем свойства в которое будет - * произведено коприрование - */ - mixin: function (dest, src, tmpl) { - safe.argumentNotNull(dest, "dest"); - if (!src) - return dest; - - var keys, i, p; - if (arguments.length < 3) { - keys = _keys(src); - for (i = 0; i < keys.length; i++) { - p = keys[i]; - dest[p] = src[p]; - } - } else { - if (tmpl instanceof Array) { - for (i = 0; i < tmpl.length; i++) { - p = tmpl[i]; - if (p in src) - dest[p] = src[p]; - } - - } else { - keys = _keys(src); - for (i = 0; i < keys.length; i++) { - p = keys[i]; - if (p in tmpl) - dest[tmpl[p]] = src[p]; - } - } - } - return dest; - }, - - /** Wraps the specified function to emulate an asynchronous execution. - * @param{Object} thisArg [Optional] Object which will be passed as 'this' to the function. - * @param{Function|String} fn [Required] Function wich will be wrapped. - */ - async: function (fn, thisArg) { - if (arguments.length == 2 && !(fn instanceof Function)) - fn = thisArg[fn]; - - if (fn == null) - throw new Error("The function must be specified"); - - function wrapresult(x, e) { - if (e) { - return { - then: function (cb, eb) { - try { - return eb ? wrapresult(eb(e)) : this; - } catch (e2) { - return wrapresult(null, e2); - } - } - }; - } else { - if (x && x.then) - return x; - return { - then : function(cb) { - try { - return cb ? wrapresult(cb(x)) : this; - } catch(e2) { - return wrapresult(e2); - } - } - }; - } - } - - try { - return wrapresult(fn.apply(thisArg, arguments)); - } catch (e) { - return wrapresult(null, e); - } - }, - - create: function () { - if (console && console.warn) - console.warn("implab/safe::create is deprecated use Object.create instead"); - _create.apply(this, arguments); - }, - - delegate: function (target, method) { - if (!(method instanceof Function)) { - this.argumentNotNull(target, "target"); - method = target[method]; - } - - if (!(method instanceof Function)) - throw new Error("'method' argument must be a Function or a method name"); - - return function () { - return method.apply(target, arguments); - }; - }, - - /** - * Для каждого элемента массива вызывает указанную функцию и сохраняет - * возвращенное значение в массиве результатов. - * - * @remarks cb может выполняться асинхронно, при этом одновременно будет - * только одна операция. - * - * @async - */ - pmap: function (items, cb) { - safe.argumentNotNull(cb, "cb"); - - if (items && items.then instanceof Function) - return items.then(function (data) { - return safe.pmap(data, cb); - }); - - if (safe.isNull(items) || !items.length) - return items; - - var i = 0, - result = []; - - function next() { - var r, ri; - - function chain(x) { - result[ri] = x; - return next(); - } - - while (i < items.length) { - r = cb(items[i], i); - ri = i; - i++; - if (r && r.then) { - return r.then(chain); - } else { - result[ri] = r; - } - } - return result; - } - - return next(); - }, - - /** - * Для каждого элемента массива вызывает указанную функцию, результаты - * не сохраняются - * - * @remarks cb может выполняться асинхронно, при этом одновременно будет - * только одна операция. - * @async - */ - pfor: function (items, cb) { - safe.argumentNotNull(cb, "cb"); - - if (items && items.then instanceof Function) - return items.then(function (data) { - return safe.pmap(data, cb); - }); - - if (safe.isNull(items) || !items.length) - return items; - - var i = 0; - - function next() { - while (i < items.length) { - var r = cb(items[i], i); - i++; - if (r && r.then) - return r.then(next); - } - } - - return next(); - }, - - /** - * Выбирает первый элемент из последовательности, или обещания, если в - * качестве параметра используется обещание, оно должно вернуть массив. - * - * @param{Function} cb обработчик результата, ему будет передан первый - * элемент последовательности в случае успеха - * @param{Fucntion} err обработчик исключения, если массив пустой, либо - * не массив - * - * @remarks Если не указаны ни cb ни err, тогда функция вернет либо - * обещание, либо первый элемент. - * @async - */ - first: function (sequence, cb, err) { - if (sequence) { - if (sequence.then instanceof Function) { - return sequence.then(function (res) { - return safe.first(res, cb, err); - }, err); - } else if (sequence && "length" in sequence) { - if (sequence.length === 0) { - if (err) - return err(new Error("The sequence is empty")); - else - throw new Error("The sequence is empty"); - } - return cb ? cb(sequence[0]) : sequence[0]; - } - } - - if (err) - return err(new Error("The sequence is required")); - else - throw new Error("The sequence is required"); - } - }; - - return safe; - }); \ No newline at end of file diff --git a/src/js/text/format-compile.js b/src/js/text/format-compile.js --- a/src/js/text/format-compile.js +++ b/src/js/text/format-compile.js @@ -18,13 +18,13 @@ define( var espaceString = function(s) { if (!s) return s; - return "'" + s.replace(/('|\\)/g, "\\$1") + "'"; + return "'" + s.replace(/('|\\)/g, "\\$1").replace("\n","\\n") + "'"; }; var encode = function(s) { if (!s) return s; - return s.replace(/\\{|\\}|&|\\:/g, function(m) { + return s.replace(/\\{|\\}|&|\\:|\n/g, function(m) { return map[m] || m; }); }; 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,89 @@ +import { ICancellation, IDestroyable } from "./interfaces"; +import { argumentNotNull } from "./safe"; + +const destroyed = { + destroy() { + } +}; + +export class Cancellation implements ICancellation { + private _reason: any; + private _cbs: Array<(e) => void>; + + constructor(action: (cancel: (e) => void) => void) { + argumentNotNull(action, "action"); + + action(this._cancel.bind(this)); + } + + isSupported(): boolean { + return true; + } + throwIfRequested(): void { + if (this._reason) + throw this._reason; + } + + isRequested(): boolean { + return !!this._reason; + } + + register(cb: (e: any) => void): IDestroyable { + argumentNotNull(cb, "cb"); + + if (this._reason) { + cb(this._reason); + return destroyed; + } else { + if (!this._cbs) + this._cbs = [cb]; + else + this._cbs.push(cb); + + let me = this; + return { + destroy() { + me._unregister(cb); + } + }; + } + } + + private _unregister(cb) { + if(this._cbs) { + let i = this._cbs.indexOf(cb); + if ( i>=0 ) + this._cbs.splice(i,1); + } + } + + private _cancel(reason) { + if (this._reason) + return; + + this._reason = (reason = reason || new Error("Operation cancelled")); + + + if (this._cbs) { + this._cbs.forEach(cb => cb(reason)); + this._cbs = null; + } + } + + static readonly none: ICancellation = { + isSupported(): boolean { + return false; + }, + + throwIfRequested(): void { + }, + + isRequested(): boolean { + return false; + }, + + register(_cb: (e: any) => void): IDestroyable { + return destroyed; + } + }; +} \ No newline at end of file diff --git a/src/ts/Uuid.ts b/src/ts/Uuid.ts --- a/src/ts/Uuid.ts +++ b/src/ts/Uuid.ts @@ -6,6 +6,8 @@ // Copyright (c) 2010-2012 Robert Kieffer // MIT License - http://opensource.org/licenses/mit-license.php +declare var window: any; + let _window : any = 'undefined' !== typeof window ? window : null; // Unique ID creation requires a high quality random # generator. We diff --git a/src/ts/components/ActivatableMixin.ts b/src/ts/components/ActivatableMixin.ts new file mode 100644 --- /dev/null +++ b/src/ts/components/ActivatableMixin.ts @@ -0,0 +1,87 @@ +import { IActivationController, IActivatable, ICancellation } from '../interfaces'; +import { AsyncComponent } from './AsyncComponent'; +import { Cancellation } from '../Cancellation'; +import * as TraceSource from '../log/TraceSource'; + +type Constructor = new (...args: any[]) => T; + +const log = TraceSource.get('@implab/core/components/ActivatableMixin'); + +function ActivatableMixin>(Base: TBase) { + return class extends Base implements IActivatable { + _controller: IActivationController; + + _active: boolean; + + isActive() { + return this._active; + } + + getActivationController() { + return this._controller; + } + + setActivationController(controller: IActivationController) { + this._controller = controller; + } + + async onActivating(ct: ICancellation) { + if (this._controller) + await this._controller.activating(this, ct); + } + + async onActivated(ct: ICancellation) { + if (this._controller) + await this._controller.activated(this, ct); + } + + activate(ct: ICancellation = Cancellation.none) { + return this.runOperation(this._activateAsync.bind(this), ct); + } + + async _activateAsync(ct: ICancellation) { + if (this.isActive()) + return; + + await this.onActivating(ct); + this._active = true; + try { + await this.onActivated(ct); + } catch (e) { + log.error("Suppressed onActivated error: {0}", e); + } + } + + async onDeactivating(ct: ICancellation) { + if (this._controller) + await this._controller.deactivating(this, ct); + } + + async onDeactivated(ct: ICancellation) { + if (this._controller) + await this._controller.deactivated(this, ct); + } + + deactivate(ct: ICancellation = Cancellation.none) { + return this.runOperation(this._deactivateAsync.bind(this), ct); + } + + async _deactivateAsync(ct: ICancellation) { + if (!this.isActive()) + return; + await this.onDeactivating(ct); + this._active = false; + try { + await this.onDeactivated(ct); + } catch (e) { + log.error("Suppressed onDeactivated error: {0}", e); + } + } + } +} + +namespace ActivatableMixin { + export const traceSource = log; +} + +export = ActivatableMixin; \ No newline at end of file diff --git a/src/ts/components/AsyncComponent.ts b/src/ts/components/AsyncComponent.ts new file mode 100644 --- /dev/null +++ b/src/ts/components/AsyncComponent.ts @@ -0,0 +1,17 @@ +import { Cancellation } from "../Cancellation"; +import { IAsyncComponent, ICancellation } from "../interfaces"; + +export class AsyncComponent implements IAsyncComponent { + _completion: Promise = Promise.resolve(); + + getCompletion() { return this._completion }; + + runOperation(op: (ct: ICancellation) => any, ct: ICancellation = Cancellation.none) { + // TODO create cancellation source here + async function guard() { + await op(ct); + } + + return this._completion = guard(); + } +} \ 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,198 @@ +import { IObservable, IDestroyable, ICancellation } from '../interfaces'; +import { Cancellation } from '../Cancellation' +import { argumentNotNull } from '../safe'; + + +interface Handler { + (x: T): void +} + +interface Initializer { + (notify: Handler, error?: (e: any) => void, complete?: () => void): void; +} + +// TODO: think about to move this interfaces.ts and make it public +interface IObserver { + next(event: T): void + + error(e: any): void + + complete(): void +} + +const noop = () => {}; + +class Observable implements IObservable { + private _once = new Array>(); + + private _observers = new Array>(); + + + private _complete: boolean + + private _error: any + + constructor(func?: Initializer) { + if (func) + func( + this._notifyNext.bind(this), + this._notifyError.bind(this), + this._notifyCompleted.bind(this) + ); + } + + /** + * Registers handlers for the current observable object. + * + * @param next the handler for events + * @param error the handler for a error + * @param complete the handler for a completion + * @returns {IDestroyable} the handler for the current subscription, this + * handler can be used to unsubscribe from events. + * + */ + on(next: Handler, error?: Handler, complete?: () => void): IDestroyable { + argumentNotNull(next, "next"); + + let me = this; + + let observer: IObserver & IDestroyable = { + next: next, + error: error ? error.bind(null) : noop, + complete: complete ? complete.bind(null) : noop, + + destroy() { + me._removeObserver(this); + } + } + + this._addObserver(observer); + + + return observer; + } + + private _addObserver(observer: IObserver) { + if (this._complete) { + try { + if (this._error) + observer.error(this._error); + else + observer.complete(); + } catch (e) { + this.onObserverException(e); + } + } else { + this._observers.push(observer); + } + } + + /** + * Waits for the next event. This method can't be used to read messages + * as a sequence since it can skip some messages between calls. + * + * @param ct a cancellation token + */ + next(ct: ICancellation = Cancellation.none): Promise { + return new Promise((resolve, reject) => { + let observer: IObserver = { + next: resolve, + error: reject, + complete: () => reject("No more events are available") + }; + + if (this._addOnce(observer) && ct.isSupported()) { + ct.register((e) => { + this._removeOnce(observer); + reject(e); + }); + } + }); + } + + private _addOnce(observer: IObserver) { + if (this._complete) { + try { + if (this._error) + observer.error(this._error); + else + observer.complete(); + } catch (e) { + this.onObserverException(e); + } + return false; + } + + this._once.push(observer); + return true; + } + + protected onObserverException(e: any) { + } + + private _removeOnce(d: IObserver) { + let i = this._once.indexOf(d); + if (i >= 0) + this._once.splice(i, 1); + } + + private _removeObserver(d: IObserver) { + let i = this._observers.indexOf(d); + if (i >= 0) + this._observers.splice(i, 1); + } + + private _notify(guard: (observer: IObserver) => void) { + 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]); + } + + protected _notifyNext(evt: T) { + let guard = (observer: IObserver) => { + try { + observer.next(evt); + } catch (e) { + this.onObserverException(e); + } + } + + this._notify(guard); + } + + protected _notifyError(e: any) { + let guard = (observer: IObserver) => { + try { + observer.error(e); + } catch (e) { + this.onObserverException(e); + } + } + + this._notify(guard); + this._observers = []; + } + + protected _notifyCompleted() { + let guard = (observer: IObserver) => { + try { + observer.complete(); + } catch (e) { + this.onObserverException(e); + } + } + + this._notify(guard); + this._observers = []; + } +} + +namespace Observable { +} + +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,76 @@ +export interface IDestroyable { + destroy(); +} + +export interface ICancellation { + throwIfRequested(): void; + isRequested(): boolean; + isSupported(): boolean; + register(cb: (e: any) => void): IDestroyable; +} + +/** + * Интерфейс поддерживающий асинхронную активацию + */ +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 IObservable { + on(next: (x:T) => void, error?: (e:any) => void, complete?:() => void): IDestroyable; + next(ct?: ICancellation) : Promise; +} \ No newline at end of file diff --git a/src/ts/log/TraceEvent.ts b/src/ts/log/TraceEvent.ts new file mode 100644 --- /dev/null +++ b/src/ts/log/TraceEvent.ts @@ -0,0 +1,21 @@ +import * as TraceSource from './TraceSource' + +class TraceEvent { + readonly source: TraceSource; + + readonly level: Number; + + readonly arg: any; + + constructor(source: TraceSource, level: Number, arg: any) { + this.source = source; + this.level = level; + this.arg = arg; + } +} + +namespace TraceEvent { + +} + +export = TraceEvent \ No newline at end of file diff --git a/src/ts/log/TraceSource.ts b/src/ts/log/TraceSource.ts new file mode 100644 --- /dev/null +++ b/src/ts/log/TraceSource.ts @@ -0,0 +1,171 @@ +import * as format from '../text/format' +import { argumentNotNull } from '../safe'; +import * as Observable from '../components/Observable' +import { IDestroyable } from '../interfaces'; +import * as TraceEvent from './TraceEvent' + +class Registry { + static readonly instance = new Registry(); + + private _registry: object = new Object(); + private _listeners: object = new Object(); + private _nextCookie: number = 1; + + get(id: any): TraceSource { + argumentNotNull(id, "id"); + + if (this._registry[id]) + return this._registry[id]; + + var source = new TraceSource(id); + this._registry[id] = source; + this._onNewSource(source); + + return source; + } + + add(id: any, source: TraceSource) { + argumentNotNull(id, "id"); + argumentNotNull(source, "source"); + + this._registry[id] = source; + this._onNewSource(source); + } + + _onNewSource(source: TraceSource) { + for (let i in this._listeners) + this._listeners[i].call(null, source); + } + + on(handler: (source: TraceSource) => void): IDestroyable { + argumentNotNull(handler, "handler"); + var me = this; + + var cookie = this._nextCookie++; + + this._listeners[cookie] = handler; + + for (let i in this._registry) + handler(this._registry[i]); + + return { + destroy() { + delete me._listeners[cookie]; + } + }; + } +} + +class TraceSource extends Observable { + readonly id: any + + level: number + + constructor(id: any) { + super(); + this.id = id || new Object(); + } + + protected emit(level: number, arg: any) { + this._notifyNext(new TraceEvent(this, level, arg)); + } + + isDebugEnabled() { + return this.level >= TraceSource.DebugLevel; + } + + debug(msg: string, ...args: any[]) { + if (this.isEnabled(TraceSource.DebugLevel)) + this.emit(TraceSource.DebugLevel, format(msg, args)); + } + + isLogEnabled() { + return this.level >= TraceSource.LogLevel; + } + + log(msg: string, ...args: any[]) { + if (this.isEnabled(TraceSource.LogLevel)) + this.emit(TraceSource.LogLevel, format(msg, args)); + } + + isWarnEnabled() { + return this.level >= TraceSource.WarnLevel; + } + + warn(msg: string, ...args: any[]) { + if (this.isEnabled(TraceSource.WarnLevel)) + this.emit(TraceSource.WarnLevel, format(msg, args)); + } + + /** + * returns true if errors will be recorded. + */ + isErrorEnabled() { + return this.level >= TraceSource.ErrorLevel; + } + + /** + * Traces a error. + * + * @param msg the message. + * @param args parameters which will be substituted in the message. + */ + error(msg: string, ...args: any[]) { + if (this.isEnabled(TraceSource.ErrorLevel)) + this.emit(TraceSource.ErrorLevel, format(msg, args)); + } + + /** + * Checks whether the specified level is enabled for this + * trace source. + * + * @param level the trace level which should be checked. + */ + isEnabled(level: number) { + return (this.level >= level); + } + + /** + * Traces a raw event, passing data as it is to the underlying listeners + * + * @param level the level of the event + * @param arg the data of the event, can be a simple string or any object. + */ + traceEvent(level: number, arg: any) { + if (this.isEnabled(level)) + this.emit(level, arg); + } + + /** + * Register the specified handler to be called for every new and already + * created trace source. + * + * @param handler the handler which will be called for each trace source + */ + static on(handler: (source: TraceSource) => void) { + return Registry.instance.on(handler); + } + + /** + * Creates or returns already created trace source for the specified id. + * + * @param id the id for the trace source + */ + static get(id: any) { + return Registry.instance.get(id); + } +} + +namespace TraceSource { + export const DebugLevel = 400; + + export const LogLevel = 300; + + export const WarnLevel = 200; + + export const ErrorLevel = 100; + + export const SilentLevel = 0; +} + +export = TraceSource; \ No newline at end of file diff --git a/src/ts/log/writers/ConsoleWriter.ts b/src/ts/log/writers/ConsoleWriter.ts new file mode 100644 --- /dev/null +++ b/src/ts/log/writers/ConsoleWriter.ts @@ -0,0 +1,35 @@ +import { IObservable, IDestroyable, ICancellation } from "../../interfaces"; +import * as TraceEvent from '../TraceEvent'; +import { Cancellation } from "../../Cancellation"; +import * as TraceSource from "../TraceSource"; + +class ConsoleWriter implements IDestroyable { + readonly _subscriptions = new Array(); + + writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { + var subscription = source.on(this.writeEvent.bind(this)); + if (ct.isSupported()) { + ct.register(subscription.destroy.bind(subscription)); + } + this._subscriptions.push(subscription); + } + + writeEvent(next: TraceEvent) { + if (next.level >= TraceSource.LogLevel) { + console.log(next.source.id.toString(), next.arg); + } else if(next.level >= TraceSource.WarnLevel) { + console.warn(next.source.id.toString(), next.arg); + } else { + console.error(next.source.id.toString(), next.arg); + } + } + + destroy() { + this._subscriptions.forEach(x => x.destroy()); + } +} + +namespace ConsoleWriter { +} + +export = ConsoleWriter; \ No newline at end of file diff --git a/src/ts/safe.ts b/src/ts/safe.ts new file mode 100644 --- /dev/null +++ b/src/ts/safe.ts @@ -0,0 +1,231 @@ +export function argumentNotNull(arg, name) { + if (arg === null || arg === undefined) + throw new Error("The argument " + name + " can't be null or undefined"); +} + +export function argumentNotEmptyString(arg, name) { + if (typeof (arg) !== "string" || !arg.length) + throw new Error("The argument '" + name + "' must be a not empty string"); +} + +export function argumentNotEmptyArray(arg, name) { + if (!(arg instanceof Array) || !arg.length) + throw new Error("The argument '" + name + "' must be a not empty array"); +} + +export function argumentOfType(arg, type, name) { + if (!(arg instanceof type)) + throw new Error("The argument '" + name + "' type doesn't match"); +} + +export function isNull(arg) { + return (arg === null || arg === undefined); +} + +export function isPrimitive(arg) { + return (arg === null || arg === undefined || typeof (arg) === "string" || + typeof (arg) === "number" || typeof (arg) === "boolean"); +} + +export function isInteger(arg) { + return parseInt(arg) == arg; +} + +export function isNumber(arg) { + return parseFloat(arg) == arg; +} + +export function isString(val) { + return typeof (val) == "string" || val instanceof String; +} + +export function isNullOrEmptyString(str) { + if (str === null || str === undefined || + ((typeof (str) == "string" || str instanceof String) && str.length === 0)) + return true; +} + +export function isNotEmptyArray(arg) { + return (arg instanceof Array && arg.length > 0); +} + +/** + * Выполняет метод для каждого элемента массива, останавливается, когда + * либо достигнут конец массива, либо функция cb вернула + * значение. + * + * @param {Array | Object} obj массив элементов для просмотра + * @param {Function} cb функция, вызываемая для каждого элемента + * @param {Object} thisArg значение, которое будет передано в качестве + * this в cb. + * @returns Результат вызова функции cb, либо undefined + * если достигнут конец массива. + */ +export function each(obj, cb, thisArg) { + argumentNotNull(cb, "cb"); + var i, x; + if (obj instanceof Array) { + for (i = 0; i < obj.length; i++) { + x = cb.call(thisArg, obj[i], i); + if (x !== undefined) + return x; + } + } else { + var keys = Object.keys(obj); + for (i = 0; i < keys.length; i++) { + var k = keys[i]; + x = cb.call(thisArg, obj[k], k); + if (x !== undefined) + return x; + } + } +} + +/** Wraps the specified function to emulate an asynchronous execution. + * @param{Object} thisArg [Optional] Object which will be passed as 'this' to the function. + * @param{Function|String} fn [Required] Function wich will be wrapped. + */ +export function async(_fn: (...args: any[]) => any, thisArg) : (...args: any[]) => PromiseLike { + let fn = _fn; + + if (arguments.length == 2 && !(fn instanceof Function)) + fn = thisArg[fn]; + + if (fn == null) + throw new Error("The function must be specified"); + + function wrapresult(x, e?) : PromiseLike { + if (e) { + return { + then: function (cb, eb) { + try { + return eb ? wrapresult(eb(e)) : this; + } catch (e2) { + return wrapresult(null, e2); + } + } + }; + } else { + if (x && x.then) + return x; + return { + then: function (cb) { + try { + return cb ? wrapresult(cb(x)) : this; + } catch (e2) { + return wrapresult(e2); + } + } + }; + } + } + + return function () { + try { + return wrapresult(fn.apply(thisArg, arguments)); + } catch (e) { + return wrapresult(null, e); + } + }; +} + +export function delegate(target, _method: (string | Function)) { + let method : Function; + + if (!(_method instanceof Function)) { + argumentNotNull(target, "target"); + method = target[_method]; + } else { + method = _method; + } + + if (!(method instanceof Function)) + throw new Error("'method' argument must be a Function or a method name"); + + return function () { + return method.apply(target, arguments); + }; +} + +/** + * Для каждого элемента массива вызывает указанную функцию и сохраняет + * возвращенное значение в массиве результатов. + * + * @remarks cb может выполняться асинхронно, при этом одновременно будет + * только одна операция. + * + * @async + */ +export function pmap(items, cb) { + argumentNotNull(cb, "cb"); + + if (items && items.then instanceof Function) + return items.then(function (data) { + return pmap(data, cb); + }); + + if (isNull(items) || !items.length) + return items; + + var i = 0, + result = []; + + function next() { + var r, ri; + + function chain(x) { + result[ri] = x; + return next(); + } + + while (i < items.length) { + r = cb(items[i], i); + ri = i; + i++; + if (r && r.then) { + return r.then(chain); + } else { + result[ri] = r; + } + } + return result; + } + + return next(); +} + +/** + * Выбирает первый элемент из последовательности, или обещания, если в + * качестве параметра используется обещание, оно должно вернуть массив. + * + * @param {Function} cb обработчик результата, ему будет передан первый + * элемент последовательности в случае успеха + * @param {Function} err обработчик исключения, если массив пустой, либо + * не массив + * + * @remarks Если не указаны ни cb ни err, тогда функция вернет либо + * обещание, либо первый элемент. + * @async + */ +export function first(sequence: any, cb: Function, err: Function) { + if (sequence) { + if (sequence.then instanceof Function) { + return sequence.then(function (res) { + return first(res, cb, err); + }, err); + } else if (sequence && "length" in sequence) { + if (sequence.length === 0) { + if (err) + return err(new Error("The sequence is empty")); + else + throw new Error("The sequence is empty"); + } + return cb ? cb(sequence[0]) : sequence[0]; + } + } + + if (err) + return err(new Error("The sequence is required")); + else + throw new Error("The sequence is required"); +} \ No newline at end of file diff --git a/src/ts/text/format.d.ts b/src/ts/text/format.d.ts new file mode 100644 --- /dev/null +++ b/src/ts/text/format.d.ts @@ -0,0 +1,7 @@ +declare function format(format: string, ...args: any[]): string; + +declare namespace format { + +} + +export = format; \ No newline at end of file diff --git a/test/js/plan.js b/test/js/plan.js --- a/test/js/plan.js +++ b/test/js/plan.js @@ -1,1 +1,2 @@ -define(["./dummy", "./example"]); \ No newline at end of file +define(["./ActivatableTests", "./trace-test", "./TraceSourceTests", "./CancellationTests"]); +//define(["./CancellationTests"]); \ No newline at end of file diff --git a/test/js/trace-test.js b/test/js/trace-test.js new file mode 100644 --- /dev/null +++ b/test/js/trace-test.js @@ -0,0 +1,29 @@ +define(["tape"], function(tape) { + "use strict"; + var sourceId = '73a633f3-eab8-49b0-8601-07cae710f234'; + var sourceId2 = '3ba9c7cd-ed77-437b-9a2f-1cbeb1226b5b'; + tape('Load TraceSource for the module', function(t) { + require(["core/log/trace!" + sourceId, "core/log/TraceSource"], function(trace, TraceSource) { + t.equal(trace && trace.id, sourceId, "trace should be taken from the loader plugin parameter"); + + var count = 0; + + var h = TraceSource.on(function(x) { + if(x.id == sourceId || x.id == sourceId2) + count++; + }); + + t.equal(count, 1, "should see created channel immediatelly"); + t.equal(trace, TraceSource.get(sourceId), "should get same TraceSource from registry"); + t.equal(count, 1); + + TraceSource.get(sourceId2); + + t.equal(count, 2); + + h.destroy(); + + t.end(); + }); + }); +}); \ No newline at end of file diff --git a/test/ts/ActivatableTests.ts b/test/ts/ActivatableTests.ts new file mode 100644 --- /dev/null +++ b/test/ts/ActivatableTests.ts @@ -0,0 +1,108 @@ +import * as tape from 'tape'; +import * as ActivatableMixin from '@implab/core/components/ActivatableMixin'; +import { AsyncComponent } from '@implab/core/components/AsyncComponent'; +import { IActivationController, IActivatable, ICancellation } from '@implab/core/interfaces'; +import { Cancellation } from '@implab/core/Cancellation'; + +class SimpleActivatable extends ActivatableMixin(AsyncComponent) { + +} + +class MockActivationController implements IActivationController { + + _active: IActivatable = null; + + + getActive() : IActivatable { + return this._active; + } + + async deactivate() { + if (this._active) + await this._active.deactivate(); + this._active = null; + } + + async activate(component: IActivatable) { + if (!component || component.isActive()) + return; + component.setActivationController(this); + + await component.activate(); + } + + async activating(component: IActivatable, ct: ICancellation = Cancellation.none) { + if (component != this._active) + await this.deactivate(); + } + + async activated(component: IActivatable, ct: ICancellation = Cancellation.none) { + this._active = component; + } + + async deactivating(component: IActivatable, ct: ICancellation = Cancellation.none) { + + } + + async deactivated(component: IActivatable, ct: ICancellation = Cancellation.none) { + if (this._active == component) + this._active = null; + } +} + +tape('simple activation',async function(t){ + + let a = new SimpleActivatable(); + t.false(a.isActive()); + + await a.activate(); + t.true(a.isActive()); + + await a.deactivate(); + t.false(a.isActive()); + + t.end(); +}); + +tape('controller activation', async function(t) { + + let a = new SimpleActivatable(); + let c = new MockActivationController(); + + t.false(a.isActive(), "the component is not active by default"); + t.assert(c.getActive() == null, "the activation controller doesn't have an active component by default"); + t.assert(a.getActivationController() == null, "the component doesn't have an activation controller by default"); + + t.comment("Active the component through the controller"); + await c.activate(a); + t.true(a.isActive(), "The component should successfully activate"); + 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.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(); +}); + +tape('handle error in onActivating', async function(t) { + let a = new SimpleActivatable(); + + a.onActivating = async function() { + throw "Should fail"; + }; + + try { + await a.activate(); + t.fail("activation should fail"); + } catch { + } + + t.false(a.isActive(), "the component should remain inactive"); + + t.end(); +}); \ No newline at end of file diff --git a/test/ts/CancellationTests.ts b/test/ts/CancellationTests.ts new file mode 100644 --- /dev/null +++ b/test/ts/CancellationTests.ts @@ -0,0 +1,97 @@ +import * as tape from 'tape'; +import { Cancellation } from '@implab/core/Cancellation'; +import { ICancellation } from '@implab/core/interfaces'; +import { delay } from './TestTraits'; + +tape('standalone cancellation', async t => { + + let doCancel: (e) => void; + + let ct = new Cancellation(cancel => { + doCancel = cancel; + }); + + let counter = 0; + let reason = "BILL"; + + t.true(ct.isSupported(), "Cancellation must be supported"); + t.false(ct.isRequested(), "Cancellation shouldn't be requested"); + ct.throwIfRequested(); + t.pass("The exception shouldn't be thrown unless the cancellation is requested"); + + ct.register(() => counter++); + t.equals(counter, 0, "counter should be zero"); + + ct.register(() => counter++).destroy(); + + doCancel(reason); + + t.true(ct.isRequested(), "Cancellation should be requested"); + t.equals(counter, 1, "The registered callback should be triggered"); + + ct.register(() => counter++); + t.equals(counter, 2, "The callback should be triggered immediately"); + + let msg; + ct.register((e) => msg = e); + t.equals(msg, reason, "The cancellation reason should be passed to callback"); + + try { + msg = null; + ct.throwIfRequested(); + t.fail("The exception should be thrown"); + } catch (e) { + msg = e; + } + t.equals(msg, reason, "The cancellation reason should be catched"); + + t.end(); +}); + +tape('async cancellation', async t => { + + let ct = new Cancellation(cancel => { + cancel("STOP!"); + }); + + try { + await delay(0, ct); + t.fail("Should thow the exception"); + } catch (e) { + t.equals(e, "STOP!", "Should throw the cancellation reason"); + } + + t.end(); +}); + +tape('cancel with external event', async t => { + let ct = new Cancellation((cancel) => { + setTimeout(x => cancel('STOP!'), 0); + }) + + try { + await delay(10000, ct); + t.fail("Should thow the exception"); + } catch (e) { + t.equals(e, "STOP!", "Should throw the cancellation reason"); + } + + t.end(); +}); + +tape('operation normal flow', async t => { + + let htimeout; + let ct = new Cancellation((cancel) => { + htimeout = setTimeout(() => cancel("STOP!"), 1000); + }); + + try { + await delay(0, ct); + t.pass("Should pass"); + } finally { + clearTimeout(htimeout); + } + + t.end(); +}); \ No newline at end of file diff --git a/test/ts/TestTraits.ts b/test/ts/TestTraits.ts new file mode 100644 --- /dev/null +++ b/test/ts/TestTraits.ts @@ -0,0 +1,63 @@ +import { IObservable, ICancellation, IDestroyable } from "../../build/dist/interfaces"; +import * as TraceEvent from '../../build/dist/log/TraceEvent'; +import { Cancellation } from "../../build/dist/Cancellation"; +import * as TraceSource from "../../build/dist/log/TraceSource"; +import * as tape from 'tape'; +import { argumentNotNull } from "../../build/dist/safe"; + +export class TapeWriter implements IDestroyable { + readonly _tape: tape.Test + + _subscriptions = new Array(); + + constructor(tape: tape.Test) { + argumentNotNull(tape, "tape"); + this._tape = tape; + } + + writeEvents(source: IObservable, ct: ICancellation = Cancellation.none) { + let subscription = source.on(this.writeEvent.bind(this)); + if (ct.isSupported()) { + ct.register(subscription.destroy.bind(subscription)); + } + this._subscriptions.push(subscription); + } + + writeEvent(next: TraceEvent) { + if (next.level >= TraceSource.LogLevel) { + this._tape.comment("LOG " + next.arg); + } else if (next.level >= TraceSource.WarnLevel) { + this._tape.comment("WARN " + next.arg); + } else { + this._tape.comment("ERROR " + next.arg); + } + } + + destroy() { + this._subscriptions.forEach(x => x.destroy()); + } +} + +export async function delay(timeout: number, ct: ICancellation = Cancellation.none) { + let un: IDestroyable; + + try { + await new Promise((resolve, reject) => { + if (ct.isRequested()) { + un = ct.register(reject); + } else { + let ht = setTimeout(() => { + resolve(); + }, timeout); + + un = ct.register(e => { + clearTimeout(ht); + reject(e); + }); + } + }); + } finally { + if(un) + un.destroy(); + }; +} \ No newline at end of file diff --git a/test/ts/TraceSourceTests.ts b/test/ts/TraceSourceTests.ts new file mode 100644 --- /dev/null +++ b/test/ts/TraceSourceTests.ts @@ -0,0 +1,69 @@ +import * as TraceSource from '@implab/core/log/TraceSource' +import * as tape from 'tape'; +import { TapeWriter } from './TestTraits'; + +const sourceId = 'test/TraceSourceTests'; + +tape('trace message', t => { + let trace = TraceSource.get(sourceId); + + trace.level = TraceSource.DebugLevel; + + let h = trace.on((ev) => { + t.equal(ev.source, trace, "sender should be the current trace source"); + t.equal(ev.level, TraceSource.DebugLevel, "level should be debug level"); + t.equal(ev.arg, "Hello, World!", "The message should be a formatted message"); + + t.end(); + }); + + 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((ev) => { + t.equal(ev.source, trace, "sender should be the current trace source"); + t.equal(ev.level, TraceSource.DebugLevel, "level should be debug level"); + t.equal(ev.arg, event, "The message should be the specified object"); + + t.end(); + }); + + trace.traceEvent(TraceSource.DebugLevel, event); + + h.destroy(); +}); + +tape('tape comment writer', async t => { + let writer = new TapeWriter(t); + + TraceSource.on(ts => { + writer.writeEvents(ts); + }); + + let trace = TraceSource.get(sourceId); + trace.level = TraceSource.DebugLevel; + + trace.log("Hello, {0}!", 'World'); + trace.log("Multi\n line"); + trace.warn("Look at me!"); + trace.error("DIE!"); + + writer.destroy(); + + trace.log("You shouldn't see it!"); + + t.comment("DONE"); + + t.end(); +}); \ No newline at end of file diff --git a/tsc.json b/tsc.json --- a/tsc.json +++ b/tsc.json @@ -4,7 +4,10 @@ "module": "amd", "sourceMap": true, "outDir" : "build/dist", - "declaration": true + "declaration": true, + "lib": [ + "ES2015" + ] }, "include" : [ "src/ts/**/*.ts" diff --git a/tsc.test.json b/tsc.test.json --- a/tsc.test.json +++ b/tsc.test.json @@ -4,7 +4,10 @@ "module": "amd", "sourceMap": true, "outDir" : "build/test", - "moduleResolution": "node" + "moduleResolution": "node", + "lib": [ + "ES2015" + ] }, "include" : [ "test/ts/**/*.ts"