once-kernel
Advanced tools
| /** | ||
| * Find duplicate effects that ALREADY happened. | ||
| * | ||
| * Every other part of this library prevents a future loss, which means asking | ||
| * someone who has never been burned to restructure their code against a bug | ||
| * they cannot see. This module inverts that: point it at records you already | ||
| * have — a payments table, a send log, a CSV export — and it tells you what | ||
| * fired twice last month. | ||
| * | ||
| * It requires adopting nothing. There is no store to install and no call site | ||
| * to change. That is deliberate: the answer is worth money before the fix is. | ||
| * | ||
| * The hard part is not finding equal rows. It is deciding what "the same | ||
| * operation" means when nobody was tracking one — and being honest about the | ||
| * cases where we are guessing. Every result carries the reason it was flagged | ||
| * and a confidence, because a wrong duplicate accusation sends someone chasing | ||
| * a refund that never happened. | ||
| */ | ||
| export interface EffectRecord { | ||
| /** Anything that identifies the row for a human: an id, a URL, a row number. */ | ||
| id: string; | ||
| /** When it happened. Epoch ms, or anything Date can parse. */ | ||
| at: number | string | Date; | ||
| /** | ||
| * The fields that define WHAT was done. Two records with the same subject are | ||
| * the same operation — so leave out timestamps, trace ids, retry counters and | ||
| * anything else that differs between attempts of one operation. | ||
| */ | ||
| subject: Record<string, unknown>; | ||
| /** Optional: what it cost. Used to total the exposure. */ | ||
| amount?: number; | ||
| /** Optional: an idempotency key, if the system had one. */ | ||
| key?: string; | ||
| } | ||
| export type DuplicateReason = "same-key" | "identical-subject-within-window" | "identical-subject"; | ||
| export interface DuplicateGroup { | ||
| reason: DuplicateReason; | ||
| /** How much to trust it. Explained in `note`. */ | ||
| confidence: "high" | "medium" | "low"; | ||
| note: string; | ||
| subjectHash: string; | ||
| records: EffectRecord[]; | ||
| /** Extra occurrences beyond the first — the ones that should not exist. */ | ||
| duplicateCount: number; | ||
| /** Cost of those extra occurrences, if amounts were supplied. */ | ||
| duplicateAmount?: number; | ||
| /** Milliseconds between first and last occurrence. */ | ||
| spanMs: number; | ||
| } | ||
| export interface AuditReport { | ||
| recordsExamined: number; | ||
| groups: DuplicateGroup[]; | ||
| totalDuplicates: number; | ||
| totalDuplicateAmount?: number; | ||
| /** Stated in the report itself so a reader cannot mistake it for certainty. */ | ||
| caveats: string[]; | ||
| } | ||
| export interface AuditOptions { | ||
| /** | ||
| * Two identical subjects further apart than this are treated as a deliberate | ||
| * repeat rather than a duplicate. Default 24h. | ||
| * | ||
| * This is the judgement call of the whole module. A subscription charged on | ||
| * the 1st of each month is identical and legitimate; the same charge twice in | ||
| * ten seconds is not. Outside the window we still report, at low confidence, | ||
| * clearly labelled — because sometimes it IS a duplicate, and silently hiding | ||
| * it would be the worse error. | ||
| */ | ||
| windowMs?: number; | ||
| /** Ignore groups whose duplicates are worth less than this. */ | ||
| minAmount?: number; | ||
| } | ||
| /** | ||
| * Group records by what they did, and report which groups happened more than | ||
| * once. Pure — no I/O, no store, nothing to install. | ||
| */ | ||
| export declare function findDuplicates(records: EffectRecord[], opts?: AuditOptions): AuditReport; | ||
| /** Render a report for a human. Money first, uncertainty stated. */ | ||
| export declare function formatAuditReport(r: AuditReport): string; | ||
| //# sourceMappingURL=audit.d.ts.map |
| {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,8DAA8D;IAC9D,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3B;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,eAAe,GACvB,UAAU,GACV,iCAAiC,GACjC,mBAAmB,CAAC;AAExB,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,eAAe,CAAC;IACxB,iDAAiD;IACjD,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,2EAA2E;IAC3E,cAAc,EAAE,MAAM,CAAC;IACvB,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sDAAsD;IACtD,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,cAAc,EAAE,CAAC;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,+EAA+E;IAC/E,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EAAE,EACvB,IAAI,GAAE,YAAiB,GACtB,WAAW,CAoIb;AAED,oEAAoE;AACpE,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,WAAW,GAAG,MAAM,CAyBxD"} |
+143
| /** | ||
| * Find duplicate effects that ALREADY happened. | ||
| * | ||
| * Every other part of this library prevents a future loss, which means asking | ||
| * someone who has never been burned to restructure their code against a bug | ||
| * they cannot see. This module inverts that: point it at records you already | ||
| * have — a payments table, a send log, a CSV export — and it tells you what | ||
| * fired twice last month. | ||
| * | ||
| * It requires adopting nothing. There is no store to install and no call site | ||
| * to change. That is deliberate: the answer is worth money before the fix is. | ||
| * | ||
| * The hard part is not finding equal rows. It is deciding what "the same | ||
| * operation" means when nobody was tracking one — and being honest about the | ||
| * cases where we are guessing. Every result carries the reason it was flagged | ||
| * and a confidence, because a wrong duplicate accusation sends someone chasing | ||
| * a refund that never happened. | ||
| */ | ||
| import { payloadHashHex } from "./canonical.js"; | ||
| const ms = (v) => v instanceof Date ? v.getTime() : typeof v === "number" ? v : Date.parse(v); | ||
| /** | ||
| * Group records by what they did, and report which groups happened more than | ||
| * once. Pure — no I/O, no store, nothing to install. | ||
| */ | ||
| export function findDuplicates(records, opts = {}) { | ||
| const windowMs = opts.windowMs ?? 24 * 60 * 60 * 1000; | ||
| // An explicit key, where one exists, beats any inference we could make. | ||
| const byKey = new Map(); | ||
| const bySubject = new Map(); | ||
| for (const r of records) { | ||
| if (r.key) { | ||
| const g = byKey.get(r.key) ?? []; | ||
| g.push(r); | ||
| byKey.set(r.key, g); | ||
| } | ||
| else { | ||
| const h = payloadHashHex(r.subject); | ||
| const g = bySubject.get(h) ?? []; | ||
| g.push(r); | ||
| bySubject.set(h, g); | ||
| } | ||
| } | ||
| const groups = []; | ||
| const make = (recs, reason, confidence, note, hash) => { | ||
| const sorted = [...recs].sort((a, b) => ms(a.at) - ms(b.at)); | ||
| const extra = sorted.slice(1); | ||
| const amounts = extra.map((r) => r.amount).filter((a) => typeof a === "number"); | ||
| return { | ||
| reason, | ||
| confidence, | ||
| note, | ||
| subjectHash: hash, | ||
| records: sorted, | ||
| duplicateCount: extra.length, | ||
| duplicateAmount: amounts.length ? amounts.reduce((a, b) => a + b, 0) : undefined, | ||
| spanMs: ms(sorted[sorted.length - 1].at) - ms(sorted[0].at), | ||
| }; | ||
| }; | ||
| for (const [key, recs] of byKey) { | ||
| if (recs.length < 2) | ||
| continue; | ||
| groups.push(make(recs, "same-key", "high", `The system's own idempotency key ${JSON.stringify(key)} appears on ` + | ||
| `${recs.length} separate effects. If the key means what it says, this is a duplicate.`, key)); | ||
| } | ||
| for (const [hash, recs] of bySubject) { | ||
| if (recs.length < 2) | ||
| continue; | ||
| const sorted = [...recs].sort((a, b) => ms(a.at) - ms(b.at)); | ||
| // Split the group at gaps larger than the window, so a monthly charge | ||
| // becomes twelve legitimate singles rather than one twelve-fold duplicate. | ||
| let run = [sorted[0]]; | ||
| const runs = []; | ||
| for (let i = 1; i < sorted.length; i++) { | ||
| if (ms(sorted[i].at) - ms(sorted[i - 1].at) <= windowMs) | ||
| run.push(sorted[i]); | ||
| else { | ||
| runs.push(run); | ||
| run = [sorted[i]]; | ||
| } | ||
| } | ||
| runs.push(run); | ||
| const clustered = runs.filter((r) => r.length >= 2); | ||
| for (const r of clustered) { | ||
| groups.push(make(r, "identical-subject-within-window", "medium", `${r.length} effects with an identical subject inside ${Math.round(windowMs / 3600000)}h. ` + | ||
| `No idempotency key was recorded, so this is inferred from the payload — ` + | ||
| `check that nothing distinguishing was left out of \`subject\`.`, hash)); | ||
| } | ||
| // Repeats spread beyond the window: reported, but flagged low, because a | ||
| // recurring charge looks exactly like this and usually is not a bug. | ||
| if (clustered.length === 0 && runs.length >= 2) { | ||
| groups.push(make(sorted, "identical-subject", "low", `${sorted.length} identical effects spread over time. This is what a legitimate ` + | ||
| `recurring charge looks like — reported so you can rule it out, not because it is wrong.`, hash)); | ||
| } | ||
| } | ||
| const filtered = opts.minAmount === undefined | ||
| ? groups | ||
| : groups.filter((g) => (g.duplicateAmount ?? 0) >= opts.minAmount); | ||
| const rank = { high: 0, medium: 1, low: 2 }; | ||
| filtered.sort((a, b) => rank[a.confidence] - rank[b.confidence] || | ||
| (b.duplicateAmount ?? 0) - (a.duplicateAmount ?? 0) || | ||
| b.duplicateCount - a.duplicateCount); | ||
| const amounts = filtered | ||
| .map((g) => g.duplicateAmount) | ||
| .filter((a) => typeof a === "number"); | ||
| return { | ||
| recordsExamined: records.length, | ||
| groups: filtered, | ||
| totalDuplicates: filtered.reduce((n, g) => n + g.duplicateCount, 0), | ||
| totalDuplicateAmount: amounts.length ? amounts.reduce((a, b) => a + b, 0) : undefined, | ||
| caveats: [ | ||
| "A duplicate here means two records described the same operation — not that money definitely moved twice. Confirm against the provider before acting.", | ||
| "Anything left out of `subject` makes two different operations look identical. Anything varying that was left IN hides real duplicates.", | ||
| "Low-confidence groups are usually legitimate recurring activity. They are shown so you can rule them out.", | ||
| ], | ||
| }; | ||
| } | ||
| /** Render a report for a human. Money first, uncertainty stated. */ | ||
| export function formatAuditReport(r) { | ||
| const out = []; | ||
| out.push(`Examined ${r.recordsExamined} records.`); | ||
| if (r.groups.length === 0) { | ||
| out.push("No duplicate effects found."); | ||
| out.push("That is this check finding nothing — not proof the system is safe."); | ||
| return out.join("\n"); | ||
| } | ||
| out.push(`Found ${r.totalDuplicates} duplicate occurrence(s) across ${r.groups.length} group(s)` + | ||
| (r.totalDuplicateAmount !== undefined | ||
| ? `, worth ${r.totalDuplicateAmount} in repeated effects.` | ||
| : ".")); | ||
| out.push(""); | ||
| for (const g of r.groups.slice(0, 20)) { | ||
| const amt = g.duplicateAmount !== undefined ? ` (${g.duplicateAmount})` : ""; | ||
| out.push(`[${g.confidence}] ${g.duplicateCount} extra${amt} — ${g.reason}`); | ||
| out.push(` ${g.note}`); | ||
| out.push(` ids: ${g.records.map((x) => x.id).join(", ")}`); | ||
| out.push(""); | ||
| } | ||
| out.push("Before acting:"); | ||
| for (const c of r.caveats) | ||
| out.push(` · ${c}`); | ||
| return out.join("\n"); | ||
| } | ||
| //# sourceMappingURL=audit.js.map |
| {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"} |
| /** | ||
| * A spending ceiling at the same choke point as the fence. | ||
| * | ||
| * If you are already intercepting every money-moving call in order to | ||
| * deduplicate it, you are one line away from bounding what it may spend. The | ||
| * insertion point is identical; only the question differs. That matters | ||
| * commercially as much as technically: it is a second reason to install, aimed | ||
| * at exactly the people who care about the first. | ||
| * | ||
| * The failure this prevents is not a duplicate. It is an agent doing something | ||
| * individually reasonable, repeatedly, until the money is gone. No single call | ||
| * looks wrong — the ceiling is the only thing that sees the pattern. | ||
| * | ||
| * Two decisions worth stating outright: | ||
| * | ||
| * 1. **It refuses rather than warns.** The fence's guard warns because | ||
| * blocking an effect you cannot prove is unsafe would be worse than the | ||
| * bug. A ceiling is different: the caller set the number, so enforcing it | ||
| * is doing what they asked. | ||
| * | ||
| * 2. **It reserves before the call and settles after.** Checking a total | ||
| * afterwards is a time-of-check-to-time-of-use race — ten concurrent calls | ||
| * each see room and every one proceeds. Reserve first, and the tenth is | ||
| * refused while the ninth is still in flight. | ||
| */ | ||
| export interface BudgetWindow { | ||
| /** Ceiling for this window, in whatever unit you spend in. */ | ||
| limit: number; | ||
| /** Window length in ms. Rolling, not calendar-aligned. */ | ||
| windowMs: number; | ||
| } | ||
| export interface SpendAttempt { | ||
| /** What is being spent on. Surfaced in the refusal. */ | ||
| what: string; | ||
| amount: number; | ||
| context?: Record<string, string | number>; | ||
| } | ||
| export declare class BudgetExceeded extends Error { | ||
| readonly limit: number; | ||
| readonly spent: number; | ||
| readonly attempted: number; | ||
| readonly windowMs: number; | ||
| readonly resetsInMs: number; | ||
| constructor(a: { | ||
| what: string; | ||
| limit: number; | ||
| spent: number; | ||
| attempted: number; | ||
| windowMs: number; | ||
| resetsInMs: number; | ||
| }); | ||
| } | ||
| export declare class SpendLimiter { | ||
| private readonly limit; | ||
| private readonly windowMs; | ||
| private entries; | ||
| constructor(w: BudgetWindow); | ||
| private prune; | ||
| /** Committed spend in the current window — reservations included. */ | ||
| spent(now?: number): number; | ||
| remaining(now?: number): number; | ||
| /** | ||
| * Reserve headroom BEFORE the call. Throws if it would breach the ceiling. | ||
| * | ||
| * Returns a handle: `settle()` if it went through, `release()` if it did not. | ||
| * A reservation that is never resolved simply expires with the window, so a | ||
| * crash costs you conservatism rather than a stuck ceiling. | ||
| */ | ||
| reserve(a: SpendAttempt): { | ||
| settle: () => void; | ||
| release: () => void; | ||
| }; | ||
| /** | ||
| * Run `fn` only if it fits under the ceiling. Releases the reservation if it | ||
| * throws, so a failed call does not consume budget it never spent. | ||
| */ | ||
| run<T>(a: SpendAttempt, fn: () => T | Promise<T>): Promise<T>; | ||
| /** Test-only: forget all spend. */ | ||
| reset(): void; | ||
| } | ||
| //# sourceMappingURL=budget.d.ts.map |
| {"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,MAAM,WAAW,YAAY;IAC3B,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,YAAY;IAC3B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CAC3C;AAED,qBAAa,cAAe,SAAQ,KAAK;IACvC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,CAAC,EAAE;QACb,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;KACpB;CAeF;AAQD,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,OAAO,CAAe;gBAElB,CAAC,EAAE,YAAY;IAO3B,OAAO,CAAC,KAAK;IAKb,qEAAqE;IACrE,KAAK,CAAC,GAAG,SAAa,GAAG,MAAM;IAK/B,SAAS,CAAC,GAAG,SAAa,GAAG,MAAM;IAInC;;;;;;OAMG;IACH,OAAO,CAAC,CAAC,EAAE,YAAY,GAAG;QAAE,MAAM,EAAE,MAAM,IAAI,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE;IA+BrE;;;OAGG;IACG,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,YAAY,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAYnE,mCAAmC;IACnC,KAAK,IAAI,IAAI;CAGd"} |
+127
| /** | ||
| * A spending ceiling at the same choke point as the fence. | ||
| * | ||
| * If you are already intercepting every money-moving call in order to | ||
| * deduplicate it, you are one line away from bounding what it may spend. The | ||
| * insertion point is identical; only the question differs. That matters | ||
| * commercially as much as technically: it is a second reason to install, aimed | ||
| * at exactly the people who care about the first. | ||
| * | ||
| * The failure this prevents is not a duplicate. It is an agent doing something | ||
| * individually reasonable, repeatedly, until the money is gone. No single call | ||
| * looks wrong — the ceiling is the only thing that sees the pattern. | ||
| * | ||
| * Two decisions worth stating outright: | ||
| * | ||
| * 1. **It refuses rather than warns.** The fence's guard warns because | ||
| * blocking an effect you cannot prove is unsafe would be worse than the | ||
| * bug. A ceiling is different: the caller set the number, so enforcing it | ||
| * is doing what they asked. | ||
| * | ||
| * 2. **It reserves before the call and settles after.** Checking a total | ||
| * afterwards is a time-of-check-to-time-of-use race — ten concurrent calls | ||
| * each see room and every one proceeds. Reserve first, and the tenth is | ||
| * refused while the ninth is still in flight. | ||
| */ | ||
| export class BudgetExceeded extends Error { | ||
| limit; | ||
| spent; | ||
| attempted; | ||
| windowMs; | ||
| resetsInMs; | ||
| constructor(a) { | ||
| super(`refusing "${a.what}": ${a.attempted} would take spending to ` + | ||
| `${a.spent + a.attempted} against a ceiling of ${a.limit} per ` + | ||
| `${Math.round(a.windowMs / 60000)} minutes. ` + | ||
| `${a.spent} already committed; the window frees up in ` + | ||
| `${Math.round(a.resetsInMs / 1000)}s.`); | ||
| this.name = "BudgetExceeded"; | ||
| this.limit = a.limit; | ||
| this.spent = a.spent; | ||
| this.attempted = a.attempted; | ||
| this.windowMs = a.windowMs; | ||
| this.resetsInMs = a.resetsInMs; | ||
| } | ||
| } | ||
| export class SpendLimiter { | ||
| limit; | ||
| windowMs; | ||
| entries = []; | ||
| constructor(w) { | ||
| if (!(w.limit > 0)) | ||
| throw new Error("budget limit must be positive"); | ||
| if (!(w.windowMs > 0)) | ||
| throw new Error("budget window must be positive"); | ||
| this.limit = w.limit; | ||
| this.windowMs = w.windowMs; | ||
| } | ||
| prune(now = Date.now()) { | ||
| const cutoff = now - this.windowMs; | ||
| this.entries = this.entries.filter((e) => e.at > cutoff); | ||
| } | ||
| /** Committed spend in the current window — reservations included. */ | ||
| spent(now = Date.now()) { | ||
| this.prune(now); | ||
| return this.entries.reduce((n, e) => n + e.amount, 0); | ||
| } | ||
| remaining(now = Date.now()) { | ||
| return Math.max(0, this.limit - this.spent(now)); | ||
| } | ||
| /** | ||
| * Reserve headroom BEFORE the call. Throws if it would breach the ceiling. | ||
| * | ||
| * Returns a handle: `settle()` if it went through, `release()` if it did not. | ||
| * A reservation that is never resolved simply expires with the window, so a | ||
| * crash costs you conservatism rather than a stuck ceiling. | ||
| */ | ||
| reserve(a) { | ||
| if (!(a.amount >= 0)) | ||
| throw new Error("spend amount must be >= 0"); | ||
| const now = Date.now(); | ||
| const spent = this.spent(now); | ||
| if (spent + a.amount > this.limit) { | ||
| const oldest = this.entries[0]?.at ?? now; | ||
| throw new BudgetExceeded({ | ||
| what: a.what, | ||
| limit: this.limit, | ||
| spent, | ||
| attempted: a.amount, | ||
| windowMs: this.windowMs, | ||
| resetsInMs: Math.max(0, oldest + this.windowMs - now), | ||
| }); | ||
| } | ||
| const entry = { at: now, amount: a.amount, settled: false }; | ||
| this.entries.push(entry); | ||
| return { | ||
| settle: () => { | ||
| entry.settled = true; | ||
| }, | ||
| release: () => { | ||
| const i = this.entries.indexOf(entry); | ||
| if (i >= 0 && !entry.settled) | ||
| this.entries.splice(i, 1); | ||
| }, | ||
| }; | ||
| } | ||
| /** | ||
| * Run `fn` only if it fits under the ceiling. Releases the reservation if it | ||
| * throws, so a failed call does not consume budget it never spent. | ||
| */ | ||
| async run(a, fn) { | ||
| const h = this.reserve(a); | ||
| try { | ||
| const out = await fn(); | ||
| h.settle(); | ||
| return out; | ||
| } | ||
| catch (e) { | ||
| h.release(); | ||
| throw e; | ||
| } | ||
| } | ||
| /** Test-only: forget all spend. */ | ||
| reset() { | ||
| this.entries = []; | ||
| } | ||
| } | ||
| //# sourceMappingURL=budget.js.map |
| {"version":3,"file":"budget.js","sourceRoot":"","sources":["../src/budget.ts"],"names":[],"mappings":"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"} |
| /** | ||
| * `guardEffect` — shout at the moment an irreversible effect is about to run | ||
| * without a dedup key. | ||
| * | ||
| * The idea came from a maintainer reviewing one of our findings, and it is the | ||
| * sharpest thing anyone has said about this problem: a caller-addressable | ||
| * idempotency key only protects you if the *other side* honours it, and today a | ||
| * caller has no way to know whether it does. So the safe default for any client | ||
| * is to assume retries are unsafe — and almost nobody does, because nothing | ||
| * tells them. | ||
| * | ||
| * Every other part of this library helps someone who already knows they have | ||
| * the problem. This part is for the far larger group who do not. It is the | ||
| * difference between a tool you reach for and a tool that reaches you. | ||
| * | ||
| * It deliberately does NOT prevent the call. A library that silently blocked | ||
| * effects would be worse than the bug: teams would rip it out. It raises a | ||
| * flag at exactly the moment a supervisor wants one, and gets out of the way. | ||
| */ | ||
| export type Reversibility = "irreversible" | "reversible" | "unknown"; | ||
| export interface EffectDescriptor { | ||
| /** What this does, in the caller's words. Used in the warning. */ | ||
| what: string; | ||
| /** Whether undoing it is possible. Money and messages are irreversible. */ | ||
| reversibility?: Reversibility; | ||
| /** The dedup key, if the caller has one. Its ABSENCE is the whole point. */ | ||
| idempotencyKey?: string; | ||
| /** | ||
| * Whether the endpoint has *stated* that it deduplicates. | ||
| * | ||
| * Not whether you believe it does — whether it said so. An x402 challenge | ||
| * carrying an `idempotency` block, a documented `Idempotency-Key` header, a | ||
| * provider that publishes the semantics. If you are guessing, this is | ||
| * `"unknown"`, and that is the case worth warning about. | ||
| */ | ||
| endpointDeduplicates?: "declared" | "declared-absent" | "unknown"; | ||
| /** Free-form, surfaced in the warning: endpoint, amount, recipient. */ | ||
| context?: Record<string, string | number>; | ||
| } | ||
| export interface GuardOptions { | ||
| /** Where warnings go. Defaults to console.warn. */ | ||
| onWarn?: (warning: EffectWarning) => void; | ||
| /** | ||
| * Throw instead of warning. Off by default and it should stay off in most | ||
| * systems — see the note at the top about libraries that block effects. | ||
| */ | ||
| strict?: boolean; | ||
| /** Warn at most once per distinct reason+what. Default true. */ | ||
| dedupeWarnings?: boolean; | ||
| } | ||
| export interface EffectWarning { | ||
| reason: "no-key-no-disclosure" | "no-key" | "no-disclosure"; | ||
| what: string; | ||
| message: string; | ||
| context?: Record<string, string | number>; | ||
| } | ||
| export declare class UnguardedEffectError extends Error { | ||
| readonly warning: EffectWarning; | ||
| constructor(w: EffectWarning); | ||
| } | ||
| /** | ||
| * Check an effect before running it. Returns the warning it raised, or null. | ||
| * | ||
| * ```ts | ||
| * guardEffect({ | ||
| * what: "charge card", | ||
| * reversibility: "irreversible", | ||
| * idempotencyKey: paymentId, // undefined is the case that warns | ||
| * endpointDeduplicates: "unknown", | ||
| * context: { amount: "49.00", to: "acct_123" }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export declare function guardEffect(d: EffectDescriptor, opts?: GuardOptions): EffectWarning | null; | ||
| /** Wrap a function so the check runs immediately before it. */ | ||
| export declare function withGuard<A extends unknown[], R>(d: EffectDescriptor, fn: (...args: A) => R, opts?: GuardOptions): (...args: A) => R; | ||
| /** Test-only: forget which warnings have already been shown. */ | ||
| export declare function resetGuardWarnings(): void; | ||
| //# sourceMappingURL=guard.d.ts.map |
| {"version":3,"file":"guard.d.ts","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,SAAS,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,4EAA4E;IAC5E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,UAAU,GAAG,iBAAiB,GAAG,SAAS,CAAC;IAClE,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gEAAgE;IAChE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,sBAAsB,GAAG,QAAQ,GAAG,eAAe,CAAC;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAC;CAC3C;AAED,qBAAa,oBAAqB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;gBACpB,CAAC,EAAE,aAAa;CAK7B;AA+CD;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,CAAC,EAAE,gBAAgB,EACnB,IAAI,GAAE,YAAiB,GACtB,aAAa,GAAG,IAAI,CAatB;AAED,+DAA+D;AAC/D,wBAAgB,SAAS,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAC9C,CAAC,EAAE,gBAAgB,EACnB,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,EACrB,IAAI,CAAC,EAAE,YAAY,GAClB,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,CAKnB;AAED,gEAAgE;AAChE,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"} |
+103
| /** | ||
| * `guardEffect` — shout at the moment an irreversible effect is about to run | ||
| * without a dedup key. | ||
| * | ||
| * The idea came from a maintainer reviewing one of our findings, and it is the | ||
| * sharpest thing anyone has said about this problem: a caller-addressable | ||
| * idempotency key only protects you if the *other side* honours it, and today a | ||
| * caller has no way to know whether it does. So the safe default for any client | ||
| * is to assume retries are unsafe — and almost nobody does, because nothing | ||
| * tells them. | ||
| * | ||
| * Every other part of this library helps someone who already knows they have | ||
| * the problem. This part is for the far larger group who do not. It is the | ||
| * difference between a tool you reach for and a tool that reaches you. | ||
| * | ||
| * It deliberately does NOT prevent the call. A library that silently blocked | ||
| * effects would be worse than the bug: teams would rip it out. It raises a | ||
| * flag at exactly the moment a supervisor wants one, and gets out of the way. | ||
| */ | ||
| export class UnguardedEffectError extends Error { | ||
| warning; | ||
| constructor(w) { | ||
| super(w.message); | ||
| this.name = "UnguardedEffectError"; | ||
| this.warning = w; | ||
| } | ||
| } | ||
| const seen = new Set(); | ||
| function build(d) { | ||
| const irreversible = (d.reversibility ?? "unknown") !== "reversible"; | ||
| if (!irreversible) | ||
| return null; | ||
| const hasKey = typeof d.idempotencyKey === "string" && d.idempotencyKey.length > 0; | ||
| const disclosure = d.endpointDeduplicates ?? "unknown"; | ||
| const endpointSaysYes = disclosure === "declared"; | ||
| // Both present: the caller controls dedup and the endpoint honours it. | ||
| if (hasKey && endpointSaysYes) | ||
| return null; | ||
| // Nothing to say if the caller has a key AND we simply don't know about the | ||
| // endpoint — that is the normal, reasonable case and warning on it would | ||
| // make this noise, which is how warnings get switched off. | ||
| if (hasKey && disclosure === "unknown") | ||
| return null; | ||
| const reason = !hasKey && !endpointSaysYes ? "no-key-no-disclosure" : !hasKey ? "no-key" : "no-disclosure"; | ||
| const lines = [ | ||
| `About to run an irreversible effect without a dedup guarantee: ${d.what}`, | ||
| ]; | ||
| if (!hasKey) { | ||
| lines.push(` · no idempotency key was supplied, so a retry cannot be recognised as the same operation`); | ||
| } | ||
| if (disclosure === "declared-absent") { | ||
| lines.push(` · the endpoint has stated that it does NOT deduplicate`); | ||
| } | ||
| else if (!endpointSaysYes) { | ||
| lines.push(` · the endpoint has not stated whether it deduplicates, so assume it does not`); | ||
| } | ||
| lines.push(` If this call is retried after a timeout, the effect may happen twice and look like one success.`); | ||
| if (d.context) { | ||
| for (const [k, v] of Object.entries(d.context)) | ||
| lines.push(` ${k}: ${v}`); | ||
| } | ||
| return { reason, what: d.what, message: lines.join("\n"), context: d.context }; | ||
| } | ||
| /** | ||
| * Check an effect before running it. Returns the warning it raised, or null. | ||
| * | ||
| * ```ts | ||
| * guardEffect({ | ||
| * what: "charge card", | ||
| * reversibility: "irreversible", | ||
| * idempotencyKey: paymentId, // undefined is the case that warns | ||
| * endpointDeduplicates: "unknown", | ||
| * context: { amount: "49.00", to: "acct_123" }, | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export function guardEffect(d, opts = {}) { | ||
| const w = build(d); | ||
| if (!w) | ||
| return null; | ||
| if (opts.dedupeWarnings !== false) { | ||
| const k = `${w.reason}:${w.what}`; | ||
| if (seen.has(k)) | ||
| return w; | ||
| seen.add(k); | ||
| } | ||
| if (opts.strict) | ||
| throw new UnguardedEffectError(w); | ||
| (opts.onWarn ?? ((x) => console.warn(`[once] ${x.message}`)))(w); | ||
| return w; | ||
| } | ||
| /** Wrap a function so the check runs immediately before it. */ | ||
| export function withGuard(d, fn, opts) { | ||
| return (...args) => { | ||
| guardEffect(d, opts); | ||
| return fn(...args); | ||
| }; | ||
| } | ||
| /** Test-only: forget which warnings have already been shown. */ | ||
| export function resetGuardWarnings() { | ||
| seen.clear(); | ||
| } | ||
| //# sourceMappingURL=guard.js.map |
| {"version":3,"file":"guard.js","sourceRoot":"","sources":["../src/guard.ts"],"names":[],"mappings":"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"} |
+39
-0
@@ -62,2 +62,15 @@ /** | ||
| export declare function normalizeKey(key: string): string; | ||
| /** Evidence that an operation ran, and what it ran on. */ | ||
| export interface Receipt { | ||
| key: string; | ||
| status: Status; | ||
| /** SHA-256 of the RFC 8785 canonical payload — proves WHAT ran. */ | ||
| payloadHash: string; | ||
| /** 1 normally. Higher means a worker died mid-flight and another took over. */ | ||
| generation: number; | ||
| firstSeenAt: string; | ||
| settledAt: string; | ||
| result?: unknown; | ||
| error?: string; | ||
| } | ||
| export interface Outcome { | ||
@@ -122,2 +135,28 @@ /** True for exactly one caller: the one that must run the effect. */ | ||
| /** | ||
| * Has this operation already happened? Answers WITHOUT running anything. | ||
| * | ||
| * Until now the only way to find out was to attempt the operation, which is | ||
| * useless during reconciliation: "did we already refund order 4471?" is a | ||
| * question ops and finance ask daily, and attempting a refund to find out is | ||
| * not an acceptable way to answer it. | ||
| * | ||
| * Returns `"unknown"` when there is no record — which is genuinely different | ||
| * from "it did not happen". A record can expire, or the operation may predate | ||
| * the fence. Saying "no" there would be a lie. | ||
| */ | ||
| status(key: string): Promise<{ | ||
| state: "completed" | "failed" | "in_progress" | "unknown"; | ||
| record?: Record_; | ||
| }>; | ||
| /** | ||
| * A receipt proving what happened for this key, or null if nothing is known. | ||
| * | ||
| * For anyone in payments or regulated work, evidence is worth more than | ||
| * prevention: "show me this refund was issued exactly once" is an audit | ||
| * question with money attached to the answer. The payload hash makes the | ||
| * receipt content-addressed — you can prove WHAT ran, not merely that | ||
| * something did. | ||
| */ | ||
| receipt(key: string): Promise<Receipt | null>; | ||
| /** | ||
| * begin → fn() → complete/fail, with concurrent callers coalesced onto the | ||
@@ -124,0 +163,0 @@ * single execution rather than racing or erroring. |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"kernel.d.ts","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAEpG,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,eAAe,OAAQ,CAAC;AAErC,MAAM,MAAM,MAAM,GAAG,aAAa,GAAG,WAAW,GAAG,QAAQ,CAAC;AAE5D,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAI5C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;gBAEnB,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM;CAWrE;AAED,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;gBAElB,GAAG,EAAE,MAAM,EAAE,YAAY,SAAM;CAM5C;AAED,qBAAa,cAAe,SAAQ,KAAK;gBAC3B,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAOxC;AAED,qBAAa,WAAY,SAAQ,KAAK;gBACxB,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM;CAIpC;AAED,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED,0DAA0D;AAC1D,MAAM,WAAW,OAAO;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,mEAAmE;IACnE,WAAW,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,qEAAqE;IACrE,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,KAAK;IACpB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC/C,0EAA0E;IAC1E,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC7D,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACpG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/F,iEAAiE;IACjE,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC5E,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtF;AAcD;;;;;;;GAOG;AACH,qBAAa,WAAY,YAAW,KAAK;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IAE7C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAS9C,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAU5D,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM;IAOhF,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO;IAa3E,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAQ3E,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM;CAOxE;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,IAAI;IACf,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAS;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;gBAE5B,IAAI,GAAE,WAAgB;IAOlC,4DAA4D;IACtD,KAAK,CACT,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,EAChB,IAAI,GAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GAChD,OAAO,CAAC,OAAO,CAAC;YAmCL,MAAM;IAsBd,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAO7F,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,UAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAI/F;;;;;;;;;;;OAWG;IACG,MAAM,CACV,GAAG,EAAE,MAAM,GACV,OAAO,CAAC;QAAE,KAAK,EAAE,WAAW,GAAG,QAAQ,GAAG,aAAa,GAAG,SAAS,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAM3F;;;;;;;;OAQG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAgBnD;;;OAGG;IACG,GAAG,CAAC,CAAC,EACT,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EACxB,IAAI,GAAE;QACJ,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,MAAM,CAAC,EAAE,MAAM,CAAC;KACZ,GACL,OAAO,CAAC,CAAC,CAAC;CAsCd"} |
+43
-0
@@ -233,2 +233,45 @@ /** | ||
| /** | ||
| * Has this operation already happened? Answers WITHOUT running anything. | ||
| * | ||
| * Until now the only way to find out was to attempt the operation, which is | ||
| * useless during reconciliation: "did we already refund order 4471?" is a | ||
| * question ops and finance ask daily, and attempting a refund to find out is | ||
| * not an acceptable way to answer it. | ||
| * | ||
| * Returns `"unknown"` when there is no record — which is genuinely different | ||
| * from "it did not happen". A record can expire, or the operation may predate | ||
| * the fence. Saying "no" there would be a lie. | ||
| */ | ||
| async status(key) { | ||
| const rec = await this.store.get(normalizeKey(key)); | ||
| if (!rec) | ||
| return { state: "unknown" }; | ||
| return { state: rec.status, record: rec }; | ||
| } | ||
| /** | ||
| * A receipt proving what happened for this key, or null if nothing is known. | ||
| * | ||
| * For anyone in payments or regulated work, evidence is worth more than | ||
| * prevention: "show me this refund was issued exactly once" is an audit | ||
| * question with money attached to the answer. The payload hash makes the | ||
| * receipt content-addressed — you can prove WHAT ran, not merely that | ||
| * something did. | ||
| */ | ||
| async receipt(key) { | ||
| const rec = await this.store.get(normalizeKey(key)); | ||
| if (!rec) | ||
| return null; | ||
| return { | ||
| key: rec.key, | ||
| status: rec.status, | ||
| payloadHash: rec.payloadHash, | ||
| /** Increments on every reclaim; >1 means a worker died and was replaced. */ | ||
| generation: rec.generation, | ||
| firstSeenAt: new Date(rec.createdAt * 1000).toISOString(), | ||
| settledAt: new Date(rec.updatedAt * 1000).toISOString(), | ||
| result: rec.result, | ||
| error: rec.error, | ||
| }; | ||
| } | ||
| /** | ||
| * begin → fn() → complete/fail, with concurrent callers coalesced onto the | ||
@@ -235,0 +278,0 @@ * single execution rather than racing or erroring. |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"kernel.js","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"kernel.js","sourceRoot":"","sources":["../src/kernel.ts"],"names":[],"mappings":"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"} |
+15
-3
| { | ||
| "name": "once-kernel", | ||
| "version": "0.1.0", | ||
| "description": "Idempotency kernel for side-effecting operations. 1,000 racing callers, exactly one execution — proven, not asserted.", | ||
| "version": "0.2.0", | ||
| "description": "Idempotency kernel for side-effecting operations. 1,000 racing callers, exactly one execution \u2014 proven, not asserted.", | ||
| "keywords": [ | ||
@@ -38,2 +38,14 @@ "idempotency", | ||
| "default": "./dist/stores/sqlite.js" | ||
| }, | ||
| "./guard": { | ||
| "types": "./dist/guard.d.ts", | ||
| "default": "./dist/guard.js" | ||
| }, | ||
| "./audit": { | ||
| "types": "./dist/audit.d.ts", | ||
| "default": "./dist/audit.js" | ||
| }, | ||
| "./budget": { | ||
| "types": "./dist/budget.d.ts", | ||
| "default": "./dist/budget.js" | ||
| } | ||
@@ -51,3 +63,3 @@ }, | ||
| "clean": "rm -rf dist", | ||
| "test": "node --experimental-strip-types --no-warnings --test test/canonical.test.ts test/kernel.test.ts test/store-conformance.test.ts", | ||
| "test": "node --experimental-strip-types --no-warnings --test test/canonical.test.ts test/kernel.test.ts test/store-conformance.test.ts test/guard.test.ts test/capabilities.test.ts test/readme.test.ts", | ||
| "test:storm": "node --experimental-strip-types --no-warnings --test test/storm.test.ts", | ||
@@ -54,0 +66,0 @@ "test:all": "npm run test && npm run test:storm", |
+194
-66
| # once-kernel | ||
| **Idempotency for side effects that cost money. 1,000 racing callers, exactly one execution — proven on every commit, not asserted in a README.** | ||
| **Stop side effects happening twice — and find the ones that already did.** | ||
@@ -9,2 +9,25 @@ ```bash | ||
| Zero runtime dependencies. No build step. Node 22.5+. | ||
| --- | ||
| ## The problem | ||
| An agent retries a call after a timeout. It doesn't know whether the first | ||
| attempt landed — the request reached the server, the work happened, the response | ||
| never came back. | ||
| Nothing failed loudly. It succeeded twice. | ||
| That's the easy half. The hard half is the worker that dies **between** reserving | ||
| a key and finishing the effect. An in-memory `Set` leaves that key claimed | ||
| forever and the effect never runs at all. Restart the process and the `Set` is | ||
| empty, so it runs twice. | ||
| --- | ||
| ## Four things this does | ||
| ### 1. Run something exactly once | ||
| ```ts | ||
@@ -16,6 +39,5 @@ import { Once } from "once-kernel"; | ||
| // Called once, twice, or by fifty racing workers — the card is charged once. | ||
| const receipt = await once.run( | ||
| `charge:${orderId}`, // the key | ||
| { amount: 4900, currency: "usd" }, // the payload — part of the identity | ||
| `charge:${orderId}`, // the key | ||
| { amount: 4900, currency: "usd" }, // the payload is part of the identity | ||
| () => stripe.charges.create({ amount: 4900, currency: "usd" }), | ||
@@ -25,63 +47,135 @@ ); | ||
| Every caller gets the same `receipt`. The charge happens once. | ||
| Fifty racing callers, one charge. Every caller gets the same `receipt`. | ||
| ### 2. Ask whether something already happened — without doing it | ||
| ```ts | ||
| const { state } = await once.status(`refund:${orderId}`); | ||
| // "completed" | "failed" | "in_progress" | "unknown" | ||
| ``` | ||
| "Did we already refund order 4471?" is a question ops asks daily. Until now the | ||
| only way to answer it was to attempt the refund. | ||
| `"unknown"` means *no record*, which is **not** the same as "it didn't happen" — | ||
| records expire, and operations can predate the fence. Saying "no" there would be | ||
| a lie somebody refunds a customer twice on. | ||
| ### 3. Prove what ran | ||
| ```ts | ||
| const r = await once.receipt(`charge:${orderId}`); | ||
| // { status, payloadHash, generation, firstSeenAt, settledAt, result } | ||
| ``` | ||
| `payloadHash` is a SHA-256 of the canonical payload, so a receipt proves **what** | ||
| ran, not merely that something did. `generation > 1` means a worker died | ||
| mid-flight and another took over — the visible trace of a crash you'd otherwise | ||
| never see. | ||
| For regulated or payments work, evidence is usually worth more than prevention. | ||
| ### 4. Find duplicates that already happened | ||
| No adoption required. Point it at records you already have. | ||
| ```ts | ||
| import { findDuplicates, formatAuditReport } from "once-kernel/audit"; | ||
| const report = findDuplicates( | ||
| rows.map((r) => ({ | ||
| id: r.id, | ||
| at: r.created_at, | ||
| subject: { customer: r.customer_id, amount: r.amount }, // what was DONE | ||
| amount: r.amount, | ||
| key: r.idempotency_key, // if the system had one | ||
| })), | ||
| ); | ||
| console.log(formatAuditReport(report)); | ||
| ``` | ||
| ``` | ||
| Examined 12,480 records. | ||
| Found 3 duplicate occurrence(s) across 2 group(s), worth 348 in repeated effects. | ||
| [high] 1 extra (49) — same-key | ||
| The system's own idempotency key "idem_88f2" appears on 2 separate effects. | ||
| ids: ch_4471, ch_4472 | ||
| ``` | ||
| **`subject` is the whole game.** It should contain what makes two operations | ||
| *different* and nothing that varies between attempts of the same one — no | ||
| timestamps, no trace ids, no retry counters. Leave something out and unrelated | ||
| operations look identical; leave something varying in and real duplicates hide. | ||
| Results carry a confidence and the reason they were flagged. A monthly | ||
| subscription looks exactly like a duplicate, so spread-out repeats are reported | ||
| at **low** confidence and clearly labelled, rather than sent to someone as a | ||
| finding. | ||
| --- | ||
| ## Why this exists | ||
| ## Also included | ||
| The bug is not two simultaneous calls. That one is easy and everybody's in-memory | ||
| `Set` handles it. | ||
| ### Warn before an unguarded effect | ||
| The bug is the worker that dies **between** reserving the key and finishing the | ||
| effect. A `Set` leaves that key claimed forever and the effect never runs at all. | ||
| Or the process restarts, the `Set` is empty, and the effect runs twice. | ||
| ```ts | ||
| import { guardEffect } from "once-kernel/guard"; | ||
| Retries are the other half. An agent whose request times out will re-send it. The | ||
| server already did the work; the response just never arrived. Nothing failed | ||
| loudly — it succeeded twice. | ||
| guardEffect({ | ||
| what: "charge card", | ||
| reversibility: "irreversible", | ||
| idempotencyKey: paymentId, // undefined is the case that warns | ||
| endpointDeduplicates: "unknown", // has the endpoint SAID it dedupes? | ||
| }); | ||
| ``` | ||
| `once` handles both: a durable record, a lease that expires if you crash, and a | ||
| fence token so the worker that stalled cannot overwrite the one that replaced it. | ||
| Warns when you're about to cause something irreversible with no dedup key | ||
| against an endpoint that has never claimed to deduplicate. It **does not block | ||
| the call** — a library that silently blocked effects would get ripped out. It | ||
| raises a flag at the moment a supervisor wants one. | ||
| ## What it guarantees | ||
| ### Cap what can be spent | ||
| - **Exactly one execution per key.** Verified by 1,000 racers across real OS | ||
| threads, released simultaneously by an `Atomics` barrier, against one shared | ||
| database. The execution count is read from a *separate table*, not | ||
| self-reported. Ten cold runs, ten times one execution. | ||
| - **Every caller gets the winner's result** — not an error, not `undefined`. | ||
| - **A crash cannot strand a key.** The lease expires and the work proceeds, with | ||
| the `generation` counter advancing so downstream systems can fence the dead worker. | ||
| - **Same key + different payload is a conflict, not a dedupe.** Guessing which | ||
| body wins is how money moves twice. | ||
| - **Key order in your JSON is irrelevant.** `{a,b}` and `{b,a}` are one operation — | ||
| which matters when an LLM reformats its arguments between retries. | ||
| ```ts | ||
| import { SpendLimiter } from "once-kernel/budget"; | ||
| ## What it does *not* do | ||
| const budget = new SpendLimiter({ limit: 500, windowMs: 24 * 60 * 60 * 1000 }); | ||
| await budget.run({ what: "pay supplier", amount: 49 }, () => pay(49)); | ||
| ``` | ||
| - It cannot make a non-idempotent remote API idempotent. If your provider charges | ||
| twice for two distinct requests, `once` stops the second request from being | ||
| *sent* — it cannot un-charge one that was. | ||
| - The default `MemoryStore` is single-process and dies with your program. That is | ||
| fine for tests and wrong for production. Use `SqliteStore`, or implement the | ||
| four-method `Store` interface against Postgres. | ||
| - It is not a queue, a scheduler, or a retry library. | ||
| The failure this catches isn't a duplicate — it's an agent doing something | ||
| individually reasonable, repeatedly, until the money is gone. Headroom is | ||
| **reserved before the call** and settled after, because checking a running total | ||
| afterwards is a race where ten concurrent calls all see room and all proceed. | ||
| ## Cross-language compatibility | ||
| --- | ||
| This is a port of [`once-kernel` on PyPI](https://pypi.org/project/once-kernel/), | ||
| and it hashes payloads **identically**: both use RFC 8785 (JSON Canonicalization | ||
| Scheme). A Python service and a Node service can key the same operation and agree | ||
| about whether it already ran. | ||
| ## What it guarantees | ||
| That claim is tested, not hoped for — `test/vectors/python-jcs-vectors.json` is | ||
| *generated by the Python implementation* and asserted against on every commit. | ||
| Hand-written expectations would only prove this file agrees with itself. | ||
| - **Exactly one execution per key.** 1,000 racers across real OS threads, | ||
| released simultaneously by an `Atomics` barrier, against one shared database. | ||
| The count is read from a *separate table*, not self-reported. Ten cold runs, | ||
| ten times one execution — and it runs on every commit in CI. | ||
| - **Every caller gets the winner's result.** Not an error, not `undefined`. | ||
| - **A crash cannot strand a key.** The lease expires, work proceeds, and | ||
| `generation` advances so downstream systems can fence the dead worker. | ||
| - **Same key + different payload is a conflict, not a dedupe.** Guessing which | ||
| body wins is how money moves twice. | ||
| - **Key order in your JSON is irrelevant.** `{a,b}` and `{b,a}` are one | ||
| operation — which matters when an LLM reformats its arguments between retries. | ||
| ### Large integers | ||
| ## What it does not do | ||
| JavaScript has one number type. By the time `once` sees `1234567890123456789`, | ||
| the parser has already rounded it to `1234567890123456800` — the precision is | ||
| gone before this library is called. **Pass large identifiers as strings.** | ||
| - It cannot make a non-idempotent remote API idempotent. It stops the second | ||
| request being *sent*; it cannot un-charge one that was. | ||
| - Not exactly-once *delivery*. That's physically impossible and anyone claiming | ||
| it is selling you something. This is exactly-once **execution**. | ||
| - The default `MemoryStore` is single-process and dies with your program — fine | ||
| for tests, wrong for production. Use `SqliteStore`. | ||
| - Not a queue, a scheduler, or a retry library. | ||
| --- | ||
| ## API | ||
@@ -93,3 +187,3 @@ | ||
| defaultTtlSec, // how long a completed record is remembered | ||
| defaultLeaseSec = 30, // how long before a crashed worker's key is reclaimed | ||
| defaultLeaseSec = 30, // before a crashed worker's key is reclaimed | ||
| maxResultBytes = 65536, | ||
@@ -99,9 +193,11 @@ }); | ||
| await once.run(key, payload, fn, { ttlSec, leaseSec, waitTimeoutMs, pollMs }); | ||
| await once.status(key); | ||
| await once.receipt(key); | ||
| ``` | ||
| Lower-level, if you need to control the boundary yourself: | ||
| Lower level, if you need the boundary yourself: | ||
| ```ts | ||
| const { execute, record } = await once.begin(key, payload); | ||
| if (!execute) return record.result; // someone else already did it | ||
| if (!execute) return record.result; // someone else already did it | ||
| try { | ||
@@ -123,28 +219,60 @@ const result = await doTheThing(); | ||
| |---|---| | ||
| | `IdempotencyConflict` | Same key, different payload. A caller bug — do not retry blindly. | | ||
| | `InProgressError` | Another caller holds the key right now. `run()` waits for you. | | ||
| | `IdempotencyConflict` | Same key, different payload. A caller bug — don't retry blindly. | | ||
| | `InProgressError` | Another caller holds the key. `run()` waits for you. | | ||
| | `WaitTimeout` | Waited past `waitTimeoutMs` for an in-flight call to settle. | | ||
| | `ResultTooLarge` | Result exceeds `maxResultBytes`. Store it elsewhere, keep a reference. | | ||
| | `CanonicalizationError` | Payload contains something JSON cannot represent (`NaN`, `undefined`, `Date`, `BigInt`). Rejected rather than silently coerced — coercion is how two payloads collide into one hash. | | ||
| | `ResultTooLarge` | Over `maxResultBytes`. Store it elsewhere, keep a reference. | | ||
| | `CanonicalizationError` | Payload holds something JSON can't represent (`NaN`, `undefined`, `Date`, `BigInt`). Rejected rather than coerced — coercion is how two payloads collide into one hash. | | ||
| | `BudgetExceeded` | The spend ceiling refused the call. Says what, how much, and when it frees up. | | ||
| ### Entry points | ||
| | Import | For | | ||
| |---|---| | ||
| | `once-kernel` | `Once`, errors, `MemoryStore` | | ||
| | `once-kernel/sqlite` | `SqliteStore` — durable, use this in production | | ||
| | `once-kernel/audit` | `findDuplicates`, `formatAuditReport` | | ||
| | `once-kernel/budget` | `SpendLimiter` | | ||
| | `once-kernel/guard` | `guardEffect`, `withGuard` | | ||
| ## Storage | ||
| `SqliteStore` uses Node's built-in `node:sqlite`. **This package has zero runtime | ||
| dependencies.** | ||
| `SqliteStore` uses Node's built-in `node:sqlite`. Every mutation is a single SQL | ||
| statement with its guard in the `WHERE` clause, so two processes racing the same | ||
| key resolve inside the database engine — reading then writing in JavaScript | ||
| would be the exact time-of-check-to-time-of-use race this library exists to | ||
| prevent. | ||
| Every mutation is a single SQL statement with its guard in the `WHERE` clause, so | ||
| two processes racing the same key resolve inside the database engine. Reading and | ||
| then writing in JavaScript would be a time-of-check-to-time-of-use race — exactly | ||
| the bug this library exists to prevent. | ||
| To use Postgres or Redis, implement `Store`: `get`, `createInProgress`, | ||
| For Postgres or Redis, implement `Store`: `get`, `createInProgress`, | ||
| `casComplete`, `casFail`, `reclaimIfLeaseDead`, `heartbeat`. Run | ||
| `test/store-conformance.test.ts` against it; that suite is the contract. | ||
| `test/store-conformance.test.ts` against it — that suite *is* the contract. | ||
| ## Requirements | ||
| ## Cross-language | ||
| Node 22.5+ (for `node:sqlite`). No build step, no native modules, no dependencies. | ||
| A port of [`once-kernel` on PyPI](https://pypi.org/project/once-kernel/), hashing | ||
| payloads **identically** via RFC 8785. A Python service and a Node service can | ||
| key the same operation and agree about whether it already ran. | ||
| Tested, not hoped for: `test/vectors/python-jcs-vectors.json` is *generated by | ||
| the Python implementation* and asserted against on every commit. Hand-written | ||
| expectations would only prove this file agrees with itself. | ||
| **Large integers:** JavaScript has one number type, and by the time `once` sees | ||
| `1234567890123456789` the parser has already rounded it. Pass large identifiers | ||
| as strings. | ||
| ## Related | ||
| [`fencescan`](https://www.npmjs.com/package/fencescan) — `npx fencescan` finds | ||
| tool calls in a codebase that could fire twice. | ||
| [`effectfence`](https://github.com/aurumflux20/effectfence) — the same guarantee | ||
| as an MCP server. | ||
| ## Commercial support | ||
| Free and Apache-2.0, staying that way. If you want help applying it to a codebase | ||
| that already moves money, email **hello@aurumflux.co** — | ||
| [the Fence Audit](https://github.com/aurumflux20/effectfence/blob/main/SUPPORT.md). | ||
| ## Licence | ||
| Apache-2.0 |
126131
67.4%27
80%1471
88.11%273
88.28%