├── .gitignore ├── examples └── cross_module_communication │ ├── README.md │ ├── index.js │ ├── subscriber.js │ └── publisher.js ├── tests ├── mocha.opts ├── 5.multiple_subscriptions.js ├── 4.mixed_pattern_topic.js ├── 1.simple_specific_topic.js ├── 7.publications_meta.js ├── 2.simple_pattern_topic.js ├── 0.arguments_validation.js ├── 6.subscriptions_control.js └── 3.greedy_pattern_topic.js ├── benchmark ├── test.js ├── tests │ ├── oneSubscriptionManyPublications.js │ ├── onePatternSubscriptionManyPublication.js │ ├── manySubscriptionsOnePublication.js │ ├── manySubscriptionsManyPublications.js │ ├── manyPatternSubscriptionsManyPublication.js │ └── manyPatternSubscriptionsOnePublication.js ├── RESULTS └── index.js ├── src ├── portage.js ├── Hub.js ├── Subscription.js └── Channel.js ├── bower.json ├── webpack.config.js ├── package.json ├── README.md └── dist ├── portage.js.map └── portage.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | bower_components 4 | -------------------------------------------------------------------------------- /examples/cross_module_communication/README.md: -------------------------------------------------------------------------------- 1 | Run with `node index.js` 2 | -------------------------------------------------------------------------------- /tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --reporter spec 3 | --compilers js:babel/register 4 | -------------------------------------------------------------------------------- /examples/cross_module_communication/index.js: -------------------------------------------------------------------------------- 1 | require("./subscriber"); 2 | require("./publisher"); 3 | -------------------------------------------------------------------------------- /benchmark/test.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | 3 | function Test(title, runner){ 4 | this.title = title; 5 | this.run = runner; 6 | } 7 | 8 | module.exports = Test; 9 | 10 | })(); 11 | -------------------------------------------------------------------------------- /examples/cross_module_communication/subscriber.js: -------------------------------------------------------------------------------- 1 | var portage = require("../../"), 2 | c = portage.channel("my-channel"); 3 | 4 | // subscribe to time.* messages 5 | 6 | c.subscribe("time.*", function(time){ 7 | console.log(time); 8 | }); 9 | -------------------------------------------------------------------------------- /src/portage.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import Channel from './Channel'; 4 | import Hub from './Hub'; 5 | import Subscription from './Subscription'; 6 | 7 | var defaultHub = new Hub(); 8 | 9 | defaultHub.Channel = Channel; 10 | defaultHub.Hub = Hub; 11 | defaultHub.Subscription = Subscription; 12 | 13 | export default defaultHub; 14 | -------------------------------------------------------------------------------- /examples/cross_module_communication/publisher.js: -------------------------------------------------------------------------------- 1 | var portage = require("../../"), 2 | c = portage.channel("my-channel"); 3 | 4 | // publish the time in different formats every 2 seconds 5 | setInterval(function(){ 6 | c.publish("time.epoch", Date.now()); 7 | c.publish("time.local", (new Date()).toString()); 8 | c.publish("time.utc", (new Date()).toISOString()); 9 | }, 2000); 10 | -------------------------------------------------------------------------------- /src/Hub.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import {isString as _isString} from 'lodash'; 4 | import Channel from './Channel'; 5 | 6 | class Hub{ 7 | constructor(){ 8 | this._channels = {}; 9 | } 10 | 11 | channel(name){ 12 | if (!_isString(name)) throw Error("channel name must be a string"); 13 | return ( this._channels[name] = this._channels[name] || new Channel() ); 14 | } 15 | } 16 | 17 | export default Hub; 18 | -------------------------------------------------------------------------------- /benchmark/tests/oneSubscriptionManyPublications.js: -------------------------------------------------------------------------------- 1 | var Test = require("../test"); 2 | 3 | module.exports = new Test( 4 | 5 | "One specific subscription, many publications", 6 | 7 | function (N, hub, done){ 8 | var start = Date.now(), 9 | c = hub.channel("C" + Math.random()), 10 | n = 0; 11 | 12 | c.subscribe("topic.hello.world", function(){ 13 | n++; 14 | if (n === N - 1) done(null, Date.now() - start); 15 | }); 16 | 17 | for (var i = 0 ; i < N ; i++){ 18 | c.publish("topic.hello.world"); 19 | c.publish("no-match"); 20 | } 21 | } 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /benchmark/tests/onePatternSubscriptionManyPublication.js: -------------------------------------------------------------------------------- 1 | var Test = require("../test"); 2 | 3 | module.exports = new Test( 4 | 5 | "One pattern subscriptions, many publications", 6 | 7 | function (N, hub, done){ 8 | var start = Date.now(), 9 | c = hub.channel("C" + Math.random()), 10 | n = 0, 11 | i; 12 | 13 | c.subscribe("topic.*.world.*", function(){ 14 | n++; 15 | if (n === N - 1) done(null, Date.now() - start); 16 | }); 17 | 18 | for (i = 0 ; i < N ; i++){ 19 | c.publish("topic.hello.world." + i); 20 | c.publish("no-match" + i); 21 | } 22 | } 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /benchmark/tests/manySubscriptionsOnePublication.js: -------------------------------------------------------------------------------- 1 | var Test = require("../test"); 2 | 3 | module.exports = new Test( 4 | 5 | "Many specific subscriptions, 1 publication", 6 | 7 | function (N, hub, done){ 8 | var start = Date.now(), 9 | c = hub.channel("C" + Math.random()), 10 | n = 0; 11 | 12 | for (var i = 0 ; i < N ; i++){ 13 | c.subscribe("topic.hello.world", function(){ 14 | n++; 15 | if (n === N - 1) done(null, Date.now() - start); 16 | }); 17 | c.subscribe("topic.hello.world.no-match", function(){}); 18 | } 19 | 20 | c.publish("topic.hello.world"); 21 | } 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portage", 3 | "version": "0.0.6", 4 | "homepage": "https://github.com/EyalAr/Portage", 5 | "authors": [ 6 | "Eyal Arubas " 7 | ], 8 | "description": "Fast pub/sub for JS", 9 | "main": "dist/portage.js", 10 | "moduleType": [ 11 | "amd", 12 | "es6", 13 | "globals", 14 | "node" 15 | ], 16 | "keywords": [ 17 | "pub", 18 | "sub", 19 | "publish", 20 | "subscribe", 21 | "hub", 22 | "channel" 23 | ], 24 | "license": "MIT", 25 | "ignore": [ 26 | "**/.*", 27 | "node_modules", 28 | "bower_components", 29 | "test", 30 | "tests" 31 | ], 32 | "dependencies": { 33 | "fuzzytree": "~0.0.7", 34 | "lodash": "~3.10.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /benchmark/tests/manySubscriptionsManyPublications.js: -------------------------------------------------------------------------------- 1 | var Test = require("../test"); 2 | 3 | module.exports = new Test( 4 | 5 | "Many specific subscriptions, many publication", 6 | 7 | function (N, hub, done){ 8 | var start = Date.now(), 9 | c = hub.channel("C" + Math.random()), 10 | n = 0, 11 | i; 12 | 13 | for (i = 0 ; i < N ; i++){ 14 | c.subscribe("topic.hello.world." + i, function(){ 15 | n++; 16 | if (n === N - 1) done(null, Date.now() - start); 17 | }); 18 | c.subscribe("topic.hello.world.no-match", function(){}); 19 | } 20 | 21 | for (i = 0 ; i < N ; i++){ 22 | c.publish("topic.hello.world." + i); 23 | c.publish("no-match"); 24 | } 25 | } 26 | 27 | ); 28 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: "./src/portage.js", 5 | module: { 6 | loaders: [{ 7 | test: /src\/.+\.js$/, 8 | loader: 'babel' 9 | }] 10 | }, 11 | output: { 12 | filename: "dist/portage.js", 13 | library: "portage", 14 | libraryTarget: "umd" 15 | }, 16 | externals: [{ 17 | lodash: { 18 | root: '_', 19 | commonjs2: 'lodash', 20 | commonjs: 'lodash', 21 | amd: 'lodash' 22 | }, 23 | fuzzytree: { 24 | root: 'FuzzyTree', 25 | commonjs2: 'fuzzytree', 26 | commonjs: 'fuzzytree', 27 | amd: 'fuzzytree' 28 | } 29 | }], 30 | plugins: [ 31 | new webpack.SourceMapDevToolPlugin( 32 | '[file].map', null, "../[resource-path]", "../[resource-path]") 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /benchmark/RESULTS: -------------------------------------------------------------------------------- 1 | N=1000 2 | 3 | Many pattern subscriptions, many publications... 4 | Postal: 7071 ms 5 | Portage: 38 ms 6 | Portage is 7033 ms faster (99.46%) 7 | 8 | Many pattern subscriptions, 1 publication... 9 | Postal: 515 ms 10 | Portage: 3 ms 11 | Portage is 512 ms faster (99.42%) 12 | 13 | Many specific subscriptions, many publication... 14 | Postal: 7207 ms 15 | Portage: 15 ms 16 | Portage is 7192 ms faster (99.79%) 17 | 18 | Many specific subscriptions, 1 publication... 19 | Postal: 1994 ms 20 | Portage: 5 ms 21 | Portage is 1989 ms faster (99.75%) 22 | 23 | One pattern subscriptions, many publications... 24 | Postal: 12 ms 25 | Portage: 8 ms 26 | Portage is 4 ms faster (33.33%) 27 | 28 | One specific subscription, many publications... 29 | Postal: 11 ms 30 | Portage: 6 ms 31 | Portage is 5 ms faster (45.45%) 32 | 33 | Portage is faster at 6/6 tests 34 | Postal is faster at 0/6 tests 35 | -------------------------------------------------------------------------------- /benchmark/tests/manyPatternSubscriptionsManyPublication.js: -------------------------------------------------------------------------------- 1 | var Test = require("../test"); 2 | 3 | module.exports = new Test( 4 | 5 | "Many pattern subscriptions, many publications", 6 | 7 | function (N, hub, done){ 8 | var start = Date.now(), 9 | c = hub.channel("C" + Math.random()), 10 | n = 0, 11 | i; 12 | 13 | for (i = 0 ; i < N ; i++){ 14 | var s1 = Math.random() < 0.5 ? "topic.#" : "*.hello", 15 | s2 = Math.random() < 0.5 ? "world.*" : "#.foo", 16 | pattern = s1 + "." + s2 + "." + i; 17 | c.subscribe(pattern, function(){ 18 | n++; 19 | if (n === N - 1) done(null, Date.now() - start); 20 | }); 21 | c.subscribe("topic.*.world.*.no-match", function(){}); 22 | } 23 | 24 | for (i = 0 ; i < N ; i++){ 25 | c.publish("topic.hello.world.foo." + i); 26 | c.publish("no-match"); 27 | } 28 | } 29 | 30 | ); 31 | -------------------------------------------------------------------------------- /src/Subscription.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { 4 | isNumber as _isNumber, 5 | isNaN as _isNaN, 6 | gte as _gte, 7 | indexOf as _indexOf 8 | } from 'lodash'; 9 | 10 | class Subscription{ 11 | constructor(cb, container){ 12 | this._cb = cb; 13 | this._container = container; 14 | this._called = 0; 15 | this._limit = null; 16 | } 17 | 18 | invoke(data, meta){ 19 | var r = this._cb(data, meta); 20 | this._called++; 21 | this._purge(); 22 | return r; 23 | } 24 | 25 | _purge(){ 26 | if (this._limit !== null && this._called >= this._limit) 27 | this.unsubscribe(); 28 | } 29 | 30 | limit(n){ 31 | if (!(_isNumber(n) && !_isNaN(n) && _gte(n, 0))) 32 | throw Error("limit must be a number greater or equal to 0"); 33 | this._limit = this._called + n; 34 | this._purge(); 35 | return this; 36 | } 37 | 38 | once(){ 39 | return this.limit(1); 40 | } 41 | 42 | unsubscribe(){ 43 | var i = _indexOf(this._container, this); 44 | if (i !== -1) this._container.splice(i, 1); 45 | } 46 | } 47 | 48 | export default Subscription; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "portage", 3 | "version": "0.0.6", 4 | "description": "Fast pub/sub for JS", 5 | "main": "dist/portage.js", 6 | "directories": { 7 | "test": "tests" 8 | }, 9 | "dependencies": { 10 | "fuzzytree": "0.0.7", 11 | "lodash": "^3.9.3" 12 | }, 13 | "devDependencies": { 14 | "async": "^1.3.0", 15 | "babel": "^5.6.14", 16 | "babel-core": "^5.6.15", 17 | "babel-loader": "^5.3.1", 18 | "jshint": "^2.8.0", 19 | "mocha": "^2.2.5", 20 | "node-libs-browser": "^0.5.2", 21 | "postal": "^1.0.6", 22 | "should": "^7.0.1", 23 | "webpack": "^1.10.1" 24 | }, 25 | "scripts": { 26 | "test": "./node_modules/.bin/mocha --opts tests/mocha.opts tests", 27 | "build": "./node_modules/.bin/webpack", 28 | "benchmark": "./node_modules/.bin/babel-node benchmark/index.js", 29 | "lint": "./node_modules/.bin/jshint src/**.js" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "https://github.com/EyalAr/Portage.git" 34 | }, 35 | "keywords": [ 36 | "pub", 37 | "sub", 38 | "publish", 39 | "subscribe", 40 | "hub", 41 | "channel" 42 | ], 43 | "author": "Eyal Arubas ", 44 | "license": "ISC", 45 | "bugs": { 46 | "url": "https://github.com/EyalAr/Portage/issues" 47 | }, 48 | "homepage": "https://github.com/EyalAr/Portage" 49 | } 50 | -------------------------------------------------------------------------------- /src/Channel.js: -------------------------------------------------------------------------------- 1 | /* jshint esnext:true */ 2 | 3 | import { 4 | isFunction as _isFunction, 5 | forEach as _forEach, 6 | reduce as _reduce 7 | } from 'lodash'; 8 | import FuzzyTree from 'fuzzytree'; 9 | import Subscription from './Subscription'; 10 | 11 | class Channel{ 12 | constructor(){ 13 | this._tree = new FuzzyTree(); 14 | this._strategy = defaultStrategy; 15 | } 16 | 17 | subscribe(topic, cb){ 18 | if (!_isFunction(cb)) throw Error("callback must be a function"); 19 | 20 | var node = this._tree.find(topic); 21 | if (!node) { 22 | node = this._tree.insert(topic); 23 | node.setData([]); 24 | } 25 | var s = new Subscription(cb, node.getData()); 26 | node.getData().push(s); 27 | return s; 28 | } 29 | 30 | publish(topic, data){ 31 | var nodes = this._tree.match(topic), 32 | subs = _reduce(nodes, (subs, node) => { 33 | _forEach(node.getData(), s => subs.push(s)); 34 | return subs; 35 | }, []); 36 | return this._strategy(subs, data, topic); 37 | } 38 | } 39 | 40 | function defaultStrategy(subs, data, topic){ 41 | _forEach(subs, s => s.invoke(data, { 42 | topic: topic, 43 | called: s._called, 44 | limit: s._limit, 45 | last: s._limit !== null && s._called === s._limit - 1 46 | })); 47 | } 48 | 49 | export default Channel; 50 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | var async = require("async"), 2 | portage = require("../src/portage"), 3 | postal = require("postal"); 4 | 5 | var N = 1e3; 6 | 7 | console.log("Portage vs Postal benchmark"); 8 | console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~\n"); 9 | console.log("N=" + N, "\n"); 10 | 11 | var testPairs = require("fs").readdirSync(__dirname + "/tests").map(function(path){ 12 | var test = require(__dirname + "/tests/" + path); 13 | return pairRunnerSetup( 14 | test.title, 15 | test.run.bind(null, N, postal), 16 | test.run.bind(null, N, portage) 17 | ); 18 | }); 19 | 20 | async.series(testPairs, function(err, results){ 21 | if (err) 22 | return console.log("Stopped tests due to an error:", err.toString()); 23 | 24 | var portageWins = results.reduce(function(p, e, i, o){ 25 | return p + (e === "portage" ? 1 : 0); 26 | }, 0), 27 | postalWins = results.length - portageWins; 28 | 29 | console.log("Portage is faster at", portageWins + "/" + results.length, "tests"); 30 | console.log("Postal is faster at", postalWins + "/" + results.length, "tests"); 31 | }); 32 | 33 | function pairRunnerSetup(title, postalRunner, portageRunner){ 34 | return function(done){ 35 | console.log(title + "..."); 36 | async.series([ 37 | postalRunner, 38 | portageRunner 39 | ], function(err, results){ 40 | if (err) return done(err); 41 | var tPostal = results[0], 42 | tPortage = results[1], 43 | tDiff = Math.abs(tPostal - tPortage), 44 | tRatio = tDiff / (tPostal > tPortage ? tPostal : tPortage); 45 | tRatio = (tRatio * 100).toFixed(2) + "%"; 46 | console.log("Postal:\t\t", tPostal, "ms"); 47 | console.log("Portage:\t", tPortage, "ms"); 48 | console.log( 49 | tPostal < tPortage ? "Postal" : "Portage", 50 | "is", 51 | tDiff, "ms faster (" + tRatio + ")" 52 | ); 53 | console.log(); 54 | done(null, tPostal < tPortage ? "postal" : "portage"); 55 | }); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /tests/5.multiple_subscriptions.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("multiple subscriptions", function(){ 5 | 6 | describe("single publication", function(){ 7 | 8 | var c = new portage.Channel(), 9 | topic = "test.hello.world.nice.to.meet.you", 10 | pattern1 = "*.hello.#", 11 | pattern2 = "test.*.world.nice.to.#", 12 | data = "foo", 13 | spy1, 14 | spy2, 15 | s1Called = 0, 16 | s2Called = 0; 17 | 18 | c.subscribe(pattern1, function(spy){ 19 | spy1 = spy; 20 | s1Called++; 21 | }); 22 | 23 | c.subscribe(pattern2, function(spy){ 24 | spy2 = spy; 25 | s2Called++; 26 | }); 27 | 28 | it("should invoke each callback once with correct data", function(){ 29 | c.publish(topic, data); 30 | 31 | should.equal(data, spy1); 32 | should.equal(data, spy2); 33 | should.equal(s1Called, 1); 34 | should.equal(s2Called, 1); 35 | }); 36 | 37 | }); 38 | 39 | describe("multiple publications", function(){ 40 | 41 | var c = new portage.Channel(), 42 | topic1 = "test.hello.world.nice.to.meet.you", 43 | topic2 = "test.hello.world.nice.to.see.you", 44 | pattern1 = "*.hello.#", 45 | pattern2 = "test.*.world.nice.to.#", 46 | data = "foo", 47 | spy1, 48 | spy2, 49 | s1Called = 0, 50 | s2Called = 0; 51 | 52 | c.subscribe(pattern1, function(spy){ 53 | spy1 = spy; 54 | s1Called++; 55 | }); 56 | 57 | c.subscribe(pattern2, function(spy){ 58 | spy2 = spy; 59 | s2Called++; 60 | }); 61 | 62 | it("should invoke each callback twice with correct data", function(){ 63 | c.publish(topic1, data); 64 | c.publish(topic2, data); 65 | 66 | should.equal(data, spy1); 67 | should.equal(data, spy2); 68 | should.equal(s1Called, 2); 69 | should.equal(s2Called, 2); 70 | }); 71 | 72 | }); 73 | 74 | }); 75 | -------------------------------------------------------------------------------- /tests/4.mixed_pattern_topic.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("mixed pattern topics", function(){ 5 | 6 | describe("single subscription", function(){ 7 | 8 | describe("single publication", function(){ 9 | 10 | describe("pattern with mixed wildcards", function(){ 11 | 12 | var c = new portage.Channel(), 13 | topic = "test.hello.world.nice.to.meet.you", 14 | patternOk = "test.*.world.*.to.#", 15 | patternNotOk = "test.*.world.to.#", 16 | data = "foo", 17 | spy, 18 | called = 0; 19 | 20 | c.subscribe(patternOk, cb); 21 | c.subscribe(patternNotOk, cb); 22 | 23 | function cb(_spy){ 24 | spy = _spy; 25 | called++; 26 | } 27 | 28 | it("should invoke callback once with correct data", function(){ 29 | c.publish(topic, data); 30 | should.equal(data, spy); 31 | should.equal(called, 1); 32 | }); 33 | 34 | }); 35 | 36 | }); 37 | 38 | describe("multiple publications", function(){ 39 | 40 | describe("pattern with mixed wildcards", function(){ 41 | 42 | var c = new portage.Channel(), 43 | topic1 = "test.hello.world.nice.to.meet.you", 44 | topic2 = "test.hello.world.nice.to.see.you", 45 | pattern = "test.*.world.*.to.#", 46 | data = "foo", 47 | spy, 48 | called = 0; 49 | 50 | c.subscribe(pattern, function(_spy){ 51 | spy = _spy; 52 | called++; 53 | }); 54 | 55 | c.publish(topic1, data); 56 | c.publish(topic2, data); 57 | 58 | it("should invoke callback twice with correct data", function(){ 59 | should.equal(data, spy); 60 | should.equal(called, 2); 61 | }); 62 | 63 | }); 64 | 65 | }); 66 | 67 | }); 68 | 69 | }); 70 | -------------------------------------------------------------------------------- /benchmark/tests/manyPatternSubscriptionsOnePublication.js: -------------------------------------------------------------------------------- 1 | var Test = require("../test"); 2 | 3 | module.exports = new Test( 4 | 5 | "Many pattern subscriptions, 1 publication", 6 | 7 | function (N, hub, done){ 8 | var start = Date.now(), 9 | c = hub.channel("C" + Math.random()), 10 | n = 0; 11 | 12 | for (var i = 0 ; i < N / 10 ; i++){ 13 | c.subscribe("#", function(){ 14 | n++; 15 | if (n === N - 1) done(null, Date.now() - start); 16 | }); 17 | c.subscribe("topic.#", function(){ 18 | n++; 19 | if (n === N - 1) done(null, Date.now() - start); 20 | }); 21 | c.subscribe("topic.hello.#", function(){ 22 | n++; 23 | if (n === N - 1) done(null, Date.now() - start); 24 | }); 25 | c.subscribe("topic.hello.world.#", function(){ 26 | n++; 27 | if (n === N - 1) done(null, Date.now() - start); 28 | }); 29 | c.subscribe("topic.hello.world.foo.#", function(){ 30 | n++; 31 | if (n === N - 1) done(null, Date.now() - start); 32 | }); 33 | c.subscribe("*.hello.world.foo.bar", function(){ 34 | n++; 35 | if (n === N - 1) done(null, Date.now() - start); 36 | }); 37 | c.subscribe("topic.*.world.foo.bar", function(){ 38 | n++; 39 | if (n === N - 1) done(null, Date.now() - start); 40 | }); 41 | c.subscribe("topic.hello.*.foo.bar", function(){ 42 | n++; 43 | if (n === N - 1) done(null, Date.now() - start); 44 | }); 45 | c.subscribe("topic.hello.world.*.bar", function(){ 46 | n++; 47 | if (n === N - 1) done(null, Date.now() - start); 48 | }); 49 | c.subscribe("topic.hello.world.foo.*", function(){ 50 | n++; 51 | if (n === N - 1) done(null, Date.now() - start); 52 | }); 53 | c.subscribe("topic.hello.world.foo.no-match", function(){}); 54 | } 55 | 56 | c.publish("topic.hello.world.foo.bar"); 57 | } 58 | 59 | ); 60 | -------------------------------------------------------------------------------- /tests/1.simple_specific_topic.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("simple specific topics", function(){ 5 | 6 | describe("single subscription", function(){ 7 | 8 | describe("single publication", function(){ 9 | 10 | describe("specific topic with one section", function(){ 11 | 12 | var c = new portage.Channel(), 13 | topic = "test", 14 | data = "foo", 15 | spy, 16 | called = 0; 17 | 18 | c.subscribe(topic, function(_spy){ 19 | spy = _spy; 20 | called++; 21 | }); 22 | 23 | it("should invoke callback once with correct data", function(){ 24 | c.publish(topic, data); 25 | 26 | should.equal(data, spy); 27 | should.equal(called, 1); 28 | }); 29 | 30 | }); 31 | 32 | describe("specific topic with multiple sections", function(){ 33 | 34 | var c = new portage.Channel(), 35 | topic = "test.hello", 36 | data = "foo", 37 | spy, 38 | called = 0; 39 | 40 | c.subscribe(topic, function(_spy){ 41 | spy = _spy; 42 | called++; 43 | }); 44 | 45 | it("should invoke callback once with correct data", function(){ 46 | c.publish(topic, data); 47 | should.equal(data, spy); 48 | should.equal(called, 1); 49 | }); 50 | 51 | }); 52 | 53 | }); 54 | 55 | describe("multiple publications", function(){ 56 | 57 | describe("specific topic", function(){ 58 | 59 | var c = new portage.Channel(), 60 | topic = "test.hello", 61 | data = "foo", 62 | spy, 63 | called = 0; 64 | 65 | c.subscribe(topic, function(_spy){ 66 | spy = _spy; 67 | called++; 68 | }); 69 | 70 | it("should invoke callback twice with correct data", function(){ 71 | c.publish(topic, data); 72 | c.publish(topic, data); 73 | 74 | should.equal(data, spy); 75 | should.equal(called, 2); 76 | }); 77 | 78 | }); 79 | 80 | }); 81 | 82 | }); 83 | 84 | }); 85 | -------------------------------------------------------------------------------- /tests/7.publications_meta.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("publications meta data", function(){ 5 | 6 | describe("topic", function(){ 7 | 8 | var c = new portage.Channel(), 9 | topicSpy; 10 | 11 | c.subscribe("test.*.world", function(data, meta){ 12 | topicSpy = meta.topic; 13 | }); 14 | 15 | it("should invoke subscription with the correct topic", function(){ 16 | c.publish("test.hello.world"); 17 | should.equal(topicSpy, "test.hello.world"); 18 | }); 19 | 20 | }); 21 | 22 | describe("called", function(){ 23 | 24 | var c = new portage.Channel(), 25 | calledSpy; 26 | 27 | c.subscribe("test.*.world", function(data, meta){ 28 | calledSpy = meta.called; 29 | }); 30 | 31 | it("should have been called 0 times before", function(){ 32 | c.publish("test.hello.world"); 33 | should.equal(calledSpy, 0); 34 | }); 35 | 36 | it("should have been called 1 times before", function(){ 37 | c.publish("test.hello.world"); 38 | should.equal(calledSpy, 1); 39 | }); 40 | 41 | }); 42 | 43 | describe("limit", function(){ 44 | 45 | var c = new portage.Channel(), 46 | limitSpy; 47 | 48 | var s = c.subscribe("test.*.world", function(data, meta){ 49 | limitSpy = meta.limit; 50 | }); 51 | s.limit(1); 52 | 53 | it("should have the correct limit", function(){ 54 | c.publish("test.hello.world"); 55 | should.equal(limitSpy, 1); 56 | }); 57 | 58 | }); 59 | 60 | describe("last", function(){ 61 | 62 | var c = new portage.Channel(), 63 | lastSpy; 64 | 65 | var s = c.subscribe("test.*.world", function(data, meta){ 66 | lastSpy = meta.last; 67 | }); 68 | s.limit(2); 69 | 70 | it("should not be last", function(){ 71 | c.publish("test.hello.world"); 72 | should.equal(lastSpy, false); 73 | }); 74 | 75 | it("should be last", function(){ 76 | c.publish("test.hello.world"); 77 | should.equal(lastSpy, true); 78 | }); 79 | 80 | }); 81 | 82 | describe("data and meta arguments", function(){ 83 | 84 | var c = new portage.Channel(), 85 | metaSpy, dataSpy; 86 | 87 | c.subscribe("test.*.world", function(data, meta){ 88 | dataSpy = data; 89 | metaSpy = meta; 90 | }); 91 | 92 | it("data should be correct", function(){ 93 | c.publish("test.hello.world", ["foo", "bar"]); 94 | dataSpy.should.have.length(2); 95 | dataSpy.should.containEql("foo"); 96 | dataSpy.should.containEql("bar"); 97 | metaSpy.should.be.type("object"); 98 | }); 99 | 100 | it("data should be undefined", function(){ 101 | c.publish("test.hello.world"); 102 | should(dataSpy).be.type("undefined"); 103 | metaSpy.should.be.type("object"); 104 | }); 105 | 106 | }); 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /tests/2.simple_pattern_topic.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("simple pattern topics", function(){ 5 | 6 | describe("single subscription", function(){ 7 | 8 | describe("single publication", function(){ 9 | 10 | describe("pattern with non greedy wildcard (partial)", function(){ 11 | 12 | var c = new portage.Channel(), 13 | topic = "test.hello", 14 | pattern = "test.*", 15 | data = "foo", 16 | spy, 17 | called = 0; 18 | 19 | c.subscribe(pattern, function(_spy){ 20 | spy = _spy; 21 | called++; 22 | }); 23 | 24 | it("should invoke callback once with correct data", function(){ 25 | c.publish(topic, data); 26 | should.equal(data, spy); 27 | should.equal(called, 1); 28 | }); 29 | 30 | }); 31 | 32 | describe("pattern with non greedy wildcard (full)", function(){ 33 | 34 | var c = new portage.Channel(), 35 | topicOk = "test", 36 | topicNotOk = "test.hello", 37 | pattern = "*", 38 | dataOk = "foo", 39 | dataNotOk = "bar", 40 | spy, 41 | called = 0; 42 | 43 | c.subscribe(pattern, function(_spy){ 44 | spy = _spy; 45 | called++; 46 | }); 47 | 48 | it("should invoke callback once with correct data", function(){ 49 | c.publish(topicNotOk, dataNotOk); 50 | c.publish(topicOk, dataOk); 51 | should.equal(dataOk, spy); 52 | should.equal(called, 1); 53 | }); 54 | 55 | }); 56 | }); 57 | 58 | describe("multiple publications", function(){ 59 | 60 | describe("pattern with non greedy wildcard (partial)", function(){ 61 | 62 | var c = new portage.Channel(), 63 | topic1 = "test.hello", 64 | topic2 = "test.world", 65 | pattern = "test.*", 66 | data = "foo", 67 | spy, 68 | called = 0; 69 | 70 | c.subscribe(pattern, function(_spy){ 71 | spy = _spy; 72 | called++; 73 | }); 74 | 75 | it("should invoke callback twice with correct data", function(){ 76 | c.publish(topic1, data); 77 | c.publish(topic2, data); 78 | 79 | should.equal(data, spy); 80 | should.equal(called, 2); 81 | }); 82 | 83 | }); 84 | 85 | describe("pattern with non greedy wildcard (full)", function(){ 86 | 87 | var c = new portage.Channel(), 88 | topic1 = "hello", 89 | topic2 = "world", 90 | pattern = "*", 91 | data = "foo", 92 | spy, 93 | called = 0; 94 | 95 | c.subscribe(pattern, function(_spy){ 96 | spy = _spy; 97 | called++; 98 | }); 99 | 100 | it("should invoke callback twice with correct data", function(){ 101 | c.publish(topic1, data); 102 | c.publish(topic2, data); 103 | 104 | should.equal(data, spy); 105 | should.equal(called, 2); 106 | }); 107 | 108 | }); 109 | 110 | }); 111 | 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /tests/0.arguments_validation.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("arguments validation", function(){ 5 | 6 | describe("Channel", function(){ 7 | 8 | describe("subscribe", function(){ 9 | 10 | var c = new portage.Channel(); 11 | 12 | it("should not throw an exception if pattern is string or array and callback is function", function(){ 13 | c.subscribe.bind(c, "topic", function(){}).should.not.throw(); 14 | c.subscribe.bind(c, ["topic", "hello"], function(){}).should.not.throw(); 15 | }); 16 | 17 | it("should throw an exception if callback is not function", function(){ 18 | c.subscribe.bind(c, ["topic", "hello"], 123).should.throw(); 19 | }); 20 | 21 | it("should throw an exception if topic is not a string or an array", function(){ 22 | c.subscribe.bind(c, 123, function(){}).should.throw(); 23 | c.subscribe.bind(c, undefined, function(){}).should.throw(); 24 | c.subscribe.bind(c, null, function(){}).should.throw(); 25 | c.subscribe.bind(c, true, function(){}).should.throw(); 26 | c.subscribe.bind(c, {}, function(){}).should.throw(); 27 | c.subscribe.bind(c, function(){}, function(){}).should.throw(); 28 | c.subscribe.bind(c, /topic/, function(){}).should.throw(); 29 | }); 30 | 31 | }); 32 | 33 | describe("publish", function(){ 34 | 35 | var c = new portage.Channel(); 36 | 37 | it("should not throw an exception if topic is string or array", function(){ 38 | c.publish.bind(c, "topic.hello").should.not.throw(); 39 | c.publish.bind(c, ["topic", "hello"]).should.not.throw(); 40 | }); 41 | 42 | it("should throw an exception if topic is not a string or an array", function(){ 43 | c.publish.bind(c, 123).should.throw(); 44 | c.publish.bind(c, undefined).should.throw(); 45 | c.publish.bind(c, null).should.throw(); 46 | c.publish.bind(c, true).should.throw(); 47 | c.publish.bind(c, {}).should.throw(); 48 | c.publish.bind(c, function(){}).should.throw(); 49 | }); 50 | 51 | it("should throw an exception if topic contains wildcards", function(){ 52 | c.publish.bind(c, "*.hello").should.throw(); 53 | c.publish.bind(c, "hello.#").should.throw(); 54 | }); 55 | 56 | }); 57 | 58 | }); 59 | 60 | describe("Subscription", function(){ 61 | 62 | describe("limit", function(){ 63 | 64 | var c = new portage.Channel(), 65 | s = c.subscribe("topic", function(){}); 66 | 67 | it("should not throw an exception if limit >= 0", function(){ 68 | s.limit.bind(s, 0).should.not.throw(); 69 | s.limit.bind(s, 3).should.not.throw(); 70 | s.limit.bind(s, 10e9).should.not.throw(); 71 | }); 72 | 73 | it("should throw an exception if limit < 0", function(){ 74 | s.limit.bind(s, -10).should.throw(); 75 | s.limit.bind(s, -10e9).should.throw(); 76 | }); 77 | 78 | it("should throw an exception if limit is NaN", function(){ 79 | s.limit.bind(s, NaN).should.throw(); 80 | }); 81 | 82 | it("should throw an exception if limit is not a number", function(){ 83 | s.limit.bind(s, "hi").should.throw(); 84 | s.limit.bind(s, "55").should.throw(); 85 | s.limit.bind(s, false).should.throw(); 86 | s.limit.bind(s, true).should.throw(); 87 | s.limit.bind(s, {}).should.throw(); 88 | s.limit.bind(s, undefined).should.throw(); 89 | s.limit.bind(s, function(){}).should.throw(); 90 | s.limit.bind(s, /111/).should.throw(); 91 | }); 92 | 93 | }); 94 | 95 | }); 96 | 97 | }); 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Portage 2 | 3 | Fast pub/sub for JS 4 | 5 | `npm install portage` 6 | 7 | `bower install portage` 8 | 9 | Works with AMD, CommonJS, global (as `portage`) and ES6. 10 | 11 | ## Intro 12 | 13 | Portage is utilizing a [tree structure](https://github.com/EyalAr/FuzzyTree) to 14 | [quickly](#benchmark) match publications with subscriptions; including support 15 | for subscriptions with wild cards (see below). 16 | 17 | Publications and subscriptions are segmented by channels. Channels are used to 18 | publish messages on certain topics, and to subscribe to messages on certain 19 | topics (or a pattern of topics). 20 | 21 | A channel internally maintains a tree structure of subscriptions. 22 | 23 | Topics are organized in a hierarchical manner. For example, `chat.new-message` 24 | is a sub-topic of `chat`. This mostly affects the way the library efficiently 25 | filters subscription handlers when a message is published, but also relates 26 | to the usage of wild cards in subscriptions (see below). 27 | 28 | Hubs are simply an aggregation of channels, which can easily be accessed (and 29 | created on the fly) with a key. 30 | 31 | ## Hub API 32 | 33 | ``` 34 | var Hub = require('portage').Hub; 35 | var myHub = new Hub(); 36 | ``` 37 | 38 | ### Get / create a channel 39 | 40 | `var myChannel = myHub.channel(name)` 41 | 42 | - `name {String}`: The channel name. 43 | 44 | If channel `name` is requested for the first time, it is first created and then 45 | returned. Otherwise the existing channel with that name is returned. 46 | 47 | ### Default global hub 48 | 49 | `var portage = require('portage')` 50 | 51 | `portage` is a `Hub` object which is created when the library is loaded. 52 | `require('portage')` always returns the same default hub. 53 | 54 | ## Channel API 55 | 56 | `var Channel = require('portage').Channel` 57 | 58 | ### Create a channel 59 | 60 | `var myChannel = new Channel()` 61 | 62 | ### Publish data on a topic 63 | 64 | `myChannel.publish(topic, data)` 65 | 66 | - `topic {String}`: The topic of the publication. See topic structure below. 67 | - `data {*}`: Data of the publication. 68 | 69 | ### Subscribe to a topic 70 | 71 | `var s = myChannel.subscribe(pattern, callback)` 72 | 73 | - `pattern {String}`: The pattern of topics of which publications to subscribe. 74 | See topic structure below. 75 | - `callback {Function}`: Callback function to invoke when a message is published 76 | whose topic matches the pattern. The function is called with two arguments: 77 | 0. `data`: The published data. 78 | 0. `meta`: An object which contains the following properties: 79 | - `topic {String}`: The topic of the publication. 80 | - `called {Integer}`: The number of times this subscription has been 81 | invoked, **not** including the current time. 82 | - `limit {Integer}`: Invocation limit of this subscription, or null if 83 | no limit. 84 | - `last {Boolean}`: Flag if this invocation is the last. 85 | 86 | **Return value** is an object with the following methods: 87 | 88 | - `s.unsubscribe()`: removes this subscription 89 | - `s.once()`: limits invocation of this subscription to one additional time, 90 | after which it is automatically removed. 91 | - `s.limit(n)`: limits invocation of this subscription to `n` additional 92 | times, after which it is automatically removed. 93 | 94 | **Note:** If `s.limit(3)` is called after the subscription was already called 5 95 | times, it will be called up to 3 more times; up to a total of 8 times. 96 | 97 | ### Topics structure 98 | 99 | Topics are strings which are divided into sections by the `'.'` separator. 100 | For example, the topic `'chat.new-message.server'` has 3 sections: `'chat'`, 101 | `'new-message'` and `'server'`. 102 | 103 | On subscriptions, a topic pattern may be specified. A pattern can include wild 104 | cards of 2 types: 105 | 106 | - `'*'`: Non greedy wild card. Will match any one section. For example, 107 | `'chat.*.server'` will match both `'chat.new-message.server'` and 108 | `'chat.remove-message.server'`, **but not** `'chat.new-message.local'` and 109 | not `'chat.new.message.local'`. 110 | 111 | - `'#'`: Greedy wild card. Will match one or more sections. For example, 112 | `'chat.#'` will match `'chat.new-message.server'`, 113 | `'chat.remove-message.server'` **and** `'chat.new-message.local'`. Basically 114 | it will match any topic that begins with `'chat.'`. `'chat.#.local'` will 115 | match any topic that begins with `'chat'` and ends with `'local'`. `'#.local'` 116 | will match any topic that ends with `'local'`. 117 | 118 | ## Benchmark 119 | 120 | Benchmark against [Postal](https://github.com/postaljs/postal.js) is available 121 | at the `benchmark/` folder. 122 | 123 | Results have shown Portage to be more than **90% faster** than Postal. 124 | 125 | To run: 126 | 127 | ``` 128 | npm install 129 | npm run benchmark 130 | ``` 131 | -------------------------------------------------------------------------------- /tests/6.subscriptions_control.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("subscriptions control", function(){ 5 | 6 | describe("unsubscribe", function(){ 7 | 8 | var c = new portage.Channel(), 9 | count = 0; 10 | 11 | var s1 = c.subscribe("test.hello.world", function(){ 12 | count++; 13 | }); 14 | 15 | var s2 = c.subscribe("test.*.world", function(){ 16 | count++; 17 | }); 18 | 19 | var s3 = c.subscribe("test.#", function(){ 20 | count++; 21 | }); 22 | 23 | it("should invoke all subscriptions before unsubscribe", function(){ 24 | c.publish("test.hello.world"); 25 | should.equal(count, 3); 26 | }); 27 | 28 | it("should invoke some subscriptions after unsubscribe", function(){ 29 | s1.unsubscribe(); 30 | s3.unsubscribe(); 31 | count = 0; 32 | c.publish("test.hello.world"); 33 | should.equal(count, 1); 34 | }); 35 | 36 | }); 37 | 38 | describe("limit", function(){ 39 | 40 | describe("non zero", function(){ 41 | 42 | describe("before first publication", function(){ 43 | 44 | var c = new portage.Channel(), 45 | count = 0; 46 | 47 | c.subscribe("test.hello.world", function(){ 48 | count++; 49 | }).limit(3); 50 | 51 | it("should invoke subscription 3 times", function(){ 52 | c.publish("test.hello.world"); 53 | c.publish("test.hello.world"); 54 | c.publish("test.hello.world"); 55 | c.publish("test.hello.world"); 56 | c.publish("test.hello.world"); 57 | should.equal(count, 3); 58 | }); 59 | 60 | }); 61 | 62 | describe("after first publication", function(){ 63 | 64 | var c = new portage.Channel(), 65 | count = 0; 66 | 67 | var s = c.subscribe("test.hello.world", function(){ 68 | count++; 69 | }); 70 | 71 | it("should invoke subscription 4 times", function(){ 72 | c.publish("test.hello.world"); 73 | 74 | s.limit(3); 75 | 76 | c.publish("test.hello.world"); 77 | c.publish("test.hello.world"); 78 | c.publish("test.hello.world"); 79 | c.publish("test.hello.world"); 80 | 81 | should.equal(count, 4); 82 | }); 83 | 84 | }); 85 | 86 | }); 87 | 88 | describe("zero", function(){ 89 | 90 | describe("before first publication", function(){ 91 | 92 | var c = new portage.Channel(), 93 | count = 0; 94 | 95 | c.subscribe("test.hello.world", function(){ 96 | count++; 97 | }).limit(0); 98 | 99 | it("should invoke subscription 0 times", function(){ 100 | c.publish("test.hello.world"); 101 | c.publish("test.hello.world"); 102 | c.publish("test.hello.world"); 103 | c.publish("test.hello.world"); 104 | c.publish("test.hello.world"); 105 | should.equal(count, 0); 106 | }); 107 | 108 | }); 109 | 110 | describe("after first publication", function(){ 111 | 112 | var c = new portage.Channel(), 113 | count = 0; 114 | 115 | var s = c.subscribe("test.hello.world", function(){ 116 | count++; 117 | }); 118 | 119 | it("should invoke subscription 1 time", function(){ 120 | c.publish("test.hello.world"); 121 | 122 | s.limit(0); 123 | 124 | c.publish("test.hello.world"); 125 | c.publish("test.hello.world"); 126 | c.publish("test.hello.world"); 127 | c.publish("test.hello.world"); 128 | 129 | should.equal(count, 1); 130 | }); 131 | 132 | }); 133 | 134 | }); 135 | 136 | }); 137 | 138 | describe("once", function(){ 139 | 140 | var c = new portage.Channel(), 141 | count = 0; 142 | 143 | c.subscribe("test.hello.world", function(){ 144 | count++; 145 | }).once(); 146 | 147 | it("should invoke subscription 1 time", function(){ 148 | c.publish("test.hello.world"); 149 | c.publish("test.hello.world"); 150 | c.publish("test.hello.world"); 151 | c.publish("test.hello.world"); 152 | c.publish("test.hello.world"); 153 | should.equal(count, 1); 154 | }); 155 | 156 | }); 157 | 158 | }); 159 | -------------------------------------------------------------------------------- /dist/portage.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../webpack/universalModuleDefinition","../webpack/bootstrap 47ae5502d4ef935e7a2c",".././src/portage.js",".././src/Channel.js","../external {\"root\":\"_\",\"commonjs2\":\"lodash\",\"commonjs\":\"lodash\",\"amd\":\"lodash\"}","../external {\"root\":\"FuzzyTree\",\"commonjs2\":\"fuzzytree\",\"commonjs\":\"fuzzytree\",\"amd\":\"fuzzytree\"}",".././src/Subscription.js",".././src/Hub.js"],"names":[],"mappings":"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","file":"dist/portage.js","sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory(require(\"lodash\"), require(\"fuzzytree\"));\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([\"lodash\", \"fuzzytree\"], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"portage\"] = factory(require(\"lodash\"), require(\"fuzzytree\"));\n\telse\n\t\troot[\"portage\"] = factory(root[\"_\"], root[\"FuzzyTree\"]);\n})(this, function(__WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__) {\nreturn \n\n\n/** WEBPACK FOOTER **\n ** webpack/universalModuleDefinition\n **/"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n/** WEBPACK FOOTER **\n ** webpack/bootstrap 47ae5502d4ef935e7a2c\n **/","/* jshint esnext:true */\n\nimport Channel from './Channel';\nimport Hub from './Hub';\nimport Subscription from './Subscription';\n\nvar defaultHub = new Hub();\n\ndefaultHub.Channel = Channel;\ndefaultHub.Hub = Hub;\ndefaultHub.Subscription = Subscription;\n\nexport default defaultHub;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/portage.js\n **/","/* jshint esnext:true */\n\nimport {\n isFunction as _isFunction,\n forEach as _forEach,\n reduce as _reduce\n} from 'lodash';\nimport FuzzyTree from 'fuzzytree';\nimport Subscription from './Subscription';\n\nclass Channel{\n constructor(){\n this._tree = new FuzzyTree();\n this._strategy = defaultStrategy;\n }\n\n subscribe(topic, cb){\n if (!_isFunction(cb)) throw Error(\"callback must be a function\");\n\n var node = this._tree.find(topic);\n if (!node) {\n node = this._tree.insert(topic);\n node.setData([]);\n }\n var s = new Subscription(cb, node.getData());\n node.getData().push(s);\n return s;\n }\n\n publish(topic, data){\n var nodes = this._tree.match(topic),\n subs = _reduce(nodes, (subs, node) => {\n _forEach(node.getData(), s => subs.push(s));\n return subs;\n }, []);\n return this._strategy(subs, data, topic);\n }\n}\n\nfunction defaultStrategy(subs, data, topic){\n _forEach(subs, s => s.invoke(data, {\n topic: topic,\n called: s._called,\n limit: s._limit,\n last: s._limit !== null && s._called === s._limit - 1\n }));\n}\n\nexport default Channel;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Channel.js\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_2__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external {\"root\":\"_\",\"commonjs2\":\"lodash\",\"commonjs\":\"lodash\",\"amd\":\"lodash\"}\n ** module id = 2\n ** module chunks = 0\n **/","module.exports = __WEBPACK_EXTERNAL_MODULE_3__;\n\n\n/*****************\n ** WEBPACK FOOTER\n ** external {\"root\":\"FuzzyTree\",\"commonjs2\":\"fuzzytree\",\"commonjs\":\"fuzzytree\",\"amd\":\"fuzzytree\"}\n ** module id = 3\n ** module chunks = 0\n **/","/* jshint esnext:true */\n\nimport {\n isNumber as _isNumber,\n isNaN as _isNaN,\n gte as _gte,\n indexOf as _indexOf\n} from 'lodash';\n\nclass Subscription{\n constructor(cb, container){\n this._cb = cb;\n this._container = container;\n this._called = 0;\n this._limit = null;\n }\n\n invoke(data, meta){\n var r = this._cb(data, meta);\n this._called++;\n this._purge();\n return r;\n }\n\n _purge(){\n if (this._limit !== null && this._called >= this._limit)\n this.unsubscribe();\n }\n\n limit(n){\n if (!(_isNumber(n) && !_isNaN(n) && _gte(n, 0)))\n throw Error(\"limit must be a number greater or equal to 0\");\n this._limit = this._called + n;\n this._purge();\n return this;\n }\n\n once(){\n return this.limit(1);\n }\n\n unsubscribe(){\n var i = _indexOf(this._container, this);\n if (i !== -1) this._container.splice(i, 1);\n }\n}\n\nexport default Subscription;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Subscription.js\n **/","/* jshint esnext:true */\n\nimport {isString as _isString} from 'lodash';\nimport Channel from './Channel';\n\nclass Hub{\n constructor(){\n this._channels = {};\n }\n\n channel(name){\n if (!_isString(name)) throw Error(\"channel name must be a string\");\n return ( this._channels[name] = this._channels[name] || new Channel() );\n }\n}\n\nexport default Hub;\n\n\n\n/** WEBPACK FOOTER **\n ** ./src/Hub.js\n **/"],"sourceRoot":""} -------------------------------------------------------------------------------- /tests/3.greedy_pattern_topic.js: -------------------------------------------------------------------------------- 1 | import should from "should"; 2 | import portage from "../src/portage"; 3 | 4 | describe("greedy pattern topics", function(){ 5 | 6 | describe("single subscription", function(){ 7 | 8 | describe("single publication", function(){ 9 | 10 | describe("pattern with greedy wildcard (at the end)", function(){ 11 | 12 | var c = new portage.Channel(), 13 | topic1 = "test.hello.world", 14 | topic2 = "test.hello.world.foo", 15 | topic3 = "test.world", 16 | topic4 = "mo-match", // should not match 17 | pattern = "test.#", 18 | data = "foo", 19 | spy, 20 | called = 0; 21 | 22 | c.subscribe(pattern, function(_spy){ 23 | spy = _spy; 24 | called++; 25 | }); 26 | 27 | it("should invoke callback 3 times with correct data", function(){ 28 | c.publish(topic1, data); 29 | c.publish(topic2, data); 30 | c.publish(topic3, data); 31 | c.publish(topic4, data); 32 | should.equal(data, spy); 33 | should.equal(called, 3); 34 | }); 35 | 36 | }); 37 | 38 | describe("pattern with greedy wildcard (at the middle)", function(){ 39 | 40 | var c = new portage.Channel(), 41 | topic1 = "test.hello.world.foo.foo.bar", 42 | topic2 = "test.hello.world.foo.bar", 43 | topic3 = "test.world.foo.bar", 44 | topic4 = "test.world", // should not match 45 | pattern = "test.#.foo.bar", 46 | data = "foo", 47 | spy, 48 | called = 0; 49 | 50 | c.subscribe(pattern, function(_spy){ 51 | spy = _spy; 52 | called++; 53 | }); 54 | 55 | it("should invoke callback 3 times with correct data", function(){ 56 | c.publish(topic1, data); 57 | c.publish(topic2, data); 58 | c.publish(topic3, data); 59 | c.publish(topic4, data); 60 | should.equal(data, spy); 61 | should.equal(called, 3); 62 | }); 63 | 64 | }); 65 | 66 | describe("pattern with greedy wildcard (at the start)", function(){ 67 | 68 | var c = new portage.Channel(), 69 | topic1 = "test.hello.world.foo.foo.bar", 70 | topic2 = "test.hello.world.foo.bar", 71 | topic3 = "test.world.foo.bar", 72 | topic4 = "world.test", // should not match 73 | pattern = "#.foo.bar", 74 | data = "foo", 75 | spy, 76 | called = 0; 77 | 78 | c.subscribe(pattern, function(_spy){ 79 | spy = _spy; 80 | called++; 81 | }); 82 | 83 | it("should invoke callback 3 times with correct data", function(){ 84 | c.publish(topic1, data); 85 | c.publish(topic2, data); 86 | c.publish(topic3, data); 87 | c.publish(topic4, data); 88 | should.equal(data, spy); 89 | should.equal(called, 3); 90 | }); 91 | 92 | }); 93 | 94 | describe("pattern with greedy wildcard (multiple)", function(){ 95 | 96 | var c = new portage.Channel(), 97 | topic1 = "test.hello.world.bar.foo.foo.boo.baz", 98 | topic2 = "test.hello.world.bar.foo.boo.baz", 99 | topic3 = "test.world.bar.foo.baz", 100 | topic4 = "world.test.foo", // should not match 101 | pattern = "test.#.bar.#.baz", 102 | data = "foo", 103 | spy, 104 | called = 0; 105 | 106 | c.subscribe(pattern, function(_spy){ 107 | spy = _spy; 108 | called++; 109 | }); 110 | 111 | it("should invoke callback 3 times with correct data", function(){ 112 | c.publish(topic1, data); 113 | c.publish(topic2, data); 114 | c.publish(topic3, data); 115 | c.publish(topic4, data); 116 | should.equal(data, spy); 117 | should.equal(called, 3); 118 | }); 119 | 120 | }); 121 | 122 | describe("pattern with greedy wildcard (multiple, consecutive)", function(){ 123 | 124 | var c = new portage.Channel(), 125 | topic1 = "test.hello.world.bar.foo.foo.boo.baz.bar", 126 | topic2 = "test.hello.world.bar.foo.boo.baz.bar", 127 | topic3 = "test.world.bar.baz.bar", 128 | topic4 = "world.test.foo", // should not match 129 | pattern = "test.#.#.baz.bar", 130 | data = "foo", 131 | spy, 132 | called = 0; 133 | 134 | c.subscribe(pattern, function(_spy){ 135 | spy = _spy; 136 | called++; 137 | }); 138 | 139 | it("should invoke callback 3 times with correct data", function(){ 140 | c.publish(topic1, data); 141 | c.publish(topic2, data); 142 | c.publish(topic3, data); 143 | c.publish(topic4, data); 144 | should.equal(data, spy); 145 | should.equal(called, 3); 146 | }); 147 | 148 | }); 149 | 150 | describe("pattern with greedy wildcard (full)", function(){ 151 | 152 | var c = new portage.Channel(), 153 | topic = "test.hello.world", 154 | pattern = "#", 155 | data = "foo", 156 | spy, 157 | called = 0; 158 | 159 | c.subscribe(pattern, function(_spy){ 160 | spy = _spy; 161 | called++; 162 | }); 163 | 164 | it("should invoke callback once with correct data", function(){ 165 | c.publish(topic, data); 166 | should.equal(data, spy); 167 | should.equal(called, 1); 168 | }); 169 | 170 | }); 171 | 172 | describe("pattern with greedy wildcard (full, multiple)", function(){ 173 | 174 | var c = new portage.Channel(), 175 | topic = "test.hello.world", 176 | pattern = "#.#", 177 | data = "foo", 178 | spy, 179 | called = 0; 180 | 181 | c.subscribe(pattern, function(_spy){ 182 | spy = _spy; 183 | called++; 184 | }); 185 | 186 | it("should invoke callback once with correct data", function(){ 187 | c.publish(topic, data); 188 | should.equal(data, spy); 189 | should.equal(called, 1); 190 | }); 191 | 192 | }); 193 | 194 | }); 195 | 196 | describe("multiple publications", function(){ 197 | 198 | describe("pattern with greedy wildcard (partial)", function(){ 199 | 200 | var c = new portage.Channel(), 201 | topic1 = "test.hello.world", 202 | topic2 = "test.world.hello", 203 | pattern = "test.#", 204 | data = "foo", 205 | spy, 206 | called = 0; 207 | 208 | c.subscribe(pattern, function(_spy){ 209 | spy = _spy; 210 | called++; 211 | }); 212 | 213 | it("should invoke callback twice with correct data", function(){ 214 | c.publish(topic1, data); 215 | c.publish(topic2, data); 216 | 217 | should.equal(data, spy); 218 | should.equal(called, 2); 219 | }); 220 | 221 | }); 222 | 223 | describe("pattern with greedy wildcard (full)", function(){ 224 | 225 | var c = new portage.Channel(), 226 | topic1 = "test.hello.world", 227 | topic2 = "test.world.hello", 228 | pattern = "#", 229 | data = "foo", 230 | spy, 231 | called = 0; 232 | 233 | c.subscribe(pattern, function(_spy){ 234 | spy = _spy; 235 | called++; 236 | }); 237 | 238 | it("should invoke callback twice with correct data", function(){ 239 | c.publish(topic1, data); 240 | c.publish(topic2, data); 241 | 242 | should.equal(data, spy); 243 | should.equal(called, 2); 244 | }); 245 | 246 | }); 247 | 248 | }); 249 | 250 | }); 251 | 252 | }); 253 | -------------------------------------------------------------------------------- /dist/portage.js: -------------------------------------------------------------------------------- 1 | (function webpackUniversalModuleDefinition(root, factory) { 2 | if(typeof exports === 'object' && typeof module === 'object') 3 | module.exports = factory(require("lodash"), require("fuzzytree")); 4 | else if(typeof define === 'function' && define.amd) 5 | define(["lodash", "fuzzytree"], factory); 6 | else if(typeof exports === 'object') 7 | exports["portage"] = factory(require("lodash"), require("fuzzytree")); 8 | else 9 | root["portage"] = factory(root["_"], root["FuzzyTree"]); 10 | })(this, function(__WEBPACK_EXTERNAL_MODULE_2__, __WEBPACK_EXTERNAL_MODULE_3__) { 11 | return /******/ (function(modules) { // webpackBootstrap 12 | /******/ // The module cache 13 | /******/ var installedModules = {}; 14 | /******/ 15 | /******/ // The require function 16 | /******/ function __webpack_require__(moduleId) { 17 | /******/ 18 | /******/ // Check if module is in cache 19 | /******/ if(installedModules[moduleId]) 20 | /******/ return installedModules[moduleId].exports; 21 | /******/ 22 | /******/ // Create a new module (and put it into the cache) 23 | /******/ var module = installedModules[moduleId] = { 24 | /******/ exports: {}, 25 | /******/ id: moduleId, 26 | /******/ loaded: false 27 | /******/ }; 28 | /******/ 29 | /******/ // Execute the module function 30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 31 | /******/ 32 | /******/ // Flag the module as loaded 33 | /******/ module.loaded = true; 34 | /******/ 35 | /******/ // Return the exports of the module 36 | /******/ return module.exports; 37 | /******/ } 38 | /******/ 39 | /******/ 40 | /******/ // expose the modules object (__webpack_modules__) 41 | /******/ __webpack_require__.m = modules; 42 | /******/ 43 | /******/ // expose the module cache 44 | /******/ __webpack_require__.c = installedModules; 45 | /******/ 46 | /******/ // __webpack_public_path__ 47 | /******/ __webpack_require__.p = ""; 48 | /******/ 49 | /******/ // Load entry module and return exports 50 | /******/ return __webpack_require__(0); 51 | /******/ }) 52 | /************************************************************************/ 53 | /******/ ([ 54 | /* 0 */ 55 | /***/ function(module, exports, __webpack_require__) { 56 | 57 | /* jshint esnext:true */ 58 | 59 | 'use strict'; 60 | 61 | Object.defineProperty(exports, '__esModule', { 62 | value: true 63 | }); 64 | 65 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 66 | 67 | var _Channel = __webpack_require__(1); 68 | 69 | var _Channel2 = _interopRequireDefault(_Channel); 70 | 71 | var _Hub = __webpack_require__(5); 72 | 73 | var _Hub2 = _interopRequireDefault(_Hub); 74 | 75 | var _Subscription = __webpack_require__(4); 76 | 77 | var _Subscription2 = _interopRequireDefault(_Subscription); 78 | 79 | var defaultHub = new _Hub2['default'](); 80 | 81 | defaultHub.Channel = _Channel2['default']; 82 | defaultHub.Hub = _Hub2['default']; 83 | defaultHub.Subscription = _Subscription2['default']; 84 | 85 | exports['default'] = defaultHub; 86 | module.exports = exports['default']; 87 | 88 | /***/ }, 89 | /* 1 */ 90 | /***/ function(module, exports, __webpack_require__) { 91 | 92 | /* jshint esnext:true */ 93 | 94 | 'use strict'; 95 | 96 | Object.defineProperty(exports, '__esModule', { 97 | value: true 98 | }); 99 | 100 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 101 | 102 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 103 | 104 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 105 | 106 | var _lodash = __webpack_require__(2); 107 | 108 | var _fuzzytree = __webpack_require__(3); 109 | 110 | var _fuzzytree2 = _interopRequireDefault(_fuzzytree); 111 | 112 | var _Subscription = __webpack_require__(4); 113 | 114 | var _Subscription2 = _interopRequireDefault(_Subscription); 115 | 116 | var Channel = (function () { 117 | function Channel() { 118 | _classCallCheck(this, Channel); 119 | 120 | this._tree = new _fuzzytree2['default'](); 121 | this._strategy = defaultStrategy; 122 | } 123 | 124 | _createClass(Channel, [{ 125 | key: 'subscribe', 126 | value: function subscribe(topic, cb) { 127 | if (!(0, _lodash.isFunction)(cb)) throw Error('callback must be a function'); 128 | 129 | var node = this._tree.find(topic); 130 | if (!node) { 131 | node = this._tree.insert(topic); 132 | node.setData([]); 133 | } 134 | var s = new _Subscription2['default'](cb, node.getData()); 135 | node.getData().push(s); 136 | return s; 137 | } 138 | }, { 139 | key: 'publish', 140 | value: function publish(topic, data) { 141 | var nodes = this._tree.match(topic), 142 | subs = (0, _lodash.reduce)(nodes, function (subs, node) { 143 | (0, _lodash.forEach)(node.getData(), function (s) { 144 | return subs.push(s); 145 | }); 146 | return subs; 147 | }, []); 148 | return this._strategy(subs, data, topic); 149 | } 150 | }]); 151 | 152 | return Channel; 153 | })(); 154 | 155 | function defaultStrategy(subs, data, topic) { 156 | (0, _lodash.forEach)(subs, function (s) { 157 | return s.invoke(data, { 158 | topic: topic, 159 | called: s._called, 160 | limit: s._limit, 161 | last: s._limit !== null && s._called === s._limit - 1 162 | }); 163 | }); 164 | } 165 | 166 | exports['default'] = Channel; 167 | module.exports = exports['default']; 168 | 169 | /***/ }, 170 | /* 2 */ 171 | /***/ function(module, exports) { 172 | 173 | module.exports = __WEBPACK_EXTERNAL_MODULE_2__; 174 | 175 | /***/ }, 176 | /* 3 */ 177 | /***/ function(module, exports) { 178 | 179 | module.exports = __WEBPACK_EXTERNAL_MODULE_3__; 180 | 181 | /***/ }, 182 | /* 4 */ 183 | /***/ function(module, exports, __webpack_require__) { 184 | 185 | /* jshint esnext:true */ 186 | 187 | "use strict"; 188 | 189 | Object.defineProperty(exports, "__esModule", { 190 | value: true 191 | }); 192 | 193 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 194 | 195 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 196 | 197 | var _lodash = __webpack_require__(2); 198 | 199 | var Subscription = (function () { 200 | function Subscription(cb, container) { 201 | _classCallCheck(this, Subscription); 202 | 203 | this._cb = cb; 204 | this._container = container; 205 | this._called = 0; 206 | this._limit = null; 207 | } 208 | 209 | _createClass(Subscription, [{ 210 | key: "invoke", 211 | value: function invoke(data, meta) { 212 | var r = this._cb(data, meta); 213 | this._called++; 214 | this._purge(); 215 | return r; 216 | } 217 | }, { 218 | key: "_purge", 219 | value: function _purge() { 220 | if (this._limit !== null && this._called >= this._limit) this.unsubscribe(); 221 | } 222 | }, { 223 | key: "limit", 224 | value: function limit(n) { 225 | if (!((0, _lodash.isNumber)(n) && !(0, _lodash.isNaN)(n) && (0, _lodash.gte)(n, 0))) throw Error("limit must be a number greater or equal to 0"); 226 | this._limit = this._called + n; 227 | this._purge(); 228 | return this; 229 | } 230 | }, { 231 | key: "once", 232 | value: function once() { 233 | return this.limit(1); 234 | } 235 | }, { 236 | key: "unsubscribe", 237 | value: function unsubscribe() { 238 | var i = (0, _lodash.indexOf)(this._container, this); 239 | if (i !== -1) this._container.splice(i, 1); 240 | } 241 | }]); 242 | 243 | return Subscription; 244 | })(); 245 | 246 | exports["default"] = Subscription; 247 | module.exports = exports["default"]; 248 | 249 | /***/ }, 250 | /* 5 */ 251 | /***/ function(module, exports, __webpack_require__) { 252 | 253 | /* jshint esnext:true */ 254 | 255 | 'use strict'; 256 | 257 | Object.defineProperty(exports, '__esModule', { 258 | value: true 259 | }); 260 | 261 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 262 | 263 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 264 | 265 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 266 | 267 | var _lodash = __webpack_require__(2); 268 | 269 | var _Channel = __webpack_require__(1); 270 | 271 | var _Channel2 = _interopRequireDefault(_Channel); 272 | 273 | var Hub = (function () { 274 | function Hub() { 275 | _classCallCheck(this, Hub); 276 | 277 | this._channels = {}; 278 | } 279 | 280 | _createClass(Hub, [{ 281 | key: 'channel', 282 | value: function channel(name) { 283 | if (!(0, _lodash.isString)(name)) throw Error('channel name must be a string'); 284 | return this._channels[name] = this._channels[name] || new _Channel2['default'](); 285 | } 286 | }]); 287 | 288 | return Hub; 289 | })(); 290 | 291 | exports['default'] = Hub; 292 | module.exports = exports['default']; 293 | 294 | /***/ } 295 | /******/ ]) 296 | }); 297 | ; 298 | //# sourceMappingURL=portage.js.map --------------------------------------------------------------------------------