İçeriğe geç

Katalog

Katalog tek bir nesne ve tek bir zincirdir; build() yoktur. Her adım aynı nesneyi döndürür ama dönüş tipini daraltır, bu yüzden her zaman dönen referansı kullan. Katalog sayaçların id’lerini, boyut şemalarını ve havuz beslemesini bilir; sayacın birimi, türü, dönemi, politikası yoktur.

Adım Ne yapar Tip etkisi
.context(schema | typed<C>()) ya da .context<C>() Bağlam şeması; her bağlı çağrıda doğrulanır. { validate: "never" } ile kapatılır Ctx
.refs(schema | ["a", "b"] | typed<R>()) ya da .refs<R>() Geçerli ref.type kümesi Ref.type birliği
.meter(id, dims?, { feeds? }) Sayaç ekler MeterId’ye ekler, DimsOf<id> türetir
.getRate(fn) (meter, dims, ctx, at) => { rate, usedSoFar?, carry? } | null bağlı bayrağı
.commit(fn) (plan) => void; fırlatırsa commit_failed, InsufficientCredit fırlatırsa insufficient_credit bağlı bayrağı
.bind({ getRate, commit }) Aynı katalogun yeni bağlı kopyası ikisi birden

Her zaman var: meters, price(), validateRate(). getRate ve commit ikisi de verilince: observe, hold, extend, capture, release, commit(plan) ve plan.*. Bağlanmadan observe çağırmak derleme hatasıdır.

examples/messaging/src/catalog.ts
import { buildMetering, type Metering, type PricedLineOf, type RefOf } from "pricemeter";
import { z } from "zod";
export const Context = z.object({
accountId: z.string(),
plan: z.enum(["free", "pro", "enterprise"]),
});
export type Context = z.infer<typeof Context>;
const country = z.string().length(2);
export const catalog = buildMetering()
.context(Context)
.refs(["otp_send", "wa_window", "month"])
// pool first: every message, whatever the channel, counts toward the monthly free platform quota
.meter("msg/free_pool")
.meter("otp/sms", { country }, { feeds: { "msg/free_pool": 1 } })
.meter("otp/whatsapp", { country }, { feeds: { "msg/free_pool": 1 } })
.meter("otp/verify")
.meter("wa/window", { country })
.meter("ip/dedicated");
export type Catalog = typeof catalog;
type State = Catalog extends Metering<infer S> ? S : never;
/** A priced line of this catalog, as `plan.observe(...).lines` returns it. */
export type CatalogLine = PricedLineOf<State>;
export type CatalogRef = RefOf<State>;
docs/src/snippets/catalog.test.ts
import { buildMetering, typed, type Rate } from "pricemeter";
const catalog = buildMetering()
.context(z.object({ accountId: z.string(), plan: z.enum(["free", "pro"]) }))
.refs(["otp_send", "period"])
// 1. a record of Standard Schemas: typed and validated on every call
.meter("otp/sms", { country: z.string().length(2) })
// 2. one object schema (zod, valibot, arktype…): typed and validated
.meter("cc/conversation", v.object({ tier: v.picklist(["short", "medium", "long"]) }))
// 3. typed<T>(): typed, never validated
.meter("storage/gb", typed<{ region: "eu" | "us" }>())
// 4. a key array: values are `any`, never validated
.meter("legacy/api", ["endpoint"])
// no dims at all
.meter("ip/dedicated");
Biçim Tip Çalışma anında doğrulama
Şema kaydı { country: z.string() } şemalardan her çağrıda, alan alan; bilinmeyen anahtar hata
Nesne şeması z.object({…}), v.object({…}) şemadan her çağrıda
typed<T>() T yok
Anahtar dizisi ["country"] değerler any yok

Boyut değerleri string, sayı ya da enum olabilir; joker yoktur (joker eşleşme getRate’in işidir, bkz. /rates). Doğrulama başarısızsa sonuç { ok: false, reason: "invalid_dims", meter, cause } olur. Kütüphane şema kütüphanesine bağımlı değildir, yalnızca ~standard.validate çağırır.

docs/src/snippets/catalog.test.ts
const withPools = buildMetering()
.context(z.object({ accountId: z.string(), plan: z.enum(["free", "pro"]) }))
.refs(["otp_send", "request"])
.meter("msg/free_pool") // a pool is an ordinary meter: no dims, no feeds, declared first
.meter("llm/credits")
.meter("otp/sms", { country: z.string().length(2) }, { feeds: { "msg/free_pool": 1 } })
.meter("otp/whatsapp", { country: z.string().length(2) }, { feeds: { "msg/free_pool": 1 } })
// weight as a function of (dims, ctx): opus output counts 5 credits per token, the rest 3
.meter("llm/output", { model: z.string() }, { feeds: { "llm/credits": (d, _ctx) => (d.model === "opus" ? 5 : 3) } });
withPools.meters["otp/sms"]; // { dims: ["country"], feeds: ["msg/free_pool"] }
  • Havuz sıradan bir sayaçtır: boyutsuz, kendisi feeds almaz ve besleyenlerden önce tanımlanır (A89). Tip sistemi yalnızca bu koşulu sağlayan id’leri feeds anahtarı olarak kabul eder.
  • Ağırlık bir sayı ya da (dims, ctx) => number fonksiyonudur. Havuz satırının miktarı ceil(quantity × weight), float gürültüsü arındırılarak: bir tamsayıya göreli 1e-9 kadar yakın değerler o tamsayıya çekilir (10 × 1.1 11 olur, 12 değil; A90, A104).
  • Havuz satırı kendi getRate’ini alır ve kendi usage satırını yazar (detail.feeder, detail.weight). Aynı çağrıda havuz satırını elle verirsen lib ikinci kez eklemez; { feeds: false } genişletmeyi kapatır.

