UNPKG

reactive-channel

Version:

A simple yet powerful abstraction that enables communication between asynchronous tasks.

1 lines 16.9 kB
{"version":3,"file":"index.cjs","names":[],"sources":["../src/lib/index.ts"],"sourcesContent":["import {makeCircularQueue, ReadonlyCircularQueue} from 'reactive-circular-queue';\nimport {makeDerivedStore, makeStore, ReadonlyStore} from 'universal-stores';\n\nexport type {ReadonlyCircularQueue} from 'reactive-circular-queue';\nexport {\n\tNotEnoughAvailableSlotsQueueError,\n\tNotEnoughFilledSlotsQueueError,\n} from 'reactive-circular-queue';\nexport type {Unsubscribe, Subscriber, ReadonlyStore} from 'universal-stores';\n\nconst noop = () => undefined as void;\n\n/**\n * Error that occurs when the channel buffer has been filled up, and thus it cannot\n * accept any more `send` calls.\n */\nexport class ChannelFullError extends Error {\n\tconstructor() {\n\t\tsuper('channel full, cannot enqueue data');\n\t}\n}\n\n/**\n * Error that occurs trying to send or receive data from\n * a closed channel.\n */\nexport class ChannelClosedError extends Error {\n\tconstructor() {\n\t\tsuper('channel closed');\n\t}\n}\n\n/**\n * Error that occurs when calling `recv`\n * and there are already too many enqueued similar requests.\n */\nexport class ChannelTooManyPendingRecvError extends Error {\n\tconstructor() {\n\t\tsuper('channel has already too many pending recv');\n\t}\n}\n\n/**\n * Transmission end of a channel.\n */\nexport type ChannelTx<T> = {\n\t/**\n\t * Push data into the channel.\n\t * This operation enqueues the passed value in the transmission queue if there\n\t * is no pending `recv`.\n\t * @param v the data to send.\n\t * @throws {ChannelClosedError} if the channel is closed.\n\t * @throws {ChannelFullError} if the channel is transmission queue is full.\n\t */\n\tsend(v: T): void;\n\t/**\n\t * Push data into the channel and waits for it to be consumed by the receiving end.\n\t * This operation enqueues the passed value in the transmission queue if there\n\t * is no pending `recv`, but removes it if the operation is aborted by an abort\n\t * signal.\n\t * @param v the data to send.\n\t * @param options.signal (optional) an abort signal to stop the pending promise.\n\t * If this signal emits before `sendWait` can resolve, the enqueued value will be removed\n\t * and the emitted value will be \"thrown\" (as in `throw ...;`) to the caller\n\t * of `sendWait`.\n\t * @throws {ChannelClosedError} if the channel is closed.\n\t * @throws {ChannelFullError} if the channel is transmission queue is full.\n\t * @throws {unknown} if `signal` triggers before `sendWait` can resolve.\n\t */\n\tsendWait(v: T, options?: {signal?: AbortSignal}): Promise<void>;\n\t/**\n\t * A store that contains true if the transmission buffer is not full and the channel is not closed.\n\t */\n\tcanWrite$: ReadonlyStore<boolean>;\n\t/**\n\t * A store that contains the number of available slots (from 0 to the channel capacity) in the output buffer or 0 if the channel is closed.\n\t */\n\tavailableOutboxSlots$: ReadonlyStore<number>;\n\t/**\n\t * Return the total size (number of slots) of the channel buffer.\n\t */\n\tget capacity(): number;\n\t/**\n\t * Close the channel, stopping all pending send/recv requests.\n\t */\n\tclose(): void;\n\t/**\n\t * A store that contains true if the channel is closed.\n\t */\n\tclosed$: ReadonlyStore<boolean>;\n};\n\n/**\n * Receiving end of a channel.\n */\nexport type ChannelRx<T> = {\n\t/**\n\t * A store that contains true if there is some data ready to be consumed, the channel is not closed and there are not too many pending `recv` requests.\n\t */\n\tcanRead$: ReadonlyStore<boolean>;\n\t/**\n\t * A store that contains the number of filled slots (from 0 to the channel capacity) in the input buffer or 0 if the channel is closed.\n\t */\n\tfilledInboxSlots$: ReadonlyStore<number>;\n\t/**\n\t * Return the total size (number of slots) of the channel buffer.\n\t */\n\tget capacity(): number;\n\t/**\n\t * Consume data from the channel buffer.\n\t * If there is no data in the channel, this method will block the caller\n\t * until it's available.\n\t * @param options.signal (optional) an abort signal to stop the pending promise.\n\t * If this signal triggers before `recv` can resolve, the channel buffer won't be\n\t * consumed and the abort reason value will be \"thrown\" (as in `throw ...;`) to the caller\n\t * of `recv`.\n\t * @throws {ChannelClosedError} if the channel is closed.\n\t * @throws {unknown} if `.abort(...)` is called before `recv` is able to consume the channel buffer.\n\t */\n\trecv(options?: {signal?: AbortSignal}): Promise<T>;\n\t/**\n\t * Return an async iterator that consumes the channel buffer\n\t * If the channel buffer is already empty the iterator will not emit any value.\n\t */\n\titer(): AsyncIterator<T>;\n\t/**\n\t * Return an async iterator that consumes the channel buffer\n\t * If the channel buffer is already empty the iterator will not emit any value.\n\t */\n\t[Symbol.asyncIterator](): AsyncIterator<T>;\n\t/**\n\t * Close the channel, stopping all pending send/recv requests.\n\t */\n\tclose(): void;\n\t/**\n\t * A store that contains true if the channel is closed.\n\t */\n\tclosed$: ReadonlyStore<boolean>;\n\t/**\n\t * A store that contains the number of currently waiting `recv` promises.\n\t */\n\tpendingRecvPromises$: ReadonlyStore<number>;\n};\n\n/**\n * A Channel is an abstraction that enables\n * communication between asynchronous tasks.\n * A channel exposes two objects: `tx` and `rx`,\n * which respectively provide methods to transmit\n * and receive data.\n *\n * Channels can be used and combined in a multitude of\n * ways. The simplest way to use a channel is by creating\n * a simplex communication: one task transmit data, another consumes it.\n * A full-duplex communication can be achieved by creating two channels\n * and exchanging the `rx` and `tx` objects between two tasks.\n *\n * It's also possible to create a Multiple Producers Single Consumer (mpsc) scenario\n * by sharing a single channel among several tasks.\n */\nexport type Channel<T> = {\n\t/**\n\t * Transmission end of the channel.\n\t */\n\ttx: ChannelTx<T>;\n\t/**\n\t * Receiving end of the channel.\n\t */\n\trx: ChannelRx<T>;\n\t/**\n\t * Return the internal buffer in readonly mode.\n\t */\n\tget buffer(): ReadonlyCircularQueue<T>;\n};\n\nexport type MakeChannelParams = {\n\t/** (optional, defaults to 1024) The maximum number of items that the channel can buffer while waiting data to be consumed. */\n\tcapacity?: number;\n\t/** (optional, defaults to 1024) The maximum number of pending `recv`. If this limit is reached, `recv` will immediately reject with {@link ChannelTooManyPendingRecvError}. */\n\tmaxConcurrentPendingRecv?: number;\n};\n\n/**\n * Create a Channel.\n *\n * A Channel is an abstraction that enables\n * communication between asynchronous tasks.\n * A channel exposes two objects: `tx` and `rx`,\n * which respectively provide methods to transmit\n * and receive data.\n *\n * Channels can be used and combined in a multitude of\n * ways. The simplest way to use a channel is by creating\n * a simplex communication: one task transmit data, another consumes it.\n * A full-duplex communication can be achieved by creating two channels\n * and exchanging the `rx` and `tx` objects between two tasks.\n *\n * It's also possible to create a Multiple Producers Single Consumer (mpsc) scenario\n * by sharing a single channel among several tasks.\n *\n * Example:\n * ```ts\n * const {tx, rx} = makeChannel<number>();\n * rx.recv().then((n) => console.log('Here it is: ' + n)); // doesn't print anything, the channel is currently empty.\n * tx.send(1); // resolves the above promise, causing it to print 'Here it is: 1'\n * ```\n *\n * @param params (optional) configuration parameters for this channel (e.g maximum capacity).\n * @param params.capacity (optional, defaults to 1024) The maximum number of items that the channel can buffer while waiting data to be consumed.\n * @param params.maxConcurrentPendingRecv (optional, defaults to 1024) The maximum number of pending `recv`. If this limit is reached, `recv` will immediately reject with {@link ChannelTooManyPendingRecvError}.\n * @returns a {@link Channel}\n */\nexport function makeChannel<T>(params?: MakeChannelParams): Channel<T> {\n\tconst {capacity = 1024, maxConcurrentPendingRecv = 1024} = params || {};\n\n\ttype BufferItemMetadata = {\n\t\tpromise: Promise<void>;\n\t\tresolveSend: () => void;\n\t\trejectSend: (err?: unknown) => void;\n\t};\n\ttype BufferItem = BufferItemMetadata & {\n\t\tvalue: T;\n\t};\n\ttype RecvQueueItem = {\n\t\tresolveRecv: (item: BufferItem) => void;\n\t\trejectRecv: (err?: unknown) => void;\n\t};\n\tconst metadataQueue = makeCircularQueue<BufferItemMetadata>(capacity);\n\tconst itemsQueue = makeCircularQueue<T>(capacity);\n\tconst recvQueue = makeCircularQueue<RecvQueueItem>(maxConcurrentPendingRecv);\n\n\tconst closed$ = makeStore(false);\n\tconst availableOutboxSlots$ = makeDerivedStore(\n\t\t[closed$, metadataQueue.availableSlots$],\n\t\t([closed, availableSlots]) => (closed ? 0 : availableSlots),\n\t);\n\tconst filledInboxSlots$ = makeDerivedStore(\n\t\t[closed$, metadataQueue.filledSlots$],\n\t\t([closed, filledSlots]) => (closed ? 0 : filledSlots),\n\t);\n\tconst canWrite$ = makeDerivedStore(\n\t\tavailableOutboxSlots$,\n\t\t(availableOutboxSlots) => availableOutboxSlots > 0,\n\t);\n\tconst canRead$ = makeDerivedStore(\n\t\t[filledInboxSlots$, recvQueue.full$],\n\t\t([filledInboxSlots, recvFull]) => filledInboxSlots > 0 && !recvFull,\n\t);\n\n\tasync function sendWait(v: T, options?: {signal?: AbortSignal}): Promise<void> {\n\t\tif (closed$.content()) {\n\t\t\tthrow new ChannelClosedError();\n\t\t}\n\t\tif (metadataQueue.full$.content()) {\n\t\t\tthrow new ChannelFullError();\n\t\t}\n\t\tlet resolveSend: () => void = noop;\n\t\tlet rejectSend: () => void = noop;\n\t\tconst promise = new Promise<void>((res, rej) => {\n\t\t\tresolveSend = res;\n\t\t\trejectSend = rej;\n\t\t});\n\t\tlet metadataItem: BufferItemMetadata | undefined;\n\t\tif (!recvQueue.empty$.content()) {\n\t\t\trecvQueue.dequeue().resolveRecv({\n\t\t\t\tpromise,\n\t\t\t\tresolveSend,\n\t\t\t\trejectSend,\n\t\t\t\tvalue: v,\n\t\t\t});\n\t\t} else {\n\t\t\tmetadataItem = {\n\t\t\t\tpromise,\n\t\t\t\tresolveSend,\n\t\t\t\trejectSend,\n\t\t\t};\n\t\t\tmetadataQueue.enqueue(metadataItem);\n\t\t\titemsQueue.enqueue(v);\n\t\t}\n\t\ttry {\n\t\t\tif (!options?.signal) {\n\t\t\t\tawait promise;\n\t\t\t} else {\n\t\t\t\tconst signal = options.signal;\n\t\t\t\tsignal.throwIfAborted();\n\t\t\t\tawait Promise.race([\n\t\t\t\t\tpromise,\n\t\t\t\t\tnew Promise<void>((_, rej) => {\n\t\t\t\t\t\tsignal.addEventListener('abort', () => {\n\t\t\t\t\t\t\t// Postpone the rejection by one \"tick\" to\n\t\t\t\t\t\t\t// make the fulfillment of the above promise\n\t\t\t\t\t\t\t// have priority over the rejection caused by the signal.\n\t\t\t\t\t\t\tPromise.resolve()\n\t\t\t\t\t\t\t\t.then(() => rej(signal.reason))\n\t\t\t\t\t\t\t\t.catch(noop);\n\t\t\t\t\t\t});\n\t\t\t\t\t}),\n\t\t\t\t]);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (metadataItem) {\n\t\t\t\tconst metadataItemIndex = metadataQueue.indexOf(metadataItem);\n\t\t\t\tif (metadataItemIndex !== -1) {\n\t\t\t\t\tmetadataQueue.remove(metadataItemIndex);\n\t\t\t\t\titemsQueue.remove(metadataItemIndex);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow err;\n\t\t}\n\t}\n\n\tfunction send(v: T): void {\n\t\tif (closed$.content()) {\n\t\t\tthrow new ChannelClosedError();\n\t\t}\n\t\tif (metadataQueue.full$.content()) {\n\t\t\tthrow new ChannelFullError();\n\t\t}\n\t\tsendWait(v).catch(noop);\n\t}\n\n\tasync function recv(options?: {signal?: AbortSignal}) {\n\t\tif (closed$.content()) {\n\t\t\tthrow new ChannelClosedError();\n\t\t}\n\t\tlet item: BufferItem;\n\t\tif (!metadataQueue.empty$.content()) {\n\t\t\titem = {...metadataQueue.dequeue(), value: itemsQueue.dequeue()};\n\t\t} else {\n\t\t\tif (recvQueue.full$.content()) {\n\t\t\t\tthrow new ChannelTooManyPendingRecvError();\n\t\t\t}\n\t\t\tconst recvContext: RecvQueueItem = {\n\t\t\t\tresolveRecv: noop,\n\t\t\t\trejectRecv: noop,\n\t\t\t};\n\t\t\tconst recvPromise = new Promise<BufferItem>((res, rej) => {\n\t\t\t\trecvContext.resolveRecv = res;\n\t\t\t\trecvContext.rejectRecv = rej;\n\t\t\t\trecvQueue.enqueue(recvContext);\n\t\t\t});\n\n\t\t\ttry {\n\t\t\t\tif (!options?.signal) {\n\t\t\t\t\titem = await recvPromise;\n\t\t\t\t} else {\n\t\t\t\t\tconst signal = options.signal;\n\t\t\t\t\tsignal.throwIfAborted();\n\t\t\t\t\titem = await Promise.race([\n\t\t\t\t\t\trecvPromise,\n\t\t\t\t\t\tnew Promise<BufferItem>((_, rej) => {\n\t\t\t\t\t\t\tsignal.addEventListener('abort', () => {\n\t\t\t\t\t\t\t\t// Postpone the rejection by one \"tick\" to\n\t\t\t\t\t\t\t\t// make the fulfillment of the above promise\n\t\t\t\t\t\t\t\t// have priority over the rejection caused by the signal.\n\t\t\t\t\t\t\t\tPromise.resolve()\n\t\t\t\t\t\t\t\t\t.then(() => rej(signal.reason))\n\t\t\t\t\t\t\t\t\t.catch(noop);\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}),\n\t\t\t\t\t]);\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tconst recvContextIndex = recvQueue.indexOf(recvContext);\n\t\t\t\tif (recvContextIndex !== -1) {\n\t\t\t\t\trecvQueue.remove(recvContextIndex);\n\t\t\t\t}\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t}\n\t\titem.resolveSend();\n\t\treturn item.value;\n\t}\n\n\tasync function* iter() {\n\t\twhile (metadataQueue.filledSlots$.content() > 0) {\n\t\t\tyield await recv();\n\t\t}\n\t}\n\n\tfunction close() {\n\t\tif (closed$.content()) {\n\t\t\treturn;\n\t\t}\n\t\tclosed$.set(true);\n\t\tconst channelClosedError = new ChannelClosedError();\n\t\tfor (const pendingRecv of recvQueue) {\n\t\t\tpendingRecv.rejectRecv(channelClosedError);\n\t\t}\n\t\tfor (const item of metadataQueue) {\n\t\t\titem.rejectSend(channelClosedError);\n\t\t}\n\t\titemsQueue.clear();\n\t}\n\n\treturn {\n\t\tbuffer: itemsQueue,\n\t\ttx: {\n\t\t\tsend,\n\t\t\tsendWait,\n\t\t\tcanWrite$,\n\t\t\tclosed$,\n\t\t\tclose,\n\t\t\tavailableOutboxSlots$,\n\t\t\tcapacity,\n\t\t},\n\t\trx: {\n\t\t\tpendingRecvPromises$: recvQueue.filledSlots$,\n\t\t\trecv,\n\t\t\titer,\n\t\t\t[Symbol.asyncIterator]: iter,\n\t\t\tcanRead$: canRead$,\n\t\t\tclosed$,\n\t\t\tclose,\n\t\t\tcapacity,\n\t\t\tfilledInboxSlots$,\n\t\t},\n\t};\n}\n"],"mappings":"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"}