Skip to content

Commit

Permalink
Merge 164e46d into 81108f2
Browse files Browse the repository at this point in the history
  • Loading branch information
stwa committed Jun 17, 2019
2 parents 81108f2 + 164e46d commit b70ccb4
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,9 @@ export function rawConnector(connectorId, connectorFn) {
if (signalCache.has(connectorId)) {
signalCache.delete(connectorId);
}
if (registry.has(connectorId)) {
console.warn("overwriting connector for", connectorId);
}
registry.set(connectorId, connectorFn);
}

Expand Down
8 changes: 8 additions & 0 deletions src/connector.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ describe("connector", () => {
connector("foo", () => {});
const s2 = connect("foo");
expect(s1).not.toBe(s2);
expect(global.console.warn).toHaveBeenCalledWith(
"overwriting connector for",
"foo",
);
});
});

Expand All @@ -87,6 +91,10 @@ describe("rawConnector", () => {
rawConnector("foo", () => signalFn(() => "foo"));
const s2 = connect("foo");
expect(s1).not.toBe(s2);
expect(global.console.warn).toHaveBeenCalledWith(
"overwriting connector for",
"foo",
);
});
});

Expand Down
51 changes: 51 additions & 0 deletions src/effect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* An effector is the place where all the dirty stuff happens. By providing an
* effects identifier the registered effect handler gets called and must do the
* required mutations to the world. This can be anything, e.g. reset a
* `signal`s state, `trigger` events, change the browsers state (e.g. updating a
* scrollbars position) or doing xhrs, just to give some examples.
*
* @module effect
*/

/** Holds the registered effectors. */
let registry = new Map();

/**
* Registers an effect handler identified by `effectId`.
*
* @param {string} effectId An effect identifier.
* @param {function} handlerFn A function which gets passed the arguments from
* the call to `effect`.
*/
export function effector(effectId, handlerFn) {
if (registry.has(effectId)) {
console.warn("overwriting effector for", effectId);
}
registry.set(effectId, handlerFn);
}

/**
* Calls the effect handler identified by `effectId` with the provided `args`.
*
* Note: for any given call to `effect` there must be a previous call to
* `effector`, registering a handler function for `effectId`.
*
* @param {string} effectId The effect identifier.
* @param {...any} args Arguments passed to the effect handler.
*/
export function effect(effectId, ...args) {
let handlerFn = registry.get(effectId);
if (handlerFn) {
handlerFn(...args);
return;
}
console.warn("no effector registered for:", effectId);
}

/**
* Clears all registered effectors.
*/
export function clearEffectors() {
registry.clear();
}
35 changes: 35 additions & 0 deletions src/effect.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { clearEffectors, effect, effector } from "./effect";

/* global global */

beforeEach(() => {
global.console.warn = jest.fn();
clearEffectors();
});

describe("effect", () => {
it("handles effects", () => {
const result = {};
effector("foo", value => (result.foo = value));
effect("foo", "bar");
expect(result).toEqual({ foo: "bar" });
});
it("logs a warning for unknown effectors", () => {
effect("foo", "bar");
expect(global.console.warn).toHaveBeenCalledWith(
"no effector registered for:",
"foo",
);
});
});

describe("effector", () => {
it("logs a warning when overwriting an existing effector", () => {
effector("foo", () => {});
effector("foo", () => {});
expect(global.console.warn).toHaveBeenCalledWith(
"overwriting effector for",
"foo",
);
});
});
131 changes: 131 additions & 0 deletions src/event.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { queue } from "./internal/queue";
import { effect } from "./effect";

/**
* The event module provides machanisms to trigger and handle the happening of
* events. Raw event handlers are based on reading from and writing to contexts.
* On top of the provided event contexts there is the concept of an event
* handler having causes and describing effects. Therefore, an event handler is
* a pure function that doesn't perform changes but describes them.
*
* @module event
*/

/** Holds the registered event handlers. */
let registry = new Map();

export const eventQueue = queue();

/**
* Registers an event handler identified by `eventId`.
*
* The event handler gets called whenever an event with the provided `eventId`
* gets triggered. It will receive a map of causes related to the event and must
* return a map which describes the resulting effects. The resulting effects are
* then performed by specific effect handlers. Interceptors can be added to
* perform additional actions based on the resulting context.
*
* @param {string} eventId An event identifier.
* @param {function} handlerFn A function which gets passed causes of the event
* and must return a map of effects that should be performed.
* @param {function[]} interceptors A list of interceptor functions.
*
* @see {@link event.effectsInterceptor} for an interceptor example.
*/
export function handler(eventId, handlerFn, interceptors = []) {
return rawHandler(
eventId,
context => {
let [eventId, args] = context.causes.event;
context.effects = handlerFn(context.causes, eventId, ...args);
return context;
},
[effectsInterceptor, ...interceptors],
);
}

/**
* Registers a raw event handler identified by `eventId`.
*
* The event handler gets called whenever an event with the provided `eventId`
* gets triggered. It will receive a context related to the event and must
* return a (modified) context. Interceptors can be added to actually perform
* actions based on the resulting context.
*
* @param {string} eventId An event identifier.
* @param {function} handlerFn A function which gets passed a context describing
* the causes of the event and modifies the context.
* @param {function[]} interceptors A list of interceptor functions.
*
* @see {@link event.effectsInterceptor} for an interceptor example.
*/
export function rawHandler(eventId, handlerFn, interceptors = []) {
interceptors.reverse();
let handlerChain = interceptors.reduce(
(handler, interceptor) => interceptor(handler),
handlerFn,
);
let handler = (eventId, ...args) => {
let context = {
causes: {
event: [eventId, args],
},
};
return handlerChain(context);
};
if (registry.has(eventId)) {
console.warn("overwriting handler for", eventId);
}
registry.set(eventId, handler);
return handler;
}

