UNPKG

@prelude/channel

Version:

Channel module.

317 lines 8.87 kB
export class AttemptBase { } export class ReadAttempt extends AttemptBase { channel; perform; constructor(channel, perform) { super(); this.channel = channel; this.perform = perform; } } export class WriteAttempt extends AttemptBase { channel; value; perform; constructor(channel, value, perform) { super(); this.channel = channel; this.value = value; this.perform = perform; } } export class Channel { #cap; #doneWriting; #reads; #writes; #doneWritingCallbacks; constructor(cap = 0) { this.#cap = cap; this.#doneWriting = false; this.#reads = []; this.#writes = []; this.#doneWritingCallbacks = []; } get cap() { return this.#cap; } /** @returns number of pending reads. */ get pendingReads() { return this.#reads.length; } /** @returns number of pending writes. */ get pendingWrites() { return this.#writes.length; } [Symbol.asyncIterator]() { return this; } async return(value) { this.close(); return { done: true, value }; } async throw(err) { this.close(err); return { done: true, value: err }; } /** @returns `true` if channels has been closed and there are no pending writes. */ get done() { return this.#doneWriting && this.#writes.length === 0; } /** @returns */ get doneWriting() { return this.#doneWriting; } /** * Closes writing only. * * Writes beyond capacity are settled with optional err. * * If there are no pending writes channel is effectively done - closed for reading and writing. * * @see {@link close} for closing channel for reading and writing. */ closeWriting(err) { if (this.#doneWriting) { throw new Error('Channel already closed for writing.'); } this.#doneWriting = true; while (true) { const cb = this.#doneWritingCallbacks.pop(); if (!cb) { break; } cb.call(this, err); } while (this.#writes.length > this.#cap) { const write = this.#writes.pop(); write.enqueued?.call(this, err); write.written?.call(this, err); } if (this.#writes.length === 0) { while (this.#reads.length > 0) { const read = this.#reads.pop(); read({ done: true, value: undefined }); } } } /** * Closes channel for both reading and writing. * * @see {@link closeWriting} for closing channel for writing only. */ close(err) { this.closeWriting(err); while (true) { const write = this.#writes.pop(); if (!write) { break; } write.enqueued?.call(this, err); write.written?.call(this, err); } while (true) { const read = this.#reads.pop(); if (!read) { break; } read?.call(this, { done: true, value: undefined }); } } /** * Registers callback to be called when channel has done writing. * Callback is called immediatelly if channel is already closed for writing. * @returns undo function that unregisters callback. */ onceDoneWriting(done) { if (this.#doneWriting) { done(); return () => { }; } this.#doneWritingCallbacks.push(done); return () => { const i = this.#doneWritingCallbacks.indexOf(done); if (i === -1) { return; } this.#doneWritingCallbacks.splice(i, 1); }; } next() { return new Promise(resolve => { if (this.done) { resolve({ done: true, value: undefined }); return; } this.#reads.push(resolve); if (this.#writes.length > 0) { this.#consume(); } }); } /** @throws if channel is closed. */ async read() { const result = await this.next(); if (result.done) { throw new Error('Channel closed.'); } return result.value; } async maybeRead() { const result = await this.next(); return result.done ? result.value : undefined; } /** @returns all values that was possible to read immediatelly, aka all pending writes. */ consumeWrites() { const values = []; while (this.#writes.length > 0) { values.push(this.consumeWrite()); } return values; } readAttempt(perform) { return new ReadAttempt(this, perform); } write(value) { return new Promise((resolve, reject) => { if (this.#doneWriting) { reject(new Error('Channel closed.')); return; } if (this.#cap === 0 && this.#reads.length > 0) { this.consumeRead({ value }); resolve(undefined); return; } else if (this.#writes.length < this.#cap) { this.#writes.push({ value }); resolve(undefined); if (this.#reads.length > 0) { this.#consume(); } return; } this.#writes.push({ value, enqueued(err) { if (err) { reject(err); } else { resolve(undefined); } } }); }); } async maybeWrite(value) { return this .write(value) .then(() => true) .catch(() => false); } writeIgnore(value) { this.write(value).catch(() => { }); } writeAttempt(value, perform) { return new WriteAttempt(this, value, perform); } /** * Pushes read to the channel. * @returns undo operation. */ pushRead(read) { if (this.#writes.length > 0) { throw new Error('Expected no writes to push read.'); } this.#reads.push(read); return () => { this.#removeRead(read); }; } /** * Pushes write to the channel. * @returns undo operation. */ pushWrite(write) { if (this.#reads.length > 0) { throw new Error('Expected no reads to push write.'); } this.#writes.push(write); return () => { this.#removeWrite(write); }; } /** * Removes read from channel. * No-op if not found. * @internal */ #removeRead(read) { const i = this.#reads.indexOf(read); if (i === -1) { return; } this.#reads.splice(i, 1); } /** * Removes write from channel. * No-op if write is not found. * @internal */ #removeWrite(write, err) { const i = this.#writes.lastIndexOf(write); if (i === -1) { return; } // TODO: make it optional? what if we want to remove without callbacks in select? if (this.#cap === 0) { this.#writes[i].enqueued?.call(this, err); } this.#writes[i].written?.call(this, err); this.#writes.splice(i, 1); } consumeRead(result) { const read = this.#reads.shift(); if (!read) { throw new Error('Expected read to consume.'); } read(result); } consumeWrite() { const write = this.#writes.shift(); if (!write) { throw new Error('Expected write to consume.'); } if (this.#cap === 0) { write.enqueued?.call(this); } else if (this.#writes.length >= this.#cap) { this.#writes[this.#cap - 1].enqueued?.call(this); } write.written?.call(this); return write.value; } #consume() { if (this.#reads.length === 0) { throw new Error('no reads'); } if (this.#writes.length === 0) { throw new Error('no writes'); } const read = this.#reads.shift(); const write = this.#writes.shift(); if (this.#cap === 0) { write.enqueued?.call(this); write.written?.call(this); } else if (this.#writes.length >= this.#cap) { this.#writes[this.#cap - 1].enqueued?.call(this); } read.call(this, { value: write.value }); } } //# sourceMappingURL=channel.js.map