@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
172 lines • 6.18 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { abortable, hasAborted, makeAbortError, mergeSignals } from "../utils/abort.js";
/**
* Limit concurrent async operations within one process.
*
* Twin of python's `concurrency/limiter.py`, which wraps
* `asyncio.Semaphore`. Node has no built-in semaphore, so the queue is
* explicit -- but the two contracts match: a permit count below one is
* rejected rather than clamped, and a release hands its permit straight
* to the longest-waiting caller instead of returning it to the pool.
* Handing off matters: returning it first would let a caller arriving in
* the same tick barge ahead of the queue and drive the count negative.
*/
export class ConcurrencyLimiter {
available;
waiters = [];
constructor(maxConcurrency) {
if (maxConcurrency < 1)
throw new Error('maxConcurrency must be at least 1');
this.available = maxConcurrency;
}
/**
* Take one permit, waiting for a free one, and return its release.
*
* The release is idempotent so a caller that unwinds twice (a `finally`
* plus an explicit call) cannot inflate the permit count -- python gets
* that from `async with` and the shape here has to supply it. An abort
* leaves the queue without a permit, as a cancelled `acquire` does in
* python.
*
* Args:
* signal: abandons the wait when it fires.
*/
async acquire(signal) {
if (signal?.aborted === true)
throw makeAbortError(signal);
if (this.available > 0) {
this.available -= 1;
}
else {
await new Promise((resolve, reject) => {
const onAbort = () => {
this.waiters.splice(this.waiters.indexOf(admit), 1);
reject(makeAbortError(signal));
};
const admit = () => {
signal?.removeEventListener('abort', onAbort);
resolve();
};
this.waiters.push(admit);
signal?.addEventListener('abort', onAbort, { once: true });
});
}
if (hasAborted(signal)) {
this.release();
throw makeAbortError(signal);
}
let released = false;
return () => {
if (released)
return;
released = true;
this.release();
};
}
release() {
const next = this.waiters.shift();
if (next !== undefined) {
next();
return;
}
this.available += 1;
}
}
/** Map in order with bounded workers; a failure stops admission and joins active work. */
export async function boundedMap(items, fn, workers) {
const results = new Array(items.length);
const remaining = items.entries();
let failure;
const worker = async () => {
for (const [index, item] of remaining) {
if (failure !== undefined)
return;
try {
results[index] = await fn(item);
}
catch (reason) {
failure ??= { reason };
return;
}
}
};
await Promise.all(Array.from({ length: Math.min(Math.max(1, workers), items.length) }, worker));
if (failure !== undefined)
throw failure.reason;
return results;
}
/** Map a stream in input order, owning all admitted work through cancellation. */
export async function* boundedMapStream(items, fn, workers, parent) {
const controller = new AbortController();
const signal = mergeSignals(parent, controller.signal) ?? controller.signal;
const permits = workers > 0 ? new ConcurrencyLimiter(workers) : null;
const ready = [];
let done = false;
let failure;
let wake;
const isDone = () => done;
const iterator = items(signal)[Symbol.asyncIterator]();
const producer = (async () => {
try {
for (;;) {
const release = permits === null ? () => undefined : await permits.acquire(signal);
const next = await abortable(iterator.next(), signal);
if (next.done === true) {
release();
return;
}
const result = fn(next.value, signal).then((value) => ({ value }), (error) => ({ error }));
ready.push({ result, release });
wake?.();
}
}
catch (error) {
failure = { error };
}
finally {
done = true;
wake?.();
}
})();
try {
for (;;) {
const entry = ready[0];
if (entry === undefined) {
if (isDone())
break;
await abortable(new Promise((resolve) => {
wake = resolve;
}), signal);
wake = undefined;
continue;
}
const result = await abortable(entry.result, signal);
if ('error' in result)
throw result.error;
yield result.value;
ready.shift();
entry.release();
}
if (failure !== undefined)
throw failure.error;
}
finally {
controller.abort();
await producer;
await Promise.all(ready.map((entry) => entry.result));
await iterator.return?.();
}
}
//# sourceMappingURL=limiter.js.map