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.
Zincir
Bölüm başlığı “Zincir”| 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.
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>;Boyutlar: dört biçim
Bölüm başlığı “Boyutlar: dört biçim”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.
Havuzlar: feeds
Bölüm başlığı “Havuzlar: feeds”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
feedsalmaz ve besleyenlerden önce tanımlanır (A89). Tip sistemi yalnızca bu koşulu sağlayan id’lerifeedsanahtarı olarak kabul eder. - Ağırlık bir sayı ya da
(dims, ctx) => numberfonksiyonudur. 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.111 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.
Tip hataları
Bölüm başlığı “Tip hataları”// @ts-expect-error — "msg/unknown" is not a pool declared earlierbuildMetering().meter("msg/free_pool").meter("otp/sms", ["country"], { feeds: { "msg/unknown": 1 } });
// @ts-expect-error — observe needs getRate and commit firstcatalog.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.
getRate, commit ve bind
Bölüm başlığı “getRate, commit ve bind”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 () => {} });getRateo anki tarifenin tamamını döner, tek bir birim fiyat değil. Ham obje dönebilir; libvalidateRateile doğrular, geçersizseinvalid_ratedöner.null/undefined= fiyat yok →ifMissing.- Aynı çağrıda aynı
(meter, dims)içingetRatebir kez çağrılır. bindkatalogu 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).
Bağlam ve ref doğrulaması
Bölüm başlığı “Bağlam ve ref doğrulaması”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 vefeedsfonksiyonlarına iletilir. Metering bağlamdan yalnızcaaccountId’yi okur; bu alan her durumda string olmalıdır. refs()bir dizi, bir şema ya da yalnızca tip alabilir. Dizi/şema verilmişseref.typedoğrulanır, geçmezseinvalid_ref(A86).ref.idher zaman string olmalıdır.
Katalogda olmayanlar
Bölüm başlığı “Katalogda olmayanlar”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ı).
Boyut ve hız
Bölüm başlığı “Boyut ve hız”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.