-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
365 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
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
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,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(); | ||
} |
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,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", | ||
); | ||
}); | ||
}); |
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,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); | ||
} |
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,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", | ||
); | ||
}); | ||
}); |
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