price() ve plan
price() kütüphanenin gerçek çekirdeğidir: saf, adapter’sız, I/O’suz. observe yalnızca “getRate çek → price() → commit” sarmalayıcısıdır. Görüşlü bir ürün price()’ı doğrudan kullanabilir.
price()
Bölüm başlığı “price()”const sms = defineRate({ model: "graduated", tiers: [{ from: 0, unitPriceMicroUsd: 20_000 }, { from: 10_000, unitPriceMicroUsd: 15_000 }] });const { result, plan, rated } = price( [{ meter: "otp/sms", dims: { country: "TR" }, quantity: 20, rate: sms, usedSoFar: 9_990 }], { type: "otp_send", id: "m9" }, { accountId: "acme" }, { at: Date.parse("2026-09-28T12:00:00Z") },);result; // { ok: true, charged: 350000, lines: [{ tierIndex: 1, breakdown: [{ from: 0, quantity: 10 }, { from: 10000, quantity: 10 }] }] }rated[0]; // { totalMicroUsd: 350000, carry: 0, holdUpperBound: 400000, … }Girdi PricedLine[]’dır: tarifesi (ValidRate) ve usedSoFar’ı eklenmiş satırlar. Çıktı { result, plan, rated }; rated[i], i. satırın rate() sonucudur (breakdown, tierIndex, carry, holdUpperBound, clamped). Kök price() havuz genişletmesi yapmaz; katalogdaki metering.price(lines, ref, ctx, { pools }) feeds’i uygular, eksik havuz satırının tarifesini opts.pools’tan alır.
Plan, kütüphanenin tek çıktısıdır. commit onu ya hep ya hiç yazmalı ve tekrarlanan (refType, refId)’yi sessizce atlamalıdır.
/** * Everything the library wants written, and the only thing it produces. * `commit` must apply it all or nothing, and treat a repeated `(refType, refId)` as a no-op. */export interface Plan { ledger: LedgerOp[]; usage: UsageRow[];}
export type LedgerOp = ChargeOp | HoldOp | ExtendOp | CaptureOp | ReleaseOp;
export interface UsageRow { account: string; meter: string; dims: Record<string, unknown>; quantity: number; amount: MicroUsd; rateId?: string; detail: UsageDetail; refType: string; refId: string; at: number; /** * The period's hard cap for this meter (from `getRate`). `commit` must refuse the whole plan with * `QuotaExceeded` if the meter's counter for the row's period plus `quantity` would exceed it. */ limit?: number;}LedgerOp.op |
Anlamı | amount |
|---|---|---|
charge |
Para hareketi | pozitif = borç; negatif = alacak (hacim düzeltmesi, kredi) |
hold |
Hold için rezervasyon | hold’un üst sınırı |
extend |
Rezervasyonu büyütür | üst sınır farkı |
capture |
Rezervasyondan tahsil | yakalanan tutar |
release |
Kalanı serbest bırakır | upperBound − captured |
UsageRow.detail satırın nasıl fiyatlandığını taşır: breakdown (kademe dilimleri), havuz satırında feeder ve weight, clamped, hold satırlarında holdId, düzeltme satırında adjustment: true, cumulative’de carry. rateId, tarifenin id’sidir: her satır onu fiyatlayan tarifeye kadar izlenebilir.
refId kuralları
Bölüm başlığı “refId kuralları”const { plan } = await metering.plan.observe( [ { meter: "otp/sms", dims: { country: "TR" }, quantity: 1 }, { meter: "otp/sms", dims: { country: "TR" }, quantity: 1 }, // same meter again → #2 ], { type: "otp_send", id: "m1" }, ctx,);plan.usage.map((u) => [u.refId, u.amount]);// [ ["otp_send:m1:otp/sms", 20000],// ["otp_send:m1:otp/sms#2", 20000],// ["otp_send:m1:otp/sms:msg/free_pool", 0], ← pool line: {refType}:{id}:{feeder}:{pool}// ["otp_send:m1:otp/sms:msg/free_pool#2", 0] ]plan.ledger.map((l) => l.refId);// [ "otp_send:m1:otp/sms", "otp_send:m1:otp/sms#2" ] ← zero amounts: usage only, no ledger row| Satır | refId |
|---|---|
| Gözlem satırı | {refType}:{ref.id}:{meter} |
| Aynı çağrıda aynı sayaç tekrar ederse | …#2, …#3 (A84) |
| Havuz satırı | {refType}:{ref.id}:{feeder}:{pool} |
| Hacim düzeltmesi | {satırın refId'si}:adj (A81) |
| Hold | {refType}:{ref.id}:hold — holdId = {refType}:{ref.id} |
| Extend | {holdId}:extend:{n} |
| Capture | {refType}:{ref.id}:{meter}:capture:{n} (A87); havuz satırı …:{feeder}:{pool}:capture:{n} |
| Release | {holdId}:release |
refId tamamen ref ve satır sırasından türetilir; aynı çağrı aynı planı üretir. Uygulama retry’ının güvenli olması buradan gelir: commit ikinci planı no-op yapar.
Hacim düzeltmesi
Bölüm başlığı “Hacim düzeltmesi”// usedSoFar 900, volume on_crossing: 200 × 8,000 and −900 × 2,000 for the earlier unitsconst { result, plan } = await metering.plan.observe("api", {}, 200, { type: "req", id: "r2" }, ctx);result; // { ok: true, charged: -200000, lines: [{ amount: 1600000, adjustment: -1800000, … }] }plan.ledger; // [{ op: "charge", amount: 1600000, refId: "req:r2:api" },// { op: "charge", amount: -1800000, refId: "req:r2:api:adj" }]Rated.totalMicroUsd düzeltmeyi içermez (A81): usage satırı gözlemin kendi tutarıdır, düzeltme …:adj refId’li ayrı bir usage satırı (quantity: 0, detail.adjustment: true) ve ayrı bir charge’dır. result.charged ikisinin toplamıdır ve negatif olabilir.
Sıfır tutarlar
Bölüm başlığı “Sıfır tutarlar”Tutarı 0 olan satır usage’a yazılır, ledger’a yazılmaz (A85). “Ücretsiz ama sayılır”: 0 fiyatlı tarife, havuzun bedava dilimi ve ifMissing: "free" bu yüzden usedSoFar sayaçlarını ilerletir ama boş ledger kalemi üretmez. Hem ledger’ı hem usage’ı boş bir plan için commit hiç çağrılmaz.
Gömülü mod
Bölüm başlığı “Gömülü mod”Sayaçlar ve bakiye tek bir atomik yerdeyse (Cloudflare’de hesabın Durable Object’i), fiyatlamayı da oraya taşımak yarışı kapatır: Worker planı kendi görüşüyle çıkarır, Durable Object satırları kendi sayaçlarıyla yeniden fiyatlar ve aynı adımda uygular.
- Worker:
metering.plan.observe(...)→{ result, plan, lines }.linesfiyatlanmış satırlardır (havuz satırları dahil, tarifeleriyle; A91). - Durable Object:
usedSoFar’ı kendi sayacından doldurur,catalog.price(lines, ref, ctx)çağırır, çıkan planıtransactionSynciçinde uygular. Aradaawaityoktur; DO’nun giriş kapısı bunu atomik yapar.
export class AccountCore implements AccountApi { constructor( private storage: DurableStorageLike, private sink: (rows: readonly UsageRow[]) => Promise<void>, private opts: { prepaid: boolean } = { prepaid: true }, ) { migrateAccount(storage.sql); }
async commit(plan: Plan): Promise<CommitReply> { return commitReply(this.storage, plan, { prepaid: this.opts.prepaid, periodOf }); }
async used(meter: string, period: string): Promise<number> { return accountUsed(this.storage.sql, meter, period); }
async observeEmbedded(lines: CatalogLine[], ref: CatalogRef, ctx: Context): Promise<Result> { // Nothing awaits between reading counters and writing: the DO's input gate makes this atomic. const fresh = lines.map((l) => ({ ...l, usedSoFar: accountUsed(this.storage.sql, l.meter, periodOf({ at: l.at ?? Date.now() })) })); const { result, plan } = catalog.price(fresh, ref, ctx); if (!result.ok) return result; const reply = commitReply(this.storage, plan, { prepaid: this.opts.prepaid, periodOf }); return reply.ok ? result : { ok: false, reason: "insufficient_credit" }; }
async credit(amountMicroUsd: number): Promise<void> { creditAccount(this.storage.sql, amountMicroUsd); }
async state(): Promise<AccountState> { return accountState(this.storage.sql); }
async flush(): Promise<number> { let total = 0; for (let n = -1; n !== 0; total += n) n = await flushOutbox(this.storage.sql, this.sink); return total; }}Bkz. mesajlaşma örneği ve Cloudflare adapter’ı.