metering.meters düz, serileştirilebilir bir görünümdür: { "otp/sms": { dims: ["country"], feeds: ["msg/free_pool"] } }. Boyut anahtarları şema kaydından, anahtar dizisinden ya da bir nesne şemasından (zod .shape, valibot .entries, arktype .props) okunur; typed<T>() için dims: [] görünür.

docs/src/snippets/catalog.test.ts
// @ts-expect-error — "msg/unknown" is not a pool declared earlier
buildMetering().meter("msg/free_pool").meter("otp/sms", ["country"], { feeds: { "msg/unknown": 1 } });
// @ts-expect-error — observe needs getRate and commit first
catalog.observe("otp/sms", { country: "TR" }, 1, { type: "otp_send", id: "1" }, { accountId: "a", plan: "pro" });

Aynı kurallar çalışma anında da denetlenir: tanımsız havuz, boyutlu havuz, feeds alan bir sayacı havuz yapmak ve aynı id’yi iki kez tanımlamak meter() çağrısında fırlatır.

docs/src/snippets/catalog.test.ts
const sms: Rate = { model: "graduated", tiers: [{ from: 0, unitPriceMicroUsd: 20_000 }] };
const metering = withPools
// declare all four parameters, even unused ones (A94)
.getRate(async (meter, dims, _ctx, _at) => {
if (meter === "otp/sms") {
dims.country; // string — dims narrow on meter
return { rate: sms };
}
return null; // no price → ifMissing
})
.commit(async (_plan) => {
/* your transaction */
});
// The same catalog with other adapters: a new bound copy; `withPools` itself is unchanged.
const forTests = withPools.bind({ getRate: async (_m, _d, _c, _a) => null, commit: async () => {} });
  • getRate o anki tarifenin tamamını döner, tek bir birim fiyat değil. Ham obje dönebilir; lib validateRate ile doğrular, geçersizse invalid_rate döner. null/undefined = fiyat yok → ifMissing.
  • Aynı çağrıda aynı (meter, dims) için getRate bir kez çağrılır.
  • bind katalogu değiştirmez, bağlı yeni bir kopya döner. Paylaşılan bir katalog (ör. hem Worker hem Durable Object içinde) adapter’sız kalır; her taraf kendi adapter’larını bağlar (mesajlaşma örneği).
docs/src/snippets/catalog.test.ts
await metering.observe("otp/sms", { country: "TUR" }, 1, { type: "otp_send", id: "1" }, ctx);
// → { ok: false, reason: "invalid_dims", meter: "otp/sms", cause: [zod issues] }
await metering.observe("otp/sms", { country: "TR" }, 1, { type: "otp_send", id: "1" }, { accountId: "a", plan: "gold" } as never);
// → { ok: false, reason: "invalid_context", cause: [zod issues] }
await metering.observe("otp/sms", { country: "TR" }, 1, { type: "refund", id: "1" } as never, ctx);
// → { ok: false, reason: "invalid_ref", cause: 'unknown ref type "refund"' }
  • Bağlam her bağlı çağrıda şemayla doğrulanır (D17/A73); şemanın dönüştürdüğü değer (coerce, default) getRate’e ve feeds fonksiyonlarına iletilir. Metering bağlamdan yalnızca accountId’yi okur; bu alan her durumda string olmalıdır.
  • refs() bir dizi, bir şema ya da yalnızca tip alabilir. Dizi/şema verilmişse ref.type doğrulanır, geçmezse invalid_ref (A86). ref.id her zaman string olmalıdır.

unit (sunum), type (gözlemin tek şekli var: miktar), aggregation / period / chargeTiming / batch (uygulamanın zamanı), missingPolicy (observe seçeneği: ifMissing), holdable (her sayaç hold alabilir), costBearing / counted (uygulama meta’sı).

500 sayaçlı bir katalog ~0,6 sn’de tip denetiminden geçer (bun run test:bench-types, bütçe 2 sn). Tip durumu her adımda alias’sız bir MeteringState<…> olarak yazılır (A93). Pratik zincir sınırı ~800 .meter() çağrısıdır; TypeScript ayrıştırıcısı 1000 zincirli çağrıda yığını taşırır (A96). Daha büyük kataloglar bölünebilir.