UNPKG

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
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)]; }