UNPKG

@fine-js/channels

Version:

Bits of Clojure's `core.async` ported to JS

151 lines (122 loc) 4.09 kB
'use strict' 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, }