Skip to content

Commit

Permalink
Initial commit of some code I had lying around.
Browse files Browse the repository at this point in the history
  • Loading branch information
domenic committed Jan 25, 2012
0 parents commit f308d01
Show file tree
Hide file tree
Showing 6 changed files with 444 additions and 0 deletions.
123 changes: 123 additions & 0 deletions lib/pubit.js
@@ -0,0 +1,123 @@
// Still TODO:
// * .mixinEmitter()
// * more argument validation (only subscribe and unsubscribe are covered now)
// * Utility functions (on the module?) for throttled/debounced listening; move subscribeOnce to this style.
// * think about the role of async (publisher? subscriber? always?)

"use strict";

var dict = require("dict");

function isHash(unknown) {
return typeof unknown === "object" && unknown !== null;
}

exports.Publisher = function () {
var that = this;

var handlerDict = dict();
var onSubscriberError = function () { };

function callHandler(handler, args) {
try {
handler.apply(null, args);
} catch (e) {
onSubscriberError(e);
}
}

function subscribeSingleHandler(eventName, handler) {
if (!handlerDict.has(eventName)) {
handlerDict.set(eventName, []);
}

handlerDict.get(eventName).push(handler);
}

function subscribeMultipleHandlers(hash) {
Object.keys(hash).forEach(function (eventName) {
subscribeSingleHandler(eventName, hash[eventName]);
});
}

function unsubscribeSingleHandler(eventName, handler) {
if (!handlerDict.has(eventName)) {
return;
}

var handlersArray = handlerDict.get(eventName);
for (var i = 0; i < handlersArray.length; ++i) {
if (handlersArray[i] === handler) {
handlersArray.splice(i, 1);
break;
}
}
}

function unsubscribeMultipleHandlers(hash) {
Object.keys(hash).forEach(function (eventName) {
unsubscribeSingleHandler(eventName, hash[eventName]);
});
}

function unsubscribeAllHandlers(eventName) {
handlerDict.delete(eventName);
}

// TODO constructor injection instead of setter injection?
that.setSubscriberErrorCallback = function (newOnSubscriberError) {
onSubscriberError = newOnSubscriberError;
};

that.publish = function (eventName, args) {
if (handlerDict.has(eventName)) {
var errorsThrown = [];

// .slice() is important to deal with self-unsubscribing handlers
handlerDict.get(eventName).slice().forEach(function (handler) {
callHandler(handler, args);
});
}
};


that.emitter = {};

that.emitter.subscribe = function (eventNameOrHash, handler) {
if (arguments.length === 1) {
if (!isHash(eventNameOrHash)) {
throw new TypeError("hash argument must be a string-to-function hash.");
}

subscribeMultipleHandlers(eventNameOrHash);
} else {
if (typeof eventNameOrHash !== "string") {
throw new TypeError("eventName argument must be a string.");
}
if (typeof handler !== "function") {
throw new TypeError("handler argument must be a function.");
}
subscribeSingleHandler(eventNameOrHash, handler);
}
};

that.emitter.unsubscribe = function (eventNameOrHash, handler) {
if (typeof eventNameOrHash === "string") {
if (typeof handler === "undefined") {
unsubscribeAllHandlers(eventNameOrHash);
} else if (typeof handler === "function") {
unsubscribeSingleHandler(eventNameOrHash, handler);
} else {
throw new TypeError("handler argument must be a function.");
}
} else if (isHash(eventNameOrHash)) {
unsubscribeMultipleHandlers(eventNameOrHash);
} else {
if (arguments.length === 2) {
throw new TypeError("eventName argument must be a string.");
} else {
throw new TypeError("eventNameOrHash argument must be a string or string-to-function hash.");
}
}
};
};
38 changes: 38 additions & 0 deletions package.json
@@ -0,0 +1,38 @@
{
"name": "pubit",
"description": "Responsible publish-subscribe. Hide the publisher and expose an emitter.",
"keywords": [
"pubsub",
"pub/sub",
"eventemitter",
"events"
],
"version": "0.0.0",
"author": "Domenic Denicola <domenic@domenicdenicola.com> (http://domenicdenicola.com)",
"repository": {
"type": "git",
"url": "https://github.com/DomenicDenicola/pubit"
},
"bugs": {
"url": "http://github.com/DomenicDenicola/pubit/issues"
},
"directories": {
"lib": "./lib"
},
"main": "./lib/pubit.js",
"scripts": {
"test": ".\\node_modules\\.bin\\mocha"
},
"engines": {
"node": "*"
},
"dependencies": {
"dict": "1"
},
"devDependencies": {
"coffee-script": "*",
"mocha": "*",
"chai": "*",
"sinon": "*"
}
}
69 changes: 69 additions & 0 deletions test/argumentValidation.coffee
@@ -0,0 +1,69 @@
expect = require("chai").expect
Assertion = require("chai").Assertion

Publisher = require("../lib/pubit").Publisher

# Add a throwArgumentError(argName, type) matcher.
Assertion.prototype.throwArgumentError = (argName, type) ->
(new Assertion(@obj)).is.a("function")

try
@obj()
catch error
@assert(
error instanceof TypeError and error.message is "#{ argName } argument must be a #{ type }.",
"expected #{ @inspect } to throw an argument error requiring #{ argName } to be of type #{ type }"
)

return this

describe "Emitter argument validation", ->
publisher = null
emitter = null

beforeEach ->
publisher = new Publisher()
emitter = publisher.emitter

describe ".subscribe(eventName, handler)", ->
it "throws an error when give a string and a number", ->
expect(-> emitter.subscribe("eventName", 5)).to.throwArgumentError("handler", "function")

