Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit of some code I had lying around.
- Loading branch information
0 parents
commit f308d01
Showing
6 changed files
with
444 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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."); | ||
} | ||
} | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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": "*" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
--reporter spec |
Oops, something went wrong.