UsageRollupStore
@forge/monorepo / backend/src / UsageRollupStore
Interface: UsageRollupStore
Defined in: backend/src/persistence/index.ts:513
Aggregated consumption, derived from the ledger — never a second source of truth.
Rollups are recomputed, not accumulated. rebuild reads a bucket's raw events and replaces the row.
That makes idempotency structural rather than bookkept: re-running a bucket produces the same numbers, and
two workers racing the same bucket write the same value. The alternative — accumulating deltas with a set of
applied event keys — needs that set to be durable, unbounded and exactly right forever, and any gap in it is
silent double counting or silent loss.
The cost is one scan per bucket in the job, not in the query. That is what a rollup job is for; AC-1 is about the read path.
Methods
get()
get(
input):Promise<UsageRollup|null>
Defined in: backend/src/persistence/index.ts:541
One bucket, or null when nothing has been recorded in it.
principalId omitted reads the tenant row; supplied reads that person's — #175. They are the same
measurement at two grains, stored as a nullable dimension on one table rather than in two tables, because
two rebuild paths is two chances to disagree and the day they disagree is the day an invoice is wrong.
A per-principal quota needs this: without it, a limit on one person is checked against everybody's usage.
Parameters
input
TenantScope & object
Returns
Promise<UsageRollup | null>
list()
list(
input):Promise<Page<UsageRollup>>
Defined in: backend/src/persistence/index.ts:551
A range of buckets, for a chart — the read path AC-1 is about.
from inclusive, to exclusive, so adjacent ranges tile without overlapping and a caller cannot
double-count a boundary bucket by asking for two ranges.
Parameters
input
TenantScope & PageRequest & object
Returns
Promise<Page<UsageRollup>>
listStaleBuckets()
listStaleBuckets(
input):Promise<Page<{bucketStart:string;period:"hour"|"day"|"week"|"month";principalId?:PrincipalId; }>>
Defined in: backend/src/persistence/index.ts:576
Buckets that have never been computed, or were computed before the newest event in them.
The rollup job's work list, derived from the ledger so an interrupted job resumes by asking again — the
same shape listStaleSources uses for re-indexing, and for the same reason.
Parameters
input
TenantScope & PageRequest & object
Returns
Promise<Page<{ bucketStart: string; period: "hour" | "day" | "week" | "month"; principalId?: PrincipalId; }>>
rebuild()
rebuild(
input):Promise<UsageRollup>
Defined in: backend/src/persistence/index.ts:528
The store stamps computedAt itself, from its own clock.
Not the caller's, and this is not a convenience: staleness is defined by comparing a bucket's
computedAt against when its newest event was recorded, and both of those are the store's timestamps. A
caller-supplied value is a caller-supplied answer to "is this rollup current" — and a caller whose clock
runs slow, or which passes a fixed value, would mark a bucket permanently stale or permanently fresh.
Found by a conformance fixture passing a constant and every bucket coming back stale.
Parameters
input
TenantScope & object
Returns
Promise<UsageRollup>
sum()
sum(
input):Promise<UsageTotals>
Defined in: backend/src/persistence/index.ts:566
The sum across a range, without returning the buckets.
Separate from list because a quota check needs one number and a chart needs many, and making the quota
check page through buckets would put the read path's cost on the admission path.
Parameters
input
TenantScope & object
Returns
Promise<UsageTotals>