Resource
Resource implements the seize/delay/release pattern for capacity-constrained shared resources — the building block of M/M/c queueing models (servers, machines, staff, connections).
Quick Start
import { SimulationEngine, Resource } from 'simloop';
type Events = {
'job:arrive': { jobId: number };
'job:done': Record<string, never>;
};
const sim = new SimulationEngine<Events>({ seed: 42 });
const server = new Resource<Events>('server'); // capacity defaults to 1
sim.on('job:arrive', (event, ctx) => {
const arrivalTime = ctx.clock;
server.request(ctx, (ctx) => {
ctx.stats.record('waitTime', ctx.clock - arrivalTime);
ctx.schedule('job:done', ctx.clock + ctx.dist.exponential(1)(), {});
});
ctx.schedule('job:arrive', ctx.clock + ctx.dist.exponential(0.8)(), {
jobId: event.payload.jobId + 1,
});
});
sim.on('job:done', (_e, ctx) => {
server.release(ctx);
});The Seize / Delay / Release Cycle
ARRIVE → SEIZE resource → DELAY (use) → RELEASE → DEPART
↑ |
└── if full: QUEUE → wait ──────────┘Constructor
const resource = new Resource<TEventMap, TStore>(name, options?);| Option | Type | Default | Description |
|---|---|---|---|
capacity | number | 1 | Number of concurrent slots. Must be >= 1. |
statsPrefix | string | name | Prefix for all auto-collected stat keys. |
resource.request(ctx, cb, opts?)
If a slot is free, cb is called immediately. If all slots are busy, the request is queued and cb fires when another holder calls release().
Returns a RequestHandle that can be passed to cancel().
| Option | Type | Default | Description |
|---|---|---|---|
priority | number | 0 | Lower value = higher precedence. Ties broken by FIFO. |
resource.release(ctx)
Decrements inUse and grants the next pending request (if any).
Throws SimulationError if no slot is currently held.
Call this exactly once per acquired slot. Forgetting to call release() keeps the slot seized permanently.
resource.cancel(handle)
Cancels a pending (not yet granted) request. Returns true if removed from queue, false if already granted.
Sets handle.cancelled = true in both cases. Note that cancel() cannot revoke an already-granted slot — once the callback has been called, the caller must call release().
resource.snapshot()
Returns a plain object with the current state. Useful for logging and assertions.
const snap = resource.snapshot();
// { name, capacity, inUse, queueLength }resource.reset()
Clears inUse, the wait queue, and the request counter. Must be called after engine.reset() before re-running the simulation.
sim.reset();
resource.reset(); // ← required
sim.init((ctx) => { /* re-init */ });
sim.run();Multi-Server (M/M/c)
const baristas = new Resource<Events>('baristas', { capacity: 3 });Accessors
| Accessor | Type | Description |
|---|---|---|
resource.name | string | Name given at construction |
resource.capacity | number | Total slot count |
resource.inUse | number | Currently occupied slots |
resource.queueLength | number | Requests waiting |
resource.isAvailable | boolean | inUse < capacity |
Auto-Collected Statistics
All stat keys are prefixed with resource.{name}.:
| Key | Description |
|---|---|
resource.{n}.waitTime | Time between request() and callback invocation |
resource.{n}.queueLength | Queue depth snapshot at each state change |
resource.{n}.utilization | inUse / capacity at each state change |
resource.{n}.requests | Total calls to request() |
resource.{n}.grants | Total successful acquisitions |
Priority Queue Behaviour
By default, all requests have priority = 0 and are served in FIFO order.
// Lower priority number = served first
resource.request(ctx, cb, { priority: 1 }); // high priority
resource.request(ctx, cb, { priority: 10 }); // low priorityWithin the same priority level, requests are served in the order request() was called (FIFO). Negative priorities are allowed — priority is a plain number; the minimum value wins.
Cancellation Pattern (Patience Timeout)
sim.on('customer:arrive', (event, ctx) => {
const handle = baristas.request(ctx, (ctx) => {
if (timeoutEvent) ctx.cancelEvent(timeoutEvent);
// do work ...
});
const timeoutEvent = ctx.schedule('customer:leave', ctx.clock + patience, {
customerId: event.payload.customerId,
});
});
sim.on('customer:leave', (event, ctx) => {
baristas.cancel(handle);
ctx.removeEntity(event.payload.customerId);
});Edge Cases and Gotchas
Forgetting to call release()
The slot remains seized indefinitely. inUse never drops, queued requests wait forever, and utilization stays at 1.0. Always ensure release() is reachable from every code path inside the callback.
// WRONG — release() never called on early return
resource.request(ctx, (ctx) => {
if (condition) return; // BUG
ctx.schedule('done', ctx.clock + 1, {});
});
// CORRECT — explicit early release
resource.request(ctx, (ctx) => {
if (condition) {
resource.release(ctx);
return;
}
ctx.schedule('done', ctx.clock + 1, {});
});capacity = Infinity (M/M/∞ model)
All requests are granted immediately — no queuing ever occurs. Useful for infinite-server queues where each customer always finds a free server.
Double-release
Calling release() twice throws SimulationError. This surfaces double-release bugs immediately during development.
Multiple resources with the same name
Stats will be recorded under the same prefix and merged — producing incorrect statistics for both resources. Names (or statsPrefix values) must be unique per simulation instance.
Preemption (Out of Scope)
Resource does not support preemption (forcibly evicting a current slot holder in favour of a higher-priority request).
Workaround: The current holder can listen for a “preempt” event and voluntarily call release() early, then re-request at a lower priority.