@fine-js/channels
Version:
Bits of Clojure's `core.async` ported to JS
151 lines (122 loc) • 4.09 kB
JavaScript
const chan = require('./chan')
const {privates, badval, valueError, once} = require('./misc')
const {floor, random} = Math
const put = (ch, val) => ch.put(val)
const take = (ch) => ch.take()
const close = (ch) => ch.close()
const timeout = (ms = 0) => {
const ch = chan()
setTimeout(close, ms, ch).unref()
return ch
}
const ready = (port) => {
return port instanceof Array
? port[0][privates].putReady()
: port[privates].takeReady()
}
const unwrapChannel = (port) => {
return port instanceof Array
? port[0]
: port
}
const execPort = async (port) => {
const putting = port instanceof Array
const ch = unwrapChannel(port)
const func = putting ? ch.put : ch.take
const args = putting ? [port[1]] : []
return [await func(...args), ch]
}
const randomInt = (lower, upper) => lower + floor(random() * (upper - lower + 1))
const randomItem = (array) => array[randomInt(0, array.length - 1)]
const castArray = (val) => { return val instanceof Array ? val : [val] }
const pairs = (arr) => arr.reduce(([odd, even], item, idx) => {
(idx % 2 === 0 ? odd : even).push(item)
return [odd, even]
}, [[], []])
const alt = async (bindings, options = {}) => {
// Create of port descriptions to expression, with path:
// channel -> ch.take -> null -> expression
// channel -> ch.put -> null -> expression
// So we know what to run when alts() returns.
const [operations, expressions] = pairs(bindings)
const channelToExpr = new Map()
const ports = operations.reduce((acc, operation, idx) => {
const opPorts = castArray(operation)
for (const port of opPorts) {
const ch = unwrapChannel(port)
if (channelToExpr.has(ch))
throw new TypeError('each channel may only appear once')
channelToExpr.set(ch, expressions[idx])
}
return acc.concat(opPorts)
}, [])
const result = await alts(ports, options)
const toRun = result[1] === alt.default
? options.default
: channelToExpr.get(result[1])
return toRun instanceof Function
? toRun(...result)
: toRun
}
const alts = (ports, options = {}) => {
const bad = ports.find((port) => port instanceof Array && badval(port[1]))
if (bad)
return Promise.reject(valueError(bad[1]))
// First we try to execute any of the available ports.
if (ports.some(ready))
return execPort(options.priority ? ports.find(ready) : randomItem(ports.filter(ready)))
// Otherwise, when passed, we must return default value.
if (Object.hasOwnProperty.call(options, 'default'))
return Promise.resolve([options.default, alts.default])
// None of the above? Then we must execute the first port to become ready.
// We subscribe to all the channels activity updates, and try executing ports on activity.
// (This is not good way of detecting port becoming available, but hey… version 0.1)
return new Promise((resolve) => {
const operations = new Map()
const subscriptions = new Map()
const done = once(resolve, true)
const unsubscribeAll = () => {
for (const [channel, token] of subscriptions)
channel[privates].unsubscribe(token)
}
// Check channel's port for being ready. Do nothing, when it's not.
// Remove all subscriptions and exec port otherwise.
const onActivity = (channel) => {
const port = operations.get(channel)
if (!ready(port))
return
unsubscribeAll()
execPort(port).then(done)
}
for (const port of ports) {
const channel = unwrapChannel(port)
operations.set(channel, port)
subscriptions.set(channel, channel[privates].subscribe(onActivity))
}
})
}
alts.default = Symbol('channels/alts-default')
alt.default = alts.default
const poll = (ch) => {
return ch[privates].takeReady()
? ch.take()
: Promise.resolve(null)
}
const offer = (ch, val) => {
if (badval(val))
return Promise.reject(valueError(val))
return ch[privates].putReady()
? ch.put(val)
: Promise.resolve(false)
}
module.exports = {
put,
take,
close,
alt,
alts,
poll,
offer,
timeout,
}