Skip to content

Instantly share code, notes, and snippets.

@Heimdell
Last active December 15, 2018 19:28
Show Gist options
  • Save Heimdell/c39f202dacbfd7b1ec966e474763d105 to your computer and use it in GitHub Desktop.
Save Heimdell/c39f202dacbfd7b1ec966e474763d105 to your computer and use it in GitHub Desktop.
/*
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