@prelude/channel
Version:
Channel module.
317 lines • 8.87 kB
JavaScript
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