Skip to Content

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?);
OptionTypeDefaultDescription
capacitynumber1Number of concurrent slots. Must be >= 1.
statsPrefixstringnamePrefix 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().

OptionTypeDefaultDescription
prioritynumber0Lower 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

AccessorTypeDescription
resource.namestringName given at construction
resource.capacitynumberTotal slot count
resource.inUsenumberCurrently occupied slots
resource.queueLengthnumberRequests waiting
resource.isAvailablebooleaninUse < capacity

Auto-Collected Statistics

All stat keys are prefixed with resource.{name}.:

KeyDescription
resource.{n}.waitTimeTime between request() and callback invocation
resource.{n}.queueLengthQueue depth snapshot at each state change
resource.{n}.utilizationinUse / capacity at each state change
resource.{n}.requestsTotal calls to request()
resource.{n}.grantsTotal 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 priority

Within 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.

Last updated on