UNPKG

@prelude/channel

Version:

Channel module.

380 lines (337 loc) 8.53 kB
export type Thunk<R = void> = () => R export type Undo = Thunk<void> export type Done = (err?: unknown) => void export type Read<T> = (result: IteratorResult<T>) => void export type Write<T> = { value: T enqueued?: Done written?: Done } export class AttemptBase { } export class ReadAttempt<T, R> extends AttemptBase { constructor( public readonly channel: Channel<T>, public readonly perform: (result: IteratorResult<T>) => IteratorResult<R> ) { super() } } export class WriteAttempt<T, R> extends AttemptBase { constructor( public readonly channel: Channel<T>, public readonly value: T, public readonly perform: (value: T) => IteratorResult<R> ) { super() } } export type Attempt = | Channel<any> | ReadAttempt<any, any> | WriteAttempt<any, any> export type Attempted<A extends Attempt> = A extends Channel<infer T> ? T : A extends ReadAttempt<any, infer R> ? R : A extends WriteAttempt<any, infer R> ? R : never export class Channel<T> implements AsyncIterableIterator<T> { #cap: number #doneWriting: boolean #reads: Read<T>[] #writes: Write<T>[] #doneWritingCallbacks: Done[] 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?: any) { this.close() return { done: true, value } } async throw(err?: any) { 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?: unknown) { 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() as Write<T> 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() as Read<T> read({ done: true, value: undefined }) } } } /** * Closes channel for both reading and writing. * * @see {@link closeWriting} for closing channel for writing only. */ close(err?: unknown) { 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: Done): Undo { 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(): Promise<IteratorResult<T>> { 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(): Promise<T> { const result = await this.next() if (result.done) { throw new Error('Channel closed.') } return result.value } async maybeRead(): Promise<undefined | T> { 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(): T[] { const values: T[] = [] while (this.#writes.length > 0) { values.push(this.consumeWrite()) } return values } readAttempt<R>(perform: (result: IteratorResult<T>) => IteratorResult<R>) { return new ReadAttempt(this, perform) } write(value: T) { return new Promise<void>((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: unknown) { if (err) { reject(err) } else { resolve(undefined) } } }) }) } async maybeWrite(value: T) { return this .write(value) .then(() => true) .catch(() => false) } writeIgnore(value: T) { this.write(value).catch(() => {}) } writeAttempt<R>(value: T, perform: (value: T) => IteratorResult<R>) { return new WriteAttempt(this, value, perform) } /** * Pushes read to the channel. * @returns undo operation. */ pushRead(read: Read<T>): Undo { 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: Write<T>): Undo { 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: Read<T>) { 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: Write<T>, err?: unknown) { 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: IteratorResult<T>) { 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() as Read<T> const write = this.#writes.shift() as Write<T> 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 }) } }