it "throws an error when give a string and null", ->
expect(-> emitter.subscribe("eventName", null)).to.throwArgumentError("handler", "function")

it "throws an error when give null and a function", ->
expect(-> emitter.subscribe(null, ->)).to.throwArgumentError("eventName", "string")

it "throws an error when give a number and a function", ->
expect(-> emitter.subscribe(5, ->)).to.throwArgumentError("eventName", "string")

describe ".subscribe(eventHash)", ->
it "throws an error when given a number", ->
expect(-> emitter.subscribe(5)).to.throwArgumentError("hash", "string-to-function hash")

it "throws an error when given null", ->
expect(-> emitter.subscribe(null)).to.throwArgumentError("hash", "string-to-function hash")

it "throws an error when given a string by itself", ->
expect(-> emitter.subscribe("eventName")).to.throwArgumentError("hash", "string-to-function hash")

describe ".unsubscribe(eventName, handler)", ->
it "throws an error when give a string and a number", ->
expect(-> emitter.unsubscribe("eventName", 5)).to.throwArgumentError("handler", "function")

it "throws an error when give a string and null", ->
expect(-> emitter.unsubscribe("eventName", null)).to.throwArgumentError("handler", "function")

it "throws an error when give null and a function", ->
expect(-> emitter.unsubscribe(null, ->)).to.throwArgumentError("eventName", "string")

it "throws an error when give a number and a function", ->
expect(-> emitter.unsubscribe(5, ->)).to.throwArgumentError("eventName", "string")

describe ".unsubscribe(eventHash)", ->
it "throws an error when given a number", ->
expect(-> emitter.unsubscribe(5)).to.throwArgumentError("eventNameOrHash", "string or string-to-function hash")

it "throws an error when given null", ->
expect(-> emitter.unsubscribe(null)).to.throwArgumentError("eventNameOrHash", "string or string-to-function hash")
126 changes: 126 additions & 0 deletions test/basicFunctionality.coffee
@@ -0,0 +1,126 @@
require("chai").should()
sinon = require("sinon")

Publisher = require("../lib/pubit").Publisher

describe "Publisher/emitter under normal usage", ->
publisher = null
emitter = null

beforeEach ->
publisher = new Publisher()
emitter = publisher.emitter

describe "when an event has been subscribed to", ->
handler = null

beforeEach ->
handler = sinon.spy()
emitter.subscribe("eventName", handler)

it "should call the subscribing handler when the event is published", ->
publisher.publish("eventName")

sinon.assert.calledOnce(handler)

it "should call the subscribing handler with the supplied arguments when the event is published with arguments", ->
publisher.publish("eventName", [1, "foo"])

sinon.assert.calledWithExactly(handler, 1, "foo")

it "should not call the handler when publishing a different event", ->
publisher.publish("anotherEvent")

sinon.assert.notCalled(handler)

it "should not call the handler when publishing after unsubscribing from the event", ->
emitter.unsubscribe("eventName", handler)
publisher.publish("eventName")

sinon.assert.notCalled(handler)

describe "when an event has been subscribed to twice by the same handler", ->
handler = null

beforeEach ->
handler = sinon.spy()
emitter.subscribe("eventName", handler)
emitter.subscribe("eventName", handler)

it "should call the subscribing handler twice when the event is published", ->
publisher.publish("eventName")

handler.callCount.should.equal(2)

it "should call the subscribing handler once when the event is unsubscribed from once, then published", ->
emitter.unsubscribe("eventName", handler)
publisher.publish("eventName")

handler.callCount.should.equal(1)

it "should not call the subscribing handler when the event is unsubscribed from twice, then published", ->
emitter.unsubscribe("eventName", handler)
emitter.unsubscribe("eventName", handler)
publisher.publish("eventName")

sinon.assert.notCalled(handler)

describe "when an event has been subscribed to by two different handlers", ->
handler1 = null
handler2 = null

beforeEach ->
handler1 = sinon.spy()
handler2 = sinon.spy()

emitter.subscribe("eventName", handler1)
emitter.subscribe("eventName", handler2)

it "should call both handlers when the event is published", ->
publisher.publish("eventName")

sinon.assert.calledOnce(handler1)
sinon.assert.calledOnce(handler2)

it "should call only one handler when the other unsubscribes, then the event is published", ->
emitter.unsubscribe("eventName", handler1)
publisher.publish("eventName")

sinon.assert.notCalled(handler1)
sinon.assert.calledOnce(handler2)

it "should call neither handler when the event is blanket-unsubscribed, then published", ->
emitter.unsubscribe("eventName")
publisher.publish("eventName")

sinon.assert.notCalled(handler1)
sinon.assert.notCalled(handler2)

describe "when a hash object mapping event names to handlers is used for subscription", ->
hash = null

beforeEach ->
hash =
event1: sinon.spy()
event2: sinon.spy()
event3: sinon.spy()
emitter.subscribe(hash)

it "publishes events correctly", ->
publisher.publish("event1")
publisher.publish("event2")

sinon.assert.calledOnce(hash.event1)
sinon.assert.calledOnce(hash.event2)
sinon.assert.notCalled(hash.event3)

it "does not publish events when they are mass-unsubscribed using the same hash", ->
emitter.unsubscribe(hash)

publisher.publish("event1")
publisher.publish("event2")
publisher.publish("event3")

sinon.assert.notCalled(hash.event1)
sinon.assert.notCalled(hash.event2)
sinon.assert.notCalled(hash.event3)
1 change: 1 addition & 0 deletions test/mocha.opts
@@ -0,0 +1 @@
--reporter spec

0 comments on commit f308d01

Please sign in to comment.