diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..1f69649 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,26 @@ +{ + "indent": 4, + "boss": true, + "undef": true, + "curly": true, + "forin": true, + "unused": true, + "newcap": true, + "eqnull": true, + "jquery": true, + "browser": true, + "noempty": true, + "latedef": true, + "camelcase": true, + "quotmark": "single", + "scripturl": true, + "-W030": false, + "-W015": false, + "globals": { + "module": true, + "define": true, + "require": true, + "console": true, + "exports": true + } +} \ No newline at end of file diff --git a/gulpfile.js b/gulpfile.js index cb2a5d5..fd2781d 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,7 +2,8 @@ var gulp = require('gulp'), uglify = require('gulp-uglify'), rjs = require('gulp-requirejs'), rename = require('gulp-rename'), - merge = require('merge'); + merge = require('merge'), + jshint = require('gulp-jshint'); var DEST = './out'; @@ -12,7 +13,8 @@ var buildOptions = { paths: { 'conduitjs': './node_modules/postal/node_modules/conduitjs/lib/conduit', 'postal': './node_modules/postal/lib/postal', - 'lodash': './node_modules/postal/node_modules/lodash/lodash' + 'lodash': './node_modules/postal/node_modules/lodash/lodash', + 'pledges': './node_modules/pledges/build/release.min', }, include: ['index'], almond: true, @@ -26,38 +28,44 @@ var buildOptions = { }; /** - * Сборка standalone-версии без каких-либо зависимостей: + * Сборка standalone-версии без каких-либо зависимостей: * файл можно просто вставить на страницу как скрипт */ gulp.task('standalone', function() { return rjs(buildOptions) - .pipe(gulp.dest(DEST)) - .pipe(rename('event-bus.min.js')) - .pipe(uglify()) - .pipe(gulp.dest(DEST)); + .pipe(gulp.dest(DEST)) + .pipe(rename('event-bus.min.js')) + .pipe(uglify()) + .pipe(gulp.dest(DEST)); }); /** * Сборка версии для Require.js: содержит только внутренние * зависимости, а внешние (типа Conduit и Lo-Dash) в сборку - * не включаются. Также ссылки на эти внешние модули + * не включаются. Также ссылки на эти внешние модули * переименовываются: к ним добавляется `packages/` */ gulp.task('requirejs', function() { return rjs(merge.recursive(true, buildOptions, { - paths: { - 'lodash': 'empty:' - }, - wrap: { - startFile: './wrappers/requirejs/start.frag', - endFile: './wrappers/requirejs/end.frag' - }, - out: 'event-bus-requirejs.js' - })) - .pipe(gulp.dest(DEST)) - .pipe(rename('event-bus-requirejs.min.js')) - .pipe(uglify()) - .pipe(gulp.dest(DEST)); + paths: { + 'lodash': 'empty:' + }, + wrap: { + startFile: './wrappers/requirejs/start.frag', + endFile: './wrappers/requirejs/end.frag' + }, + out: 'event-bus-requirejs.js' + })) + .pipe(gulp.dest(DEST)) + .pipe(rename('event-bus-requirejs.min.js')) + .pipe(uglify()) + .pipe(gulp.dest(DEST)); }); -gulp.task('default', ['standalone', 'requirejs']); \ No newline at end of file +gulp.task('lint', function(argument) { + return gulp.src('./lib/*.js') + .pipe(jshint()) + .pipe(jshint.reporter('default')) +}); + +gulp.task('default', ['lint', 'standalone', 'requirejs']); \ No newline at end of file diff --git a/index.js b/index.js index 85ddd58..52b22a0 100644 --- a/index.js +++ b/index.js @@ -1,13 +1,14 @@ if (typeof module === 'object' && typeof define !== 'function') { - var define = function (factory) { + var define = function(factory) { module.exports = factory(require, exports, module); }; } define(function(require, exports, module) { var postal = require('postal'); - var facade = require('./lib/backbone-facade'); + var backboneFacade = require('./lib/backbone-facade'); var preserve = require('./lib/preserve'); + var requestResponse = require('./lib/request-response'); function extend(obj) { for (var i = 1, il = arguments.length, src; i < il; i++) { @@ -16,14 +17,21 @@ define(function(require, exports, module) { continue; } - for (var p in src) if (src.hasOwnProperty(p)) { - obj[p] = src[p]; + for (var p in src) { + if (src.hasOwnProperty(p)) { + obj[p] = src[p]; + } } } return obj; } - extend(postal.ChannelDefinition.prototype, facade); - return extend(preserve(postal), facade); + extend(postal.ChannelDefinition.prototype, backboneFacade); + + extend(preserve(postal), backboneFacade); + + requestResponse(postal); + + return postal; }); \ No newline at end of file diff --git a/lib/backbone-facade.js b/lib/backbone-facade.js index e2c5c88..c9602d2 100644 --- a/lib/backbone-facade.js +++ b/lib/backbone-facade.js @@ -1,9 +1,10 @@ if (typeof module === 'object' && typeof define !== 'function') { - var define = function (factory) { + var define = function(factory) { module.exports = factory(require, exports, module); }; } +/*jshint unused:false*/ define(function(require, exports, module) { var postal = require('postal'); @@ -20,7 +21,7 @@ define(function(require, exports, module) { * Если не указан (передали 2 аргумента), подписываемся на глобальное событие * @param {String} event Название события, на которе подписываемся * @param {Function} callback Обработчик события - * @param {Object} context Контекст выполнения обработчика событий + * @param {Object} context Контекст выполнения обработчика событий */ on: function(channel, events, callback, context) { var event, @@ -109,12 +110,14 @@ define(function(require, exports, module) { events = Object.keys(topics); } + var subsFilter = function(s) { + return (callback != null ? s.callback.target().__original === callback : true) && (context != null ? s.callback.context() === context : true); + }; + while (event = events.shift()) { subs = topics[event] || []; - subs = subs.filter(function(s) { - return (callback != null ? s.callback.target().__original === callback : true) && (context != null ? s.callback.context() === context : true); - }); + subs = subs.filter(subsFilter); postal.unsubscribe.apply(postal, subs); } diff --git a/lib/preserve.js b/lib/preserve.js index 48644e2..10c6365 100644 --- a/lib/preserve.js +++ b/lib/preserve.js @@ -5,17 +5,18 @@ * последнего вызова события. * * Используется следующая конвенция: если событие вызывается - * с префиксом `!`, то данные этого события сохраняются - * и передаются всем последующим подписчикам. Следующий вызов + * с префиксом `!`, то данные этого события сохраняются + * и передаются всем последующим подписчикам. Следующий вызов * этого же события без префикса `!` сбрасывает это состояние, * то есть все последующие подписчики его не получат */ if (typeof module === 'object' && typeof define !== 'function') { - var define = function (factory) { + var define = function(factory) { module.exports = factory(require, exports, module); }; } +/*jshint unused:false*/ define(function(require, exports, module) { return function(postal) { var storage = {}; diff --git a/lib/request-response.js b/lib/request-response.js new file mode 100644 index 0000000..49b34b8 --- /dev/null +++ b/lib/request-response.js @@ -0,0 +1,116 @@ +/** + * Механизим, добавляющий механизм request-response сообщений. + * + * Используется следующая конвенция: если событие вызывается + * с префиксом `/`, то тип событие - команда и ожидается ответ + */ +if (typeof module === 'object' && typeof define !== 'function') { + var define = function(factory) { + module.exports = factory(require, exports, module); + }; +} + +/*jshint unused:false*/ +define(function(require, exports, module) { + var channelTest = /^@/, + commandTest = /^\//, + deferred = require('pledges').deferred; + + return function(postal) { + + function request(channel, command) { + var + defer = deferred(), + promise = defer, + data = { + __args: null + }; + + if (typeof channel !== 'string') { + return promise; + } + + if (channelTest.test(channel)) { + channel = channel.substr(1); + data.__args = Array.prototype.slice.call(arguments, 2); + } else { + data.__args = Array.prototype.slice.call(arguments, 1); + command = channel; + channel = null; + } + + if (!channel && this instanceof postal.ChannelDefinition) { + channel = this.channel; + } + + function resolve(data) { + defer.resolve(data); + } + + function reject(reason) { + defer.reject(reason); + } + + function reply(err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + } + + reply.resolve = resolve; + reply.reject = reject; + + postal.publish({ + channel: channel, + topic: command, + data: data, + reply: reply, + headers: { + replyable: true + } + }); + + return promise; + } + + postal.request = request; + + postal.ChannelDefinition.prototype.request = request; + + var SubscriptionDefinitionProto = postal.SubscriptionDefinition.prototype, + originSubscribe = SubscriptionDefinitionProto.subscribe; + + SubscriptionDefinitionProto.subscribe = function(originCallback) { + + function callbackProxy(data, envelope) { + if (envelope.headers && envelope.headers.replyable) { + try { + // если метод вызван через метод ON фасада backbode + if (originCallback.__original && data.__args) { + data.__args.unshift(envelope.reply); + } + + return originCallback.call(this, data, envelope); + } catch (ex) { + //console.log(ex); + envelope.reply(ex); + } + } + return originCallback.apply(this, arguments); + + } + + //в случае если подписались через метод ON + // сохраняем __original для отписки + if (originCallback.__original) { + callbackProxy.__original = originCallback.__original; + } + + return originSubscribe.call(this, callbackProxy); + }; + + return postal; + }; +}); \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js index b83a464..85aad98 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,8 +1,10 @@ var ArrayProto = Array.prototype, ObjProto = Object.prototype, + /*jshint -W079 */ hasOwnProperty = ObjProto.hasOwnProperty, nativeForEach = ArrayProto.forEach, - slice = ArrayProto.slice; + slice = ArrayProto.slice, + breaker = false; module.exports = { has: function(obj, key) { diff --git a/package.json b/package.json index dba9eab..1fd3bc2 100644 --- a/package.json +++ b/package.json @@ -19,15 +19,19 @@ "author": "Sergey Chikuyonok ", "license": "MIT", "dependencies": { + "pledges": "^3.0.0", "postal": "^0.10.3" }, "devDependencies": { "almond": "~0.2.9", "gulp": "^3.8.7", + "gulp-jshint": "^1.9.0", "gulp-rename": "^1.2.0", "gulp-requirejs": "^0.1.3", "gulp-uglify": "^0.3.1", "merge": "^1.2.0", - "mocha": "^1.21.4" + "mocha": "^1.21.4", + "mockery": "^1.4.0", + "q": "^1.0.1" } } diff --git a/test/request-response.js b/test/request-response.js new file mode 100644 index 0000000..b444553 --- /dev/null +++ b/test/request-response.js @@ -0,0 +1,126 @@ +var assert = require('assert'); +var mockery = require('mockery'); +mockery.enable(); +mockery.registerMock('jquery', {}); + +var bus = require('../index'); +var defer = require('q'); + +describe('request-response events.', function() { + + beforeEach(function() { + bus.off('reject'); + bus.off('resolve'); + + bus.unsubscribeFor({ + topic: 'reject' + }); + + bus.unsubscribeFor({ + topic: 'resolve' + }) + }); + + it('using request/on', function(done) { + + bus + .on('resolve', function(reply, arg1, arg2) { + reply(null, 1 + arg1 + arg2); + }) + .on('reject', function(reply, arg1, arg2) { + reply(1 + arg1 + arg2); + }); + + var resolvePromise = bus.request('resolve', 2, 3).then(function(value) { + assert.equal(value, 6); + return value; + }, + function() { + return defer.reject('Promise should be resolved'); + }); + + var rejectPromise = bus.request('reject', 2, 3).then(function(value) { + return defer.reject('Promise should be rejected'); + }, + function(value) { + assert.equal(value, 6); + return value; + }); + + defer.all([resolvePromise, rejectPromise]).then(function(val) { + done(); + }, function(reason) { + done(reason); + }); + }); + + it('using request/on in channel', function(done) { + bus + .on('@channel', 'resolve', function(reply, arg1, arg2) { + reply(null, 1 + arg1 + arg2); + }) + .on('@channel', 'reject', function(reply, arg1, arg2) { + reply(1 + arg1 + arg2); + }); + + var resolvePromise = bus.request('@channel', 'resolve', 2, 3).then(function(value) { + assert.equal(value, 6); + return value; + }, + function() { + return defer.reject('Promise should be resolved'); + }); + + var rejectPromise = bus.request('@channel', 'reject', 2, 3).then(function(value) { + return defer.reject('Promise should be rejected'); + }, + function(value) { + assert.equal(value, 6); + return value; + }); + + defer.all([resolvePromise, rejectPromise]).then(function(val) { + done(); + }, function(reason) { + done(reason); + }); + }) + + it('using request/sub', function(done) { + + bus.subscribe({ + topic: 'resolve', + callback: function(data, env) { + env.reply.resolve(1 + data.__args[0] + data.__args[1]); + } + }); + + bus.subscribe({ + topic: 'reject', + callback: function(data, env) { + env.reply(1 + data.__args[0] + data.__args[1]); + } + }); + + var resolvePromise = bus.request('resolve', 2, 3).then(function(value) { + assert.equal(value, 6); + return value; + }, function() { + return defer.reject('Promise should be resolved'); + }); + + var rejectPromise = bus.request('reject', 2, 3).then(function(value) { + return defer.reject('Promise should be rejected'); + }, function(value) { + assert.equal(value, 6); + return value; + }); + + defer.all([resolvePromise, rejectPromise]).then(function(val) { + done(); + }, function(reason) { + done(reason); + }); + }); + +}); \ No newline at end of file