UNPKG

@fine-js/channels

Version:

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

174 lines (145 loc) 4.2 kB
'use strict' const {buffer, unbuffered} = require('./buffers') const {badval, valueError, once, privates, schedule} = require('./misc') const {max} = Math const nextTicker = (() => { let now = 0 return () => { return ++now === Number.MAX_SAFE_INTEGER ? (now = 0, 1) : now } })() const makeBuffer = (arg) => { return typeof arg === 'number' ? arg === 0 ? unbuffered() : buffer(arg) : arg } const chan = (bufferOrMaxCapacity = unbuffered()) => { // eslint-disable-line max-lines-per-function const buf = makeBuffer(bufferOrMaxCapacity) const takes = [] const puts = [] let closed = false const drained = () => puts.length === 0 && buf.load() === 0 const queued = () => buf.load() + puts.length const unclaimed = () => max(0, queued() - takes.length) const takeReady = () => closed || unclaimed() > 0 const putReady = () => closed || !buf.blocking || unclaimed() < buf.capacity || queued() < takes.length const toConsumer = (val) => { if (takes.length === 0) return false schedule(takes.shift(), val) return true } const toBuffer = (val) => { if (!buf.writable()) return false buf.write(val) return true } const tryConsumingFromBuffer = () => { if (buf.load() > 0 && toConsumer(buf.peek())) { buf.read() return true } return false } const tryConsumingFromPutsQueue = () => { if (puts.length === 0) return false const [val, consumed] = puts[0] if (toConsumer(val) || toBuffer(val)) { puts.shift() schedule(consumed, true) return true } return false } const tryConsuming = () => { if (tryConsumingFromBuffer()) { schedule(tryConsuming) } else if (tryConsumingFromPutsQueue()) { schedule(tryConsuming) } else if (closed && drained()) { takes.forEach((taker) => schedule(taker, null)) takes.splice(0, takes.length) } } const put = (value) => new Promise((resolve, reject) => { if (badval(value)) return void reject(valueError(value)) if (closed) return void resolve(false) puts.push([value, once(resolve, true)]) schedule(tryConsuming) notifySubscribers() }) const take = () => new Promise((resolve) => { if (closed && drained()) return void resolve(null) takes.push(once(resolve, true)) schedule(tryConsuming) notifySubscribers() }) // Notifying all subscribers, put and take ones, used to look a little wasteful, // but it might not be a big deal for reduction in complexity it provides. // Needs a benchmark. // // Plus, it might be contributing to the accidental ordering of alts()s // in the same queue as put()s and take()s. Let's have another look at some point. const subscriptions = new Map() const notifySubscribers = () => { // Even though subscribers we are notifying can change the Map while iterating, // it does not seem like a big problem since they can only unsubscribe themselves // from this specific channel. And we have a test of JS maps for that. for (const notify of subscriptions.values()) notify(channel) } const unsubscribe = Map.prototype.delete.bind(subscriptions) const subscribe = (notify) => { const token = Symbol('channels/activity-subscription-token') subscriptions.set(token, notify) return token } const channel = { put, take, close: once(() => { closed = true notifySubscribers() schedule(tryConsuming) }), async* [Symbol.asyncIterator] () { while (true) { const val = await take() // eslint-disable-line no-await-in-loop if (val === null) break yield val } }, [privates]: { takeReady, putReady, subscribe, unsubscribe, status: (at = [Date.now(), nextTicker()]) => ({ at, takes: takes.length, puts: puts.length, closed, drained: drained(), unclaimed: unclaimed(), takeReady: takeReady(), putReady: putReady(), buffer: buf.status(), }), }, } return channel } module.exports = chan