multithreading
Version:
The missing standard library for multithreading in JavaScript (Works in the browser, Node.js, Deno, Bun).
386 lines (385 loc) • 13.8 kB
JavaScript
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
if (value !== null && value !== void 0) {
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
var dispose, inner;
if (async) {
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
dispose = value[Symbol.asyncDispose];
}
if (dispose === void 0) {
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
dispose = value[Symbol.dispose];
if (async) inner = dispose;
}
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
env.stack.push({ value: value, dispose: dispose, async: async });
}
else if (async) {
env.stack.push({ async: true });
}
return value;
};
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
return function (env) {
function fail(e) {
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
env.hasError = true;
}
var r, s = 0;
function next() {
while (r = env.stack.pop()) {
try {
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
if (r.dispose) {
var result = r.dispose.call(r.value);
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
}
else s |= 1;
}
catch (e) {
fail(e);
}
}
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
if (env.hasError) throw env.error;
}
return next();
};
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
var e = new Error(message);
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
});
var _a, _b, _c;
import { deserialize, register, Serializable, serialize, toDeserialized, toSerialized, } from "../shared.js";
import { INTERNAL_SEMAPHORE_CONTROLLER, Semaphore } from "./semaphore.js";
import { SharedJsonBuffer } from "../json_buffer.js";
const IDX_HEAD = 0;
const IDX_TAIL = 1;
const IDX_CLOSED = 2;
const IDX_CAP = 3;
const IDX_TX_COUNT = 4;
const IDX_RX_COUNT = 5;
const META_SIZE = 6;
const OPEN = 0;
const CLOSED = 1;
const ERR_DISPOSED_SENDER = new Error("Sender is disposed");
const ERR_DISPOSED_RECEIVER = new Error("Receiver disposed");
const ERR_CLOSED = new Error("Channel closed");
const ERR_CLOSED_NO_RX = new Error("Channel closed (No Receivers)");
const ERR_SPURIOUS = new Error("Spurious wakeup or illegal null value");
class ChannelInternals extends Serializable {
constructor(state, items, sendLock, recvLock, itemsAvailable, slotsAvailable) {
super();
this.state = state;
this.items = items;
this.sendLock = sendLock;
this.recvLock = recvLock;
this.itemsAvailable = itemsAvailable;
this.slotsAvailable = slotsAvailable;
}
write(value) {
const tail = this.state[IDX_TAIL];
this.items[tail] = value;
this.state[IDX_TAIL] = (tail + 1) % this.state[IDX_CAP];
}
read() {
const head = this.state[IDX_HEAD];
const val = this.items[head];
// Optimistic read check
if (val === null)
return null;
this.items[head] = null;
this.state[IDX_HEAD] = (head + 1) % this.state[IDX_CAP];
return val;
}
isClosed() {
return Atomics.load(this.state, IDX_CLOSED) === CLOSED;
}
hasReceivers() {
return Atomics.load(this.state, IDX_RX_COUNT) > 0;
}
[toSerialized]() {
const itemsSer = serialize(this.items);
const sendLockSer = serialize(this.sendLock);
const recvLockSer = serialize(this.recvLock);
const itemsAvailSer = serialize(this.itemsAvailable);
const slotsAvailSer = serialize(this.slotsAvailable);
return [
[
this.state.buffer,
itemsSer[0],
sendLockSer[0],
recvLockSer[0],
itemsAvailSer[0],
slotsAvailSer[0],
],
itemsSer[1].concat(sendLockSer[1], recvLockSer[1], itemsAvailSer[1], slotsAvailSer[1]),
];
}
static [toDeserialized](data) {
return new _a(new Int32Array(data[0]), deserialize(data[1]), deserialize(data[2]), deserialize(data[3]), deserialize(data[4]), deserialize(data[5]));
}
}
_a = ChannelInternals;
(() => {
register(4, _a);
})();
class ChannelHandle extends Serializable {
constructor(internals) {
super();
this.internals = internals;
this.disposed = false;
}
checkDisposed() {
return this.disposed ? { ok: false, error: this.disposeError } : null;
}
[toSerialized]() {
if (this.disposed)
throw new Error("Cannot move a disposed Handle");
this.disposed = true; // Ownership transfer
return serialize(this.internals);
}
}
export class Sender extends ChannelHandle {
get disposeError() {
return ERR_DISPOSED_SENDER;
}
clone() {
if (this.disposed)
throw new Error("Cannot clone disposed Sender");
Atomics.add(this.internals.state, IDX_TX_COUNT, 1);
return new _b(this.internals);
}
async send(value) {
const disposedCheck = this.checkDisposed();
if (disposedCheck)
return disposedCheck;
if (!this.internals.hasReceivers()) {
return { ok: false, error: ERR_CLOSED_NO_RX };
}
const slotToken = await this.internals.slotsAvailable.acquire();
// Check closed after acquiring slot (race condition check)
if (this.internals.isClosed()) {
slotToken[Symbol.dispose]();
return { ok: false, error: ERR_CLOSED };
}
try {
const env_1 = { stack: [], error: void 0, hasError: false };
try {
const _lockGuard = __addDisposableResource(env_1, await this.internals.sendLock.acquire(), false);
if (this.internals.isClosed()) {
slotToken[Symbol.dispose]();
return { ok: false, error: ERR_CLOSED };
}
this.internals.write(value);
}
catch (e_1) {
env_1.error = e_1;
env_1.hasError = true;
}
finally {
__disposeResources(env_1);
}
}
catch (err) {
slotToken[Symbol.dispose]();
throw err;
}
// Handover: Slot token consumed -> Item token released
this.internals.itemsAvailable[INTERNAL_SEMAPHORE_CONTROLLER].release(1);
return { ok: true, value: undefined };
}
blockingSend(value) {
const disposedCheck = this.checkDisposed();
if (disposedCheck)
return disposedCheck;
if (!this.internals.hasReceivers()) {
return { ok: false, error: ERR_CLOSED_NO_RX };
}
const slotToken = this.internals.slotsAvailable.blockingAcquire();
if (this.internals.isClosed()) {
slotToken[Symbol.dispose]();
return { ok: false, error: ERR_CLOSED };
}
try {
const lockToken = this.internals.sendLock.blockingAcquire();
try {
if (this.internals.isClosed()) {
slotToken[Symbol.dispose]();
return { ok: false, error: ERR_CLOSED };
}
this.internals.write(value);
}
finally {
lockToken[Symbol.dispose]();
}
this.internals.itemsAvailable[INTERNAL_SEMAPHORE_CONTROLLER].release(1);
return { ok: true, value: undefined };
}
catch (err) {
slotToken[Symbol.dispose]();
throw err;
}
}
close() {
if (this.disposed || this.internals.isClosed())
return;
const { state, slotsAvailable, itemsAvailable, sendLock, recvLock } = this.internals;
const g1 = sendLock.blockingAcquire();
const g2 = recvLock.blockingAcquire();
try {
if (this.internals.isClosed())
return;
Atomics.store(state, IDX_CLOSED, CLOSED);
// Wake up everyone
slotsAvailable[INTERNAL_SEMAPHORE_CONTROLLER].release(1073741823);
itemsAvailable[INTERNAL_SEMAPHORE_CONTROLLER].release(1073741823);
}
finally {
g1[Symbol.dispose]();
g2[Symbol.dispose]();
}
}
[Symbol.dispose]() {
if (this.disposed)
return;
const prevCount = Atomics.sub(this.internals.state, IDX_TX_COUNT, 1);
if (prevCount === 1)
this.close();
this.disposed = true;
}
static [toDeserialized](obj) {
return new _b(deserialize(obj));
}
}
_b = Sender;
(() => {
register(5, _b);
})();
export class Receiver extends ChannelHandle {
get disposeError() {
return ERR_DISPOSED_RECEIVER;
}
clone() {
if (this.disposed)
throw new Error("Cannot clone disposed Receiver");
Atomics.add(this.internals.state, IDX_RX_COUNT, 1);
return new _c(this.internals);
}
async recv() {
const disposedCheck = this.checkDisposed();
if (disposedCheck)
return disposedCheck;
const itemToken = await this.internals.itemsAvailable.acquire();
let val;
try {
const env_2 = { stack: [], error: void 0, hasError: false };
try {
const _lockGuard = __addDisposableResource(env_2, await this.internals.recvLock.acquire(), false);
val = this.internals.read();
}
catch (e_2) {
env_2.error = e_2;
env_2.hasError = true;
}
finally {
__disposeResources(env_2);
}
}
catch (err) {
itemToken[Symbol.dispose]();
throw err;
}
// Verify read
if (val === null) {
itemToken[Symbol.dispose]();
return this.internals.isClosed()
? { ok: false, error: ERR_CLOSED }
: { ok: false, error: ERR_SPURIOUS };
}
// Handover: Item token consumed -> Slot token released
this.internals.slotsAvailable[INTERNAL_SEMAPHORE_CONTROLLER].release(1);
return { ok: true, value: val };
}
blockingRecv() {
const disposedCheck = this.checkDisposed();
if (disposedCheck)
return disposedCheck;
const itemToken = this.internals.itemsAvailable.blockingAcquire();
let val;
try {
const lockToken = this.internals.recvLock.blockingAcquire();
try {
val = this.internals.read();
}
finally {
lockToken[Symbol.dispose]();
}
}
catch (err) {
itemToken[Symbol.dispose]();
throw err;
}
if (val === null) {
itemToken[Symbol.dispose]();
return this.internals.isClosed()
? { ok: false, error: ERR_CLOSED }
: { ok: false, error: ERR_SPURIOUS };
}
this.internals.slotsAvailable[INTERNAL_SEMAPHORE_CONTROLLER].release(1);
return { ok: true, value: val };
}
async *[Symbol.asyncIterator]() {
while (true) {
const result = await this.recv();
if (result.ok) {
yield result.value;
}
else {
const msg = result.error.message;
if (msg === ERR_CLOSED.message ||
msg === ERR_DISPOSED_RECEIVER.message) {
return;
}
throw result.error;
}
}
}
close() {
// Helper to force close via temporary sender
const sender = new Sender(this.internals);
sender.close();
}
[Symbol.dispose]() {
if (this.disposed)
return;
this.disposed = true;
const prevCount = Atomics.sub(this.internals.state, IDX_RX_COUNT, 1);
if (prevCount === 1)
this.close();
}
static [toDeserialized](obj) {
return new _c(deserialize(obj));
}
}
_c = Receiver;
(() => {
register(6, _c);
})();
export function channel(capacity = 32, options) {
const state = new Int32Array(new SharedArrayBuffer(META_SIZE * Int32Array.BYTES_PER_ELEMENT));
state[IDX_CAP] = capacity;
state[IDX_HEAD] = 0;
state[IDX_TAIL] = 0;
state[IDX_CLOSED] = OPEN;
state[IDX_TX_COUNT] = 1;
state[IDX_RX_COUNT] = 1;
const initialData = new Array(capacity).fill(null);
const items = new SharedJsonBuffer(initialData, options);
const internals = new ChannelInternals(state, items, new Semaphore(1), // sendLock
new Semaphore(1), // recvLock
new Semaphore(0), // itemsAvailable
new Semaphore(capacity));
return [new Sender(internals), new Receiver(internals)];
}