Files
2026-05-27 23:29:53 +01:00

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 injected Clock.
  • BudgetTracker.elapsedMs() — milliseconds elapsed since construction.
  • Idempotent breaches — cpuPercent, memoryMb, wallClockMs each emit at most once per tracker instance.
  • Throws SandboxPermissionError with code === SAND_PERM.BUDGET_EXCEEDED on the first newly-breached dimension and increments plugin.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 BudgetTracker
    • constructor(options)
    • tick(sample): void — throws on first newly-breached dimension.
    • elapsedMs(): number
    • readonly policy: PermissionPolicy
    • readonly 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.