/**
* Enqueues an event for processing. Processing will not happen immediately, but
* on the next tick after all previously triggered events were handled.
*
* @param {string} eventId The event identifier.
* @param {...any} args Additional arguments describing the event.
*/
export function trigger(eventId, ...args) {
eventQueue.enqueue(() => handle(eventId, ...args));
}

/**
* Triggers an event immediately without queueing.
*
* @param {string} eventId The event identifier.
* @param {...any} args Additional arguments describing the event.
*/
export function triggerImmediately(eventId, ...args) {
handle(eventId, ...args);
}

/**
* Clears all registered handlers.
*/
export function clearHandlers() {
registry.clear();
}

/**
* An interceptor which calls the corresponding effect handler for each
* described effect in `context.effects`.
*/
export function effectsInterceptor(nextFn) {
return context => {
context = nextFn(context);
for (let effectId in context.effects) {
effect(effectId, context.effects[effectId]);
}
return context;
};
}

function handle(eventId, ...args) {
let handlerFn = registry.get(eventId);
if (handlerFn) {
return handlerFn(eventId, ...args);
}
console.warn("no handler registered for:", eventId);
}
135 changes: 135 additions & 0 deletions src/event.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {
clearHandlers,
eventQueue,
trigger,
triggerImmediately,
handler,
rawHandler,
} from "./event";
import { clearEffectors, effector } from "./effect";

/* global global */

let ticker = (function() {
let fns = [];
return {
dispatch(fn) {
fns.push(fn);
},
advance() {
let fn = fns.pop();
if (fn) fn();
},
size() {
return fns.length;
},
};
})();

let dummyInterceptor = value => nextFn => context => {
context.before = [...(context.before || []), value];
context = nextFn(context);
context.after = [...(context.after || []), value];
return context;
};

beforeEach(() => {
global.console.warn = jest.fn();
eventQueue.tickFn(ticker.dispatch);
clearHandlers();
clearEffectors();
});

describe("handler", () => {
it("passes causes and event information to the handlerFn", () => {
let handlerFn = handler("foo", (causes, eventId, ...args) => {
expect(causes.event).toStrictEqual([eventId, args]);
expect(eventId).toBe("foo");
expect(args).toStrictEqual(["bar", "baz"]);

return { succeed: true };
});
let context = handlerFn("foo", "bar", "baz");
expect(context.causes.event).toStrictEqual(["foo", ["bar", "baz"]]);
expect(context.effects.succeed).toBeTruthy();
});
it("handles effects from context", () => {
let succeed = false;
let handlerFn = handler("foo", () => ({ bar: "baz" }));
effector("bar", arg => {
expect(arg).toBe("baz");
succeed = true;
});
expect(succeed).toBeFalsy();
handlerFn("foo");
expect(succeed).toBeTruthy();
});
});

describe("rawHandler", () => {
it("passes context and event information to the handler", () => {
let handlerFn = rawHandler("foo", context => {
let [eventId, args] = context.causes.event;
expect(eventId).toBe("foo");
expect(args).toStrictEqual(["bar", "baz"]);
context.effects = { succeed: true };
return context;
});
let context = handlerFn("foo", "bar", "baz");
expect(context.causes.event).toStrictEqual(["foo", ["bar", "baz"]]);
expect(context.effects.succeed).toBeTruthy();
});
it("chains and runs interceptors in correct order", () => {
let handlerFn = rawHandler("foo", context => context, [
dummyInterceptor("a"),
dummyInterceptor("b"),
dummyInterceptor("c"),
]);
let context = handlerFn("foo");
expect(context.before).toEqual(["a", "b", "c"]);
expect(context.after).toEqual(["c", "b", "a"]);
});
it("logs a warning when overwriting an existing handler", () => {
rawHandler("foo", context => context);
rawHandler("foo", context => context);
expect(global.console.warn).toHaveBeenCalledWith(
"overwriting handler for",
"foo",
);
});
});

describe("trigger", () => {
it("queues the call of the registered handler", () => {
let handled = 0;
handler("foo", () => handled++);
trigger("foo");
expect(handled).toBe(0);
ticker.advance();
expect(handled).toBe(1);
});
it("logs a warning for unknown events", () => {
trigger("bar");
ticker.advance();
expect(global.console.warn).toHaveBeenCalledWith(
"no handler registered for:",
"bar",
);
});
});

describe("triggerImmediately", () => {
it("calls the registered handler", () => {
let handled = 0;
handler("foo", () => handled++);
triggerImmediately("foo");
expect(handled).toBe(1);
});
it("logs a warning for unknown events", () => {
triggerImmediately("bar");
expect(global.console.warn).toHaveBeenCalledWith(
"no handler registered for:",
"bar",
);
});
});
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export {
withInputSignals,
} from "./connector";
export { mount } from "./dom";
export { effect, effector } from "./effect";
export { handler, rawHandler, trigger, triggerImmediately } from "./event";
export { signal, signalFn } from "./signal";

0 comments on commit b70ccb4

Please sign in to comment.