4.2 KiB
budget — BudgetTracker
(no submodules)
Purpose
This module owns the per-request resource-budget enforcement for the
sandboxed tier. Sampling primitives (CPU percentage, resident memory,
wall-clock progress) are supplied by the worker host because they live
outside this package's dependency budget; this module aggregates
samples against the policy's budgets block and raises
SandboxPermissionError(code: SAND_PERM.BUDGET_EXCEEDED) on the first
newly-breached dimension.
The tracker is idempotent across repeat samples on the SAME dimension: once a dimension has breached, subsequent ticks against that dimension do NOT re-emit the deny counter or re-throw — the dimension is considered "tripped" until the tracker is discarded at request end.
Features
BudgetTracker.tick(sample)— consumes one{ cpuPercent, memoryMb }sample plus the wall-clock from the injectedClock.BudgetTracker.elapsedMs()— milliseconds elapsed since construction.- Idempotent breaches —
cpuPercent,memoryMb,wallClockMseach emit at most once per tracker instance. - Throws
SandboxPermissionErrorwithcode === SAND_PERM.BUDGET_EXCEEDEDon the first newly-breached dimension and incrementsplugin.budget_exceeded_total{plugin, budget}.
Usage: Basic
import { BudgetTracker } from "@suaveplan/api-sandbox-permissions/budget";
import { createPermissionInstruments } from "@suaveplan/api-sandbox-permissions/instruments";
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
const policy = new PermissionPolicy({
permissions: { budgets: { cpuPercent: 50, memoryMb: 256, wallClockMs: 30_000 } },
});
const instruments = createPermissionInstruments({ plugin: "billing" });
const clock = { now: () => Date.now() };
const tracker = new BudgetTracker({ policy, instruments, clock });
tracker.tick({ cpuPercent: 10, memoryMb: 64 }); // within budget — no throw
Usage: Advanced
import { BudgetTracker } from "@suaveplan/api-sandbox-permissions/budget";
import { createPermissionInstruments } from "@suaveplan/api-sandbox-permissions/instruments";
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
import { SAND_PERM_CODES, SandboxPermissionError } from "@suaveplan/api-sandbox-permissions/error-codes";
const policy = new PermissionPolicy({
permissions: { budgets: { wallClockMs: 30_000 } },
});
const instruments = createPermissionInstruments({ plugin: "billing" });
// Injectable Clock — drive the wall-clock breach deterministically.
let t = 0;
const clock = { now: () => t };
const tracker = new BudgetTracker({ policy, instruments, clock });
t = 30_001;
try {
tracker.tick({ cpuPercent: 0, memoryMb: 0 });
} catch (err) {
if (err instanceof SandboxPermissionError) {
console.assert(err.code === SAND_PERM_CODES.BUDGET_EXCEEDED);
// err.context = { capability: "budget", reason: "wallClockMs" }
}
}
// Subsequent ticks against the same dimension are no-ops — the
// dimension is already tripped.
t = 35_000;
tracker.tick({ cpuPercent: 0, memoryMb: 0 }); // returns silently
API Reference
interface BudgetSample { cpuPercent: number; memoryMb: number; }interface BudgetTrackerOptions { policy; instruments; clock: Clock; }type BudgetBreach = "cpuPercent" | "memoryMb" | "wallClockMs"class BudgetTrackerconstructor(options)tick(sample): void— throws on first newly-breached dimension.elapsedMs(): numberreadonly policy: PermissionPolicyreadonly instruments: PermissionInstruments
Implementation Notes
The Clock is injected per @suaveplan/types — never pulled from
Date.now() directly so test code can drive a deterministic timeline
without vi.useFakeTimers(). The constructor records startedAt = clock.now()
once; every subsequent elapsedMs() call subtracts from a fresh
clock.now() so wall-clock progress is monotonic with the clock.
evaluate is private and centralises the breach record so all three
dimensions go through the SAME counter + throw codepath; this guarantees
the conformance harness's "deny counter increments exactly once per
breach" invariant holds for every dimension without per-dimension
duplication.