Last active
December 15, 2018 19:28
-
-
Save Heimdell/c39f202dacbfd7b1ec966e474763d105 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
/* | |
Array is too dumb. | |
*/ | |
class Subscribers { | |
constructor () { | |
this.counter = 0 | |
this.mapping = new Map() | |
} | |
add(sink) { | |
let index = this.counter++ | |
this.mapping.set(index, sink) | |
return index | |
} | |
feed(x) { | |
this.mapping.forEach(sink => sink(x)) | |
} | |
} | |
/* | |
Event is a dispatcher. If its fed a thing, it retranslates it to all subscribers via connectors. | |
Connector is a function that receives a thing and decides if it goes to the sinks. | |
(See "resolve" in #transform() method) | |
If you want some impure output from event, just call event#subscribe(callback). | |
You can make some basic source events using | |
- Event.timer(dt) -- sends Date.now() each "dt" | |
- Event.clicks() -- document.onclick | |
- Event.keyboard() -- document.onkeydown | |
Then, you can transform these new-made events using: | |
- event#map(f) -- applies function "f" to each thing coming through | |
- event#filter(pred) -- only routes a thing to subscribers if it makes a "pred" succeed | |
- event#reduce(acc, add) -- "add"s each thing coming in to "acc"umulator (which goes out) | |
There also are high-level methods: | |
- event#count() -- event that emits count of times "event" emitted anything | |
-- (completely ignores what event emits) | |
- event.throttle(dt) -- throws out emitted thing, if less than "dt" time passed | |
- event#delay(dt) -- looks as "event", but all things come out after "dt" ms | |
- event#case(split) -- see below, in the example section | |
- Event.merge(...events) -- If any of the events fires, then merge-event fires, too. | |
*/ | |
class Event { | |
constructor () { | |
this.subscribers = new Subscribers() | |
} | |
/* | |
Create an event and a feeder-function to actually pass things through. | |
*/ | |
static source() { | |
let event = new Event() | |
let feed = x => event.subscribers.feed(x) | |
return {event, feed} | |
} | |
/* | |
Create an event, pass its feeder to "init"ialiser function, then return the new event. | |
*/ | |
static setup(init) { | |
let {event, feed} = Event.source() | |
init(feed) | |
return event | |
} | |
/* | |
Add function to receive things coming through. | |
*/ | |
subscribe(sink) { | |
this.subscribers.add(sink) | |
return this | |
} | |
/* | |
Pattern from map/filter/reduce. | |
The "resolve" function argument decides if "x" ever goes to "sink" | |
and what happens to it before. | |
*/ | |
transform(resolve) { | |
return Event.setup(sink => this.subscribe(x => resolve(sink, x))) | |
} | |
/* | |
Create event that is fed by some event on source object. | |
*/ | |
static globalEvent(eventName, source) { | |
return Event.setup(sink => source[eventName] = sink) | |
} | |
/* | |
Create an event that is fed by timer. | |
*/ | |
static timer(dt) { | |
return Event.setup(sink => setInterval(() => sink(Date.now()), dt)) | |
} | |
/* | |
Some pretty standard events. | |
*/ | |
static clicks (source) { return Event.globalEvent("onclick", source) } | |
static keyboard(source) { return Event.globalEvent("onkeydown", source) } | |
/* | |
Basic transformers. | |
*/ | |
map (f) { return this.transform((sink, x) => sink(f(x))) } | |
filter(pred) { return this.transform((sink, x) => pred(x) ? sink(x) : 0) } | |
/* | |
If a thing comes in, add it to accumulator and give accumulator out. | |
*/ | |
reduce(seed, add) { return this.transform((sink, x) => sink(seed = add(seed, x))) } | |
replaceEach(x) { | |
return this.map(_ => x) | |
} | |
/* | |
Turn things coming through into ones, then add them. | |
*/ | |
count() { | |
return this.map(_ => 1).reduce(0, (x, y) => x + y) | |
} | |
/* | |
Make an event that is fed by all "events". | |
*/ | |
static merge(...nodes) { | |
return Event.setup(sink => | |
nodes.forEach(node => node.subscribe(sink)) | |
) | |
} | |
/* | |
Given an object with predicates, return an object of events. | |
See example. | |
*/ | |
case(split) { | |
return Object | |
.keys(split) | |
.map(key => ({[key]: this.filter(split[key])})) | |
.reduce(Object.assign, {}) | |
} | |
/* | |
Limit rate of events to 1 event per "dt" ms. | |
*/ | |
throttle(dt) { | |
let last = -Infinity | |
return this.filter(x => { | |
let t = Date.now() | |
if (t - last > dt) { | |
last = t | |
return true | |
} | |
}) | |
} | |
/* | |
Delay events by "dt" ms. | |
*/ | |
delay(dt) { | |
return Event.setup(sink => | |
this.subscribe(x => setTimeout(() => sink(x), dt)) | |
) | |
} | |
log(msg) { | |
return this.subscribe(x => console.log({[msg]: x})) | |
} | |
} | |
/* | |
A "last value" of event. | |
*/ | |
class Behaviour { | |
static of(event, start) { | |
return new Behaviour(event, start) | |
} | |
/* | |
Make that each incoming event sets a value. | |
*/ | |
constructor (event, start) { | |
this.value = start | |
event.subscribe(x => this.value = x) | |
} | |
/* | |
When an event arrives, ignore it and return current value. | |
*/ | |
sample(event) { | |
event.map(_ => this.value) | |
} | |
combine(event, f) { | |
event.map(x => f(x, this.value)) | |
} | |
} | |
// helper function | |
let is = x => y => x == y | |
// Parse out only WASD keys | |
let wasd = Event.keyboard(window) | |
.map (e => e.key) | |
.filter(key => "wasd".includes(key)) | |
// Split them onto W, A, S and D key events | |
let {w, a, s, d} = wasd.case({ | |
w: is('w'), | |
a: is('a'), | |
s: is('s'), | |
d: is('d') | |
}) | |
// Replace keys with concrete moves | |
let w1 = w.replaceEach({dx: 1, dy: 0}) | |
let a1 = a.replaceEach({dx: 0, dy: 1}) | |
let s1 = s.replaceEach({dx: -1, dy: 0}) | |
let d1 = d.replaceEach({dx: 0, dy: -1}) | |
// Merge them into move stream | |
// (You can add log to any point) | |
let moves = Event.merge(w1, a1, s1, d1).log('move') | |
// Starting with (0, 0), add moves to current coordinate | |
let place = moves.reduce({x: 0, y: 0}, ({x, y}, {dx, dy}) => ({x: x + dx, y: y + dy})) | |
// Turn current coordinate into a String | |
let shown = place.map(JSON.stringify) | |
// Noitfy if we found something | |
let foundSomething = place.filter(({x, y}) => x == 5 && y == 3) | |
// These events will be looged in console. | |
shown.log("we are at") | |
foundSomething.log("found something") | |
Event.clicks(window) | |
.map( e => ({x: e.screenX, y: e.screenY})) | |
.filter(({x, y}) => x > 100 && x < 300 && y > 100 && y < 300) | |
.map( e => JSON.stringify(e)) | |
.log("found me!") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment