observe ve hold
observe, hold, extend, capture ve release bağlı katalogdadır. Her biri satırları genişletir (feeds), getRate’i çağırır, price()’ı çalıştırır, commit eder ve bir sonuç döner. Planı görmek için metering.plan.<aynı çağrı>: aynı imza, commit yok, { result, plan, lines } döner.
observe
Bölüm başlığı “observe”İki imza: tek satır ve çok satır.
metering.observe(meter, dims, quantity, ref, ctx, opts?)metering.observe([{ meter, dims, quantity }, …], ref, ctx, opts?)Çok satırlı bir observe tek, tutarlı bir gözlemdir: tek plan, tek commit, ya hep ya hiç. quantity negatif olmayan bir tamsayıdır; birim senin seçimindir (mesaj, token, dakika, koltuk-saniye, mikro-USD işlem tutarı).
| Seçenek | Anlamı | Varsayılan |
|---|---|---|
at |
Gözlem zamanı (epoch ms); getRate’e ve usage satırına gider |
Date.now() |
usedSoFar |
getRate’inkini ezer; tek satırda sayı, çok satırda { [meter]: sayı } |
— |
ifMissing |
getRate null dönerse: reject (hiçbir şey yazma, no_price) ya da free (0 ile say ve yaz) |
reject |
feeds |
false: havuz genişletmesi yapma |
true |
Üç durum
Bölüm başlığı “Üç durum”| Durum | Ne olur |
|---|---|
| Tarife var, fiyatı 0 | Ücretsiz ve fiyatlı: sayılır, usage’a 0 yazılır, ledger’a yazılmaz |
getRate → null, ifMissing: "reject" |
{ ok: false, reason: "no_price", meter }; hiçbir şey yazılmaz |
getRate → null, ifMissing: "free" |
0 fiyatlı tek kademeli tarifeyle sayılır ve yazılır |
no_price bir hata değil bir durumdur: fiyatı geç yayımlanan bir kalem (WhatsApp pencere fiyatları gibi) kuyruğa alınıp aynı at ile yeniden denenebilir (mesajlaşma örneği).
Hold: değer nesnesi
Bölüm başlığı “Hold: değer nesnesi”// Reserve: up to 3 SMS segments + 1 verification. The hold is a value; store it (e.g. on the OTP record).const h = await metering.hold( [ { meter: "otp/sms", dims: { country: "TR" }, quantity: 3 }, { meter: "otp/verify", quantity: 1 }, ], { type: "otp_send", id: "otp_1" }, ctx,);if (!h.ok) throw new Error(h.reason);h.upperBound; // 65000 + pool line's worst case (3 × 2000) = 71000h.hold.holdId; // "otp_send:otp_1" → ledger: { op: "hold", amount: 71000, refId: "otp_send:otp_1:hold" }
// Resend: grow the same hold. Existing lines keep their tariff; getRate is asked only for new lines.const e = await metering.extend(h.hold, [{ meter: "otp/sms", dims: { country: "TR" }, quantity: 1 }], ctx);if (!e.ok) throw new Error(e.reason);// ledger: { op: "extend", amount: 22000, refId: "otp_send:otp_1:extend:1" }
// Verified: capture what was sent (pool line follows its feeder), then release the rest.const c = await metering.capture( e.hold, [ { meter: "otp/sms", quantity: 2 }, { meter: "otp/verify", quantity: 1 }, ], ctx,);if (!c.ok) throw new Error(c.reason);c.charged; // 45000 → refIds "otp_send:otp_1:otp/sms:capture:1", "otp_send:otp_1:otp/verify:capture:1", …
const r = await metering.release(c.hold, ctx);// ledger: { op: "release", amount: 93000 − 45000 = 48000, refId: "otp_send:otp_1:release" }holdçok satırlı bir rezervasyondur. Her satırın tarifesi veusedSoFar’ı hold’un içine gömülür; uygulama hold’u saklar (ör. OTP kaydının üstünde), kütüphane ledger’dan okumaz.- Her çağrı hold’un güncellenmiş hâlini döner (
result.hold, A87). Sonraki çağrıya bu yeni değeri ver. Aynı eski hold ile retry aynırefId’yi üretir vecommit’te no-op olur. - Rezervasyon her satırın hold üst sınırıdır; havuz satırları da ayrılır. Hold
charged: 0döner: rezervasyon para hareketi değildir. - Bir hold’da her
(meter, dims)için tek satır vardır (A101): aynı anahtarlı satırlar birleşir, havuz satırı besleyicilerinifeeders[]olarak taşır. Hold her anahtarı bir kez yuvarlar:cumulative’de ayrı satırlı birobserveile birebir aynıdır,per_event_up’ta yalnızca yuvarlama kadar (satır başına en fazla 2 mikro birim) farklıdır, iki yönde de. extendhold’u büyütür: var olan(meter, dims)satırları kendi tarifesiyle büyür,getRateyalnızca yeni satırlar için çağrılır; havuz satırları besleyenlerini izler.capturegerçekleşeni tahsil eder. Satırmeterile bulunur; aynı sayaç birden çok satırdaysadimsgerekir. Havuz satırları besleyenleriyle birlikte yakalanır. Kısmi ve tekrarlı capture serbesttir.releasekalan rezervasyonu (upperBound − captured) serbest bırakır ve hold’u kapatır.
Teleskopik capture
Bölüm başlığı “Teleskopik capture”// 1/3 micro-USD per unit. Three captures of 1 unit each cost what one observation of 3 costs.const m = buildMetering().refs(["job"]).meter("gpu/ms").bind(memoryAdapters({ rates: { "gpu/ms": flat(1, 3) } }));const h = await m.hold("gpu/ms", {}, 3, { type: "job", id: "j1" }, { accountId: "acme" });if (!h.ok) throw new Error(h.reason);let hold = h.hold;const charged: number[] = [];for (let i = 0; i < 3; i++) { const c = await m.capture(hold, [{ meter: "gpu/ms", quantity: 1 }], { accountId: "acme" }); if (!c.ok) throw new Error(c.reason); charged.push(c.charged); hold = c.hold;}charged; // [1, 0, 0] — R(u, c+q) − R(u, c), not [1, 1, 1]Kısmi capture’lar hold’u tek bir gözlem gibi yuvarlar: n. capture R(u, c+q) − R(u, c) tahsil eder; u hold anındaki usedSoFar, c o ana kadar yakalanan adettir (A88). Aksi hâlde per_event_up parçalı capture’da üst sınırı aşardı.
Capture anının tarifesi
Bölüm başlığı “Capture anının tarifesi”const h = await metering.hold("otp/verify", {}, 1, { type: "otp_send", id: "otp_2" }, ctx);if (!h.ok) throw new Error(h.reason);// Default: the tariff stored in the hold (5000). Pass { rate } to price at capture time instead.const c = await metering.capture(h.hold, [{ meter: "otp/verify", quantity: 1 }], ctx, { rate: flat(4_000) });// c.charged → 4000 (must still fit the hold's upper bound, else hold_exceeded)Varsayılan olarak capture, hold’daki tarifeyi kullanır ve getRate’i tekrar çağırmaz: özel fiyat sona erse bile açık hold eski fiyatı korur (B20). Capture anının fiyatını isteyen { rate } verir: tek bir tarife ya da { [meter]: Rate }. Toplam yine hold’un üst sınırını aşamaz.
hold_exceeded ve hold_closed
Bölüm başlığı “hold_exceeded ve hold_closed”if (!r.ok) throw new Error(r.reason);await metering.capture(r.hold, [{ meter: "otp/sms", quantity: 1 }], ctx); // { ok: false, reason: "hold_closed" }await metering.capture(c.hold, [{ meter: "otp/sms", quantity: 3 }], ctx); // { ok: false, reason: "hold_exceeded", meter: "otp/sms" } (2 + 3 > 4)hold_exceeded: bir satırda yakalanan adet ayrılanı aşıyor ya da toplam tutar üst sınırı aşıyor. hold_closed: hold release edilmiş (A86). İkisinde de hiçbir şey yazılmaz; önce extend et.
Result nedenleri
Bölüm başlığı “Result nedenleri”Başarı: { ok: true, charged, lines } (hold/capture’da ayrıca holdId, hold; hold’da upperBound). charged bu çağrının net para hareketidir: tahsilatlar, capture’lar ve düzeltmeler; rezervasyon değil.
reason |
Ne zaman | Yazılan |
|---|---|---|
no_price |
getRate null döndü, ifMissing: "reject" |
hiçbir şey |
usage_required |
Tarife usedSoFar istiyor, verilmedi |
hiçbir şey |
insufficient_credit |
commit InsufficientCredit fırlattı (isimle de eşleşir, RPC’den geçer) |
hiçbir şey |
quota_exceeded |
usedSoFar + istenen > getRate’in limit’i, ya da commit QuotaExceeded fırlattı; cause: { meter, limit, used, requested } |
hiçbir şey |
invalid_context |
Bağlam şemadan geçmedi, accountId string değil ya da hold başka hesabın |
hiçbir şey |
invalid_dims |
Boyutlar sayacın şemasından geçmedi | hiçbir şey |
invalid_rate |
getRate’in ya da capture { rate }’in döndüğü tarife validateRate’ten geçmedi |
hiçbir şey |
invalid_ref |
ref şekli yanlış ya da ref.type refs() içinde değil |
hiçbir şey |
hold_exceeded |
Capture ayrılanı ya da üst sınırı aşıyor | hiçbir şey |
hold_closed |
Hold zaten release edilmiş |
hiçbir şey |
commit_failed |
commit başka bir hata fırlattı; hata cause’ta |
commit’in atomikliğine bağlı: hiçbir şey olmalı |
Bakiye kapısı
Bölüm başlığı “Bakiye kapısı”“Bakiye ≥ tutar” kütüphanede değildir. commit yetersiz bakiyede InsufficientCredit fırlatır, lib insufficient_credit döner. Hold ve extend de borç sayılır: prepaid bir commit ayrılabilir bakiyeyi aşan rezervasyonu reddeder. Postpaid = commit hiç fırlatmaz (B26).
Bakiye kapısı “ödeyebiliyor mu?” sorusunu yanıtlar; kota “hakkı var mı?” sorusunu. getRate rate ve usedSoFar’ın yanında aynı dönem için bir limit dönebilir (A107):
const metering = buildMetering() .context<{ accountId: string; plan: "free" | "pro" }>() .refs(["api"]) .meter("api/call", ["route"]) .getRate(async (meter, _dims, ctx, _at) => ({ rate: free, usedSoFar: mem.store.used(ctx.accountId, meter), // this month's calls limit: ctx.plan === "free" ? 10 : undefined, // the same period's cap; 0 = not in the plan })) .commit(mem.commit);
const ctx = { accountId: "acme", plan: "free" as const };await metering.observe("api/call", { route: "/a" }, 9, { type: "api", id: "1" }, ctx); // ok: 9 of 10const r = await metering.observe("api/call", { route: "/a" }, 4, { type: "api", id: "2" }, ctx);// → { ok: false, reason: "quota_exceeded", meter: "api/call",// cause: { meter: "api/call", limit: 10, used: 9, requested: 4 } } nothing written- Ya hep ya hiç. 9/10 iken gelen 4’lük istek tamamen reddedilir. Kısmi kullanım uygulamanın kararıdır:
cause.limit − cause.usedkalanı söyler. - Sayaç başına. Aynı sayacın satırları, boyutları farklı olsa da toplanır.
limit: 0= bu sayaç planda yok (no_price’tan farklı: fiyat var, hak yok). - Havuzlar. Havuz sayacının
limit’i onu besleyen her kanalı sınırlar (“SMS + WhatsApp toplam 1.000, sonra dur”). - Atomik. Kütüphanenin ön kontrolü eşzamanlı iki isteği göremez: ikisi de 999’u okur. Bu yüzden limit usage satırına yazılır ve adapter onu
commit’in transaction’ında yeniden kontrol eder, aşarsaQuotaExceededfırlatır (adapter sözleşmesi). - Hold.
holdveextendayrılan miktarı kontrol eder;captureasla reddedilmez (gönderilmiş bir SMS’in kaydı reddedilemez). - Koltuklar değil. Sayaçlar yalnızca artar. Koltuk gibi inip çıkan seviyelerin limiti uygulamadadır (A108):
export class SeatRegistry { #seats = new Map<string, number>(); /** Level changes, kept for `integrate()` at cycle close. */ readonly samples = new Map<string, Sample[]>();
constructor(private purchased: (accountId: string) => number) {}
current(accountId: string): number { return this.#seats.get(accountId) ?? 0; }
/** All or nothing: 9 of 10 used and 4 requested → refused, `remaining: 1` tells the UI what fits. */ add(accountId: string, count: number, at: number): AddSeats { // in production: one DB transaction (SELECT … FOR UPDATE, or a Durable Object per account) const current = this.current(accountId); const purchased = this.purchased(accountId); if (current + count > purchased) return { ok: false, reason: "seat_limit", purchased, current, remaining: purchased - current }; return this.#set(accountId, current + count, at); }
remove(accountId: string, count: number, at: number): AddSeats { return this.#set(accountId, Math.max(0, this.current(accountId) - count), at); }
#set(accountId: string, seats: number, at: number): AddSeats { this.#seats.set(accountId, seats); const s = this.samples.get(accountId) ?? []; s.push({ at, value: seats }); this.samples.set(accountId, s); return { ok: true, seats }; }}