@fine-js/channels
Version:
Bits of Clojure's `core.async` ported to JS
174 lines (145 loc) • 4.2 kB
JavaScript
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