UNPKG

ws-wrapper

Version:

Lightweight WebSocket wrapper lib with socket.io-like event handling, requests, and channels

169 lines (161 loc) 4.59 kB
import WebSocketChannel from "./channel.mjs" /** * Implements the async iterator protocol on WebSocketChannel for one-way * streaming. The remote peer drives the stream by emitting "next" events on * the channel with `{value, done}` payloads. * * On the first call to `iterator.next()`, a `"start"` event is emitted on the * channel so the remote knows the consumer is ready: * * chan.on("start", () => { * chan.emit("next", { value: 42, done: false }) * chan.emit("next", { value: undefined, done: true }) * }) * * The iterator buffers at most one unconsumed item. If a second "next" event * arrives before the consumer has called `iterator.next()`, the iterator * errors with a buffer overflow, but the channel itself is left open. * * Completing the iterator via normal completion or `iterator.return()` does * NOT close the channel, allowing the same channel to be iterated again. * Each call to `[Symbol.asyncIterator]()` creates a fresh iterator that emits * a new `"start"` event on the first `next()` call. Simultaneous iterators on * the same channel are not supported. * * Calling `iterator.throw(err)` aborts the channel (anonymous channels only), * closing it and signalling the remote end to clean up. * * @returns {AsyncIterator} */ WebSocketChannel.prototype[Symbol.asyncIterator] = function () { const chan = this const { closeSignal } = chan let started = false /** @type {{resolve: Function, reject: Function} | null} */ let pending = null /** @type {{value: unknown, done: boolean} | null} */ let buffer = null /** @type {Error | null} */ let bufferError = null let done = closeSignal ? closeSignal.aborted : false function cleanup() { if (closeSignal) { closeSignal.removeEventListener("abort", onAbort) } chan.removeListener("next", onNext) done = true } function onAbort() { if (done) return cleanup() buffer = null const err = new Error("channel closed before iteration completed") if (pending) { const { reject } = pending pending = null reject(err) } else { bufferError = err } } function onNext(data) { if (done) return const isDone = !!(data && data.done) const value = data && data.value if (pending) { // Resolve promise returned by `next()` const { resolve } = pending pending = null if (isDone) { cleanup() } resolve({ value, done: isDone }) } else if (buffer == null && bufferError == null) { // Buffer for subsequent `next()` call buffer = { value, done: isDone } if (isDone) { cleanup() } } else { // Buffer overflow: consumer is too slow cleanup() buffer = null bufferError = new Error( "async iterator buffer overflow; consumer is too slow" ) } } if (!done) { if (closeSignal) { closeSignal.addEventListener("abort", onAbort, { once: true }) } chan.on("next", onNext) } return { next() { // Handle user calling `next()` before previous Promise resolves if (pending) { return Promise.reject(new Error("cannot call next() concurrently")) } // Emit "start" signal if we haven't already if (!started) { started = true if (!done) chan.emit("start") } // Always empty the bufferError / buffer first if (bufferError) { const err = bufferError bufferError = null return Promise.reject(err) } if (buffer) { const result = buffer buffer = null return Promise.resolve(result) } // Handle `done` case if (done) { return Promise.resolve({ value: undefined, done: true }) } // Return promise that resolves when "next" event is received return new Promise((resolve, reject) => { pending = { resolve, reject } }) }, return(value) { // Always clear stale buffer/error even if already done (e.g. a // terminal item was buffered which set done=true before return() ran). buffer = null bufferError = null if (!done) { cleanup() if (pending) { const { resolve } = pending pending = null resolve({ value, done: true }) } } return Promise.resolve({ value, done: true }) }, throw(err) { // Always clear stale buffer/error even if already done. buffer = null bufferError = null if (!done) { cleanup() if (pending) { const { reject } = pending pending = null reject(err) } // For anonymous channels, abort() sends {h, x} to notify remote. // For non-anonymous channels this is a no-op. chan.abort(err) } return Promise.reject(err) }, [Symbol.asyncIterator]() { return this }, } }