@lacspace/idempotency 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/index.cjs +8 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -97,18 +97,24 @@ Across processes/instances (shared store), a second call finds an in-progress re
|
|
|
97
97
|
|
|
98
98
|
## Licensing
|
|
99
99
|
|
|
100
|
-
This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** —
|
|
100
|
+
This package is **free** under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; just keep the notice.
|
|
101
101
|
|
|
102
102
|
Not every Lacspace package is free. We also offer **Commercial** (paid), **Client-specific**, and **Private** (proprietary) packages under separate terms. See the full **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
|
|
103
103
|
|
|
104
|
+
<!-- LACSPACE-DEV-PLATFORM -->
|
|
105
|
+
|
|
104
106
|
---
|
|
105
107
|
|
|
106
|
-
|
|
108
|
+
## The Lacspace Developer Platform
|
|
107
109
|
|
|
108
|
-
|
|
110
|
+
`@lacspace/idempotency` is part of **63 zero-dependency, isomorphic TypeScript packages** — one standard library for the modern web. Explore the ecosystem:
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
- 📦 **This package, documented** — https://developer.lacspace.com/packages/idempotency
|
|
113
|
+
- 🗂️ **All 63 packages** — https://developer.lacspace.com/packages
|
|
114
|
+
- 🧭 **Developer handbook** — guides & runnable recipes — https://developer.lacspace.com/handbook
|
|
115
|
+
- 🧪 **Live playground** — run any package in your browser — https://developer.lacspace.com/playground
|
|
116
|
+
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
117
|
+
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
|
113
120
|
|
|
114
|
-
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
|
package/dist/index.cjs
CHANGED
|
@@ -80,7 +80,13 @@ var Idempotency = class {
|
|
|
80
80
|
*/
|
|
81
81
|
async run(key, fn, opts = {}) {
|
|
82
82
|
const flying = this.inflight.get(key);
|
|
83
|
-
if (flying)
|
|
83
|
+
if (flying) {
|
|
84
|
+
if (opts.fingerprint && flying.fingerprint && flying.fingerprint !== opts.fingerprint) {
|
|
85
|
+
throw new IdempotencyKeyReuseError(key);
|
|
86
|
+
}
|
|
87
|
+
const out2 = await flying.promise;
|
|
88
|
+
return { value: out2.value, replayed: true };
|
|
89
|
+
}
|
|
84
90
|
const store = opts.store ?? this.store;
|
|
85
91
|
const cacheErrors = opts.cacheErrors ?? this.cacheErrors;
|
|
86
92
|
const now = opts.now ?? Date.now();
|
|
@@ -116,7 +122,7 @@ var Idempotency = class {
|
|
|
116
122
|
throw err;
|
|
117
123
|
}
|
|
118
124
|
})().finally(() => this.inflight.delete(key));
|
|
119
|
-
this.inflight.set(key, exec);
|
|
125
|
+
this.inflight.set(key, { promise: exec, fingerprint: opts.fingerprint });
|
|
120
126
|
const out = await exec;
|
|
121
127
|
return { value: out.value, replayed: !out.fresh };
|
|
122
128
|
}
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":";;;AAqCO,IAAM,wBAAA,GAAN,cAAuC,KAAA,CAAM;AAAA,EAElD,YAA4B,GAAA,EAAa;AACvC,IAAA,KAAA,CAAM,CAAA,kCAAA,EAAqC,GAAG,CAAA,yBAAA,CAA2B,CAAA;AAD/C,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AAD5B,IAAA,IAAA,CAAS,IAAA,GAAO,UAAA;AAGd,IAAA,IAAA,CAAK,IAAA,GAAO,0BAAA;AAAA,EACd;AACF;AAEO,IAAM,wBAAA,GAAN,cAAuC,KAAA,CAAM;AAAA,EAElD,YAA4B,GAAA,EAAa;AACvC,IAAA,KAAA,CAAM,CAAA,iBAAA,EAAoB,GAAG,CAAA,8CAAA,CAAgD,CAAA;AADnD,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AAD5B,IAAA,IAAA,CAAS,IAAA,GAAO,WAAA;AAGd,IAAA,IAAA,CAAK,IAAA,GAAO,0BAAA;AAAA,EACd;AACF;AAEO,IAAM,aAAA,GAAN,cAA4B,KAAA,CAAM;AAAA,EAEvC,YAAY,OAAA,EAAiB;AAC3B,IAAA,KAAA,CAAM,OAAO,CAAA;AAFf,IAAA,IAAA,CAAS,IAAA,GAAO,gBAAA;AAGd,IAAA,IAAA,CAAK,IAAA,GAAO,eAAA;AAAA,EACd;AACF;AAIO,IAAM,yBAAN,MAAyD;AAAA,EAE9D,WAAA,CAA6B,KAAA,GAAQ,EAAA,GAAK,EAAA,GAAK,KAAK,GAAA,EAAM;AAA7B,IAAA,IAAA,CAAA,KAAA,GAAA,KAAA;AAD7B,IAAA,IAAA,CAAiB,GAAA,uBAAU,GAAA,EAAqD;AAAA,EACrB;AAAA,EAE3D,IAAI,GAAA,EAA4C;AAC9C,IAAA,MAAM,CAAA,GAAI,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAG,CAAA;AAC1B,IAAA,IAAI,CAAC,GAAG,OAAO,MAAA;AACf,IAAA,IAAI,IAAA,CAAK,GAAA,EAAI,GAAI,CAAA,CAAE,GAAA,EAAK;AAAE,MAAA,IAAA,CAAK,GAAA,CAAI,OAAO,GAAG,CAAA;AAAG,MAAA,OAAO,MAAA;AAAA,IAAW;AAClE,IAAA,OAAO,CAAA,CAAE,GAAA;AAAA,EACX;AAAA,EACA,MAAA,CAAO,KAAa,MAAA,EAAoC;AACtD,IAAA,IAAI,IAAA,CAAK,GAAA,CAAI,GAAG,CAAA,EAAG,OAAO,KAAA;AAC1B,IAAA,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,EAAE,GAAA,EAAK,MAAA,EAAQ,GAAA,EAAK,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA,CAAK,KAAA,EAAO,CAAA;AAC/D,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EACA,GAAA,CAAI,KAAa,MAAA,EAAiC;AAChD,IAAA,IAAA,CAAK,GAAA,CAAI,GAAA,CAAI,GAAA,EAAK,EAAE,GAAA,EAAK,MAAA,EAAQ,GAAA,EAAK,IAAA,CAAK,GAAA,EAAI,GAAI,IAAA,CAAK,KAAA,EAAO,CAAA;AAAA,EACjE;AAAA,EACA,OAAO,GAAA,EAAmB;AACxB,IAAA,IAAA,CAAK,GAAA,CAAI,OAAO,GAAG,CAAA;AAAA,EACrB;AACF;AAIA,SAAS,gBAAgB,KAAA,EAAwB;AAC/C,EAAA,IAAI,KAAA,KAAU,QAAQ,OAAO,KAAA,KAAU,UAAU,OAAO,IAAA,CAAK,UAAU,KAAK,CAAA;AAC5E,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,KAAA,CAAM,GAAA,CAAI,eAAe,CAAA,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,CAAA;AACzE,EAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,KAAgC,EAAE,IAAA,EAAK;AAChE,EAAA,OAAO,CAAA,CAAA,EAAI,KAAK,GAAA,CAAI,CAAC,MAAM,CAAA,EAAG,IAAA,CAAK,UAAU,CAAC,CAAC,IAAI,eAAA,CAAiB,KAAA,CAAkC,CAAC,CAAC,CAAC,EAAE,CAAA,CAAE,IAAA,CAAK,GAAG,CAAC,CAAA,CAAA,CAAA;AACxH;AAGO,SAAS,YAAY,KAAA,EAAwB;AAClD,EAAA,MAAM,CAAA,GAAI,gBAAgB,KAAK,CAAA;AAC/B,EAAA,IAAI,CAAA,GAAI,UAAA;AACR,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,CAAA,CAAE,QAAQ,CAAA,EAAA,EAAK;AAAE,IAAA,CAAA,IAAK,CAAA,CAAE,WAAW,CAAC,CAAA;AAAG,IAAA,CAAA,GAAK,CAAA,IAAA,CAAM,CAAA,IAAK,CAAA,KAAM,CAAA,IAAK,CAAA,CAAA,IAAM,KAAK,CAAA,CAAA,IAAM,CAAA,IAAK,CAAA,CAAA,IAAM,CAAA,IAAK,EAAA,CAAA,CAAA,KAAU,CAAA;AAAA,EAAG;AACpI,EAAA,OAAA,CAAQ,MAAM,CAAA,EAAG,QAAA,CAAS,EAAE,CAAA,CAAE,QAAA,CAAS,GAAG,GAAG,CAAA;AAC/C;AA0BA,IAAM,KAAA,GAAQ,CAAC,EAAA,KAA8B,IAAI,OAAA,CAAQ,CAAC,CAAA,KAAM,UAAA,CAAW,CAAA,EAAG,EAAE,CAAC,CAAA;AAI1E,IAAM,cAAN,MAAkB;AAAA,EAKvB,WAAA,CAAY,IAAA,GAA4D,EAAC,EAAG;AAF5E,IAAA,IAAA,CAAiB,QAAA,uBAAe,GAAA,EAA8B;AAG5D,IAAA,IAAA,CAAK,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,IAAI,sBAAA,EAAuB;AACtD,IAAA,IAAA,CAAK,WAAA,GAAc,KAAK,WAAA,IAAe,KAAA;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,GAAA,CAAO,GAAA,EAAa,EAAA,EAA0B,IAAA,GAAmB,EAAC,EAA0B;AAEhG,IAAA,MAAM,MAAA,GAAS,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAG,CAAA;AACpC,IAAA,IAAI,MAAA,SAAe,EAAE,KAAA,EAAA,CAAQ,MAAM,MAAA,EAAQ,KAAA,EAAO,UAAU,IAAA,EAAK;AAEjE,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,IAAS,IAAA,CAAK,KAAA;AACjC,IAAA,MAAM,WAAA,GAAc,IAAA,CAAK,WAAA,IAAe,IAAA,CAAK,WAAA;AAC7C,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,IAAO,IAAA,CAAK,GAAA,EAAI;AACjC,IAAA,MAAM,MAAA,GAA4B,EAAE,MAAA,EAAQ,aAAA,EAAe,aAAa,IAAA,CAAK,WAAA,EAAa,WAAW,GAAA,EAAI;AAIzG,IAAA,MAAM,QAAQ,YAAmD;AAC/D,MAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AACpC,MAAA,IAAI,QAAA,EAAU;AACZ,QAAA,IAAI,KAAK,WAAA,IAAe,QAAA,CAAS,eAAe,QAAA,CAAS,WAAA,KAAgB,KAAK,WAAA,EAAa;AACzF,UAAA,MAAM,IAAI,yBAAyB,GAAG,CAAA;AAAA,QACxC;AACA,QAAA,IAAI,QAAA,CAAS,WAAW,WAAA,EAAa,OAAO,EAAE,KAAA,EAAO,QAAA,CAAS,KAAA,EAAY,KAAA,EAAO,KAAA,EAAM;AACvF,QAAA,IAAI,QAAA,CAAS,WAAW,QAAA,EAAU;AAChC,UAAA,IAAI,aAAa,MAAM,IAAI,aAAA,CAAc,QAAA,CAAS,SAAS,8BAA8B,CAAA;AACzF,UAAA,MAAM,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,QACxB,CAAA,MAAO;AACL,UAAA,IAAA,CAAK,IAAA,CAAK,UAAA,IAAc,OAAA,MAAa,MAAA,SAAe,EAAE,KAAA,EAAO,MAAM,IAAA,CAAK,QAAW,KAAA,EAAO,GAAA,EAAK,IAAI,CAAA,EAAG,OAAO,KAAA,EAAM;AACnH,UAAA,MAAM,IAAI,yBAAyB,GAAG,CAAA;AAAA,QACxC;AAAA,MACF;AACA,MAAA,MAAM,OAAA,GAAU,MAAM,KAAA,CAAM,MAAA,CAAO,KAAK,MAAM,CAAA;AAC9C,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,IAAA,CAAK,IAAA,CAAK,UAAA,IAAc,OAAA,MAAa,MAAA,SAAe,EAAE,KAAA,EAAO,MAAM,IAAA,CAAK,QAAW,KAAA,EAAO,GAAA,EAAK,IAAI,CAAA,EAAG,OAAO,KAAA,EAAM;AACnH,QAAA,MAAM,IAAI,yBAAyB,GAAG,CAAA;AAAA,MACxC;AACA,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,MAAM,EAAA,EAAG;AACvB,QAAA,MAAM,MAAM,GAAA,CAAI,GAAA,EAAK,EAAE,MAAA,EAAQ,aAAa,KAAA,EAAO,WAAA,EAAa,IAAA,CAAK,WAAA,EAAa,WAAW,MAAA,CAAO,SAAA,EAAW,aAAa,IAAA,CAAK,GAAA,IAAO,CAAA;AACxI,QAAA,OAAO,EAAE,KAAA,EAAO,KAAA,EAAO,IAAA,EAAK;AAAA,MAC9B,SAAS,GAAA,EAAK;AACZ,QAAA,MAAM,UAAU,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC/D,QAAA,IAAI,WAAA,QAAmB,KAAA,CAAM,GAAA,CAAI,KAAK,EAAE,MAAA,EAAQ,UAAU,KAAA,EAAO,OAAA,EAAS,aAAa,IAAA,CAAK,WAAA,EAAa,WAAW,MAAA,CAAO,SAAA,EAAW,aAAa,IAAA,CAAK,GAAA,IAAO,CAAA;AAAA,aAC1J,MAAM,KAAA,CAAM,MAAA,CAAO,GAAG,CAAA;AAC3B,QAAA,MAAM,GAAA;AAAA,MACR;AAAA,IACF,CAAA,IAAK,OAAA,CAAQ,MAAM,KAAK,QAAA,CAAS,MAAA,CAAO,GAAG,CAAC,CAAA;AAE5C,IAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,GAAA,EAAK,IAAI,CAAA;AAC3B,IAAA,MAAM,MAAM,MAAM,IAAA;AAClB,IAAA,OAAO,EAAE,KAAA,EAAO,GAAA,CAAI,OAAO,QAAA,EAAU,CAAC,IAAI,KAAA,EAAM;AAAA,EAClD;AAAA,EAEA,MAAc,OAAA,CAAW,KAAA,EAAyB,GAAA,EAAa,IAAA,EAA8B;AAC3F,IAAA,MAAM,QAAA,GAAW,KAAK,cAAA,IAAkB,EAAA;AACxC,IAAA,MAAM,OAAA,GAAU,KAAK,aAAA,IAAiB,GAAA;AACtC,IAAA,MAAM,KAAA,GAAQ,KAAK,GAAA,EAAI;AACvB,IAAA,WAAS;AACP,MAAA,MAAM,GAAA,GAAM,MAAM,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAC/B,MAAA,IAAI,CAAC,GAAA,IAAO,GAAA,CAAI,MAAA,KAAW,WAAA,EAAa;AACtC,QAAA,IAAI,GAAA,EAAK,MAAA,KAAW,WAAA,EAAa,OAAO,GAAA,CAAI,KAAA;AAC5C,QAAA,MAAM,IAAI,yBAAyB,GAAG,CAAA;AAAA,MACxC;AACA,MAAA,IAAI,GAAA,CAAI,WAAW,QAAA,EAAU,MAAM,IAAI,aAAA,CAAc,GAAA,CAAI,SAAS,mBAAmB,CAAA;AACrF,MAAA,IAAI,IAAA,CAAK,KAAI,GAAI,KAAA,GAAQ,SAAS,MAAM,IAAI,yBAAyB,GAAG,CAAA;AACxE,MAAA,MAAM,MAAM,QAAQ,CAAA;AAAA,IACtB;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,MAAA,CAAO,GAAA,EAAa,KAAA,EAAyC;AACjE,IAAA,MAAA,CAAO,KAAA,IAAS,IAAA,CAAK,KAAA,EAAO,MAAA,CAAO,GAAG,CAAA;AAAA,EACxC;AACF;AAIA,IAAM,MAAA,GAAS,IAAI,WAAA,EAAY;AASxB,SAAS,UAAA,CAAc,GAAA,EAAa,EAAA,EAA0B,IAAA,EAA0C;AAC7G,EAAA,OAAO,MAAA,CAAO,GAAA,CAAI,GAAA,EAAK,EAAA,EAAI,IAAI,CAAA;AACjC","file":"index.cjs","sourcesContent":["/**\n * @lacspace/idempotency\n *\n * Make any operation exactly-once with an idempotency key — the \"run this at\n * most once, and replay the stored result on retries\" pattern that payment APIs\n * and webhook handlers need. Framework-agnostic (every existing lib is locked to\n * Hono / AWS Lambda), zero-dependency, isomorphic.\n *\n * - Replay the cached result for a repeated key (never double-charge / double-send)\n * - Safe under concurrency: in-flight de-dupe in-process, atomic \"create-if-absent\"\n * for shared stores, and a conflict/wait policy for the rest\n * - Optional request fingerprint → detect a key reused with a different payload\n * - Pluggable store (in-memory built in; bring your own Redis/KV/SQL)\n */\n\nexport type RecordStatus = \"in-progress\" | \"completed\" | \"failed\";\n\nexport interface IdempotencyRecord<T = unknown> {\n status: RecordStatus;\n value?: T;\n error?: string;\n /** Optional request signature to detect key reuse with different params. */\n fingerprint?: string;\n createdAt: number;\n completedAt?: number;\n}\n\nexport interface IdempotencyStore {\n get(key: string): IdempotencyRecord | undefined | Promise<IdempotencyRecord | undefined>;\n /** Atomically create an in-progress record only if the key is absent. Returns true when created. */\n create(key: string, record: IdempotencyRecord): boolean | Promise<boolean>;\n set(key: string, record: IdempotencyRecord): void | Promise<void>;\n delete(key: string): void | Promise<void>;\n}\n\n/* ------------------------------ errors ------------------------------ */\n\nexport class IdempotencyConflictError extends Error {\n readonly code = \"conflict\";\n constructor(public readonly key: string) {\n super(`An operation for idempotency key \"${key}\" is already in progress.`);\n this.name = \"IdempotencyConflictError\";\n }\n}\n\nexport class IdempotencyKeyReuseError extends Error {\n readonly code = \"key-reuse\";\n constructor(public readonly key: string) {\n super(`Idempotency key \"${key}\" was reused with a different request payload.`);\n this.name = \"IdempotencyKeyReuseError\";\n }\n}\n\nexport class ReplayedError extends Error {\n readonly code = \"replayed-error\";\n constructor(message: string) {\n super(message);\n this.name = \"ReplayedError\";\n }\n}\n\n/* ------------------------------ in-memory store ------------------------------ */\n\nexport class MemoryIdempotencyStore implements IdempotencyStore {\n private readonly map = new Map<string, { rec: IdempotencyRecord; exp: number }>();\n constructor(private readonly ttlMs = 24 * 60 * 60 * 1000) {}\n\n get(key: string): IdempotencyRecord | undefined {\n const e = this.map.get(key);\n if (!e) return undefined;\n if (Date.now() > e.exp) { this.map.delete(key); return undefined; }\n return e.rec;\n }\n create(key: string, record: IdempotencyRecord): boolean {\n if (this.get(key)) return false;\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n return true;\n }\n set(key: string, record: IdempotencyRecord): void {\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n }\n delete(key: string): void {\n this.map.delete(key);\n }\n}\n\n/* ------------------------------ fingerprint ------------------------------ */\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const keys = Object.keys(value as Record<string, unknown>).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify((value as Record<string, unknown>)[k])}`).join(\",\")}}`;\n}\n\n/** Stable fingerprint of a request payload (order-independent). Pass it as `fingerprint`. */\nexport function fingerprint(value: unknown): string {\n const s = stableStringify(value);\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; }\n return (h >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/* ------------------------------ options & result ------------------------------ */\n\nexport interface RunOptions {\n store?: IdempotencyStore;\n /** Request signature; a mismatch on the same key throws {@link IdempotencyKeyReuseError}. */\n fingerprint?: string;\n /** What to do when another call for the key is in progress. Default \"throw\". */\n onConflict?: \"throw\" | \"wait\";\n /** Cache failures too (replay the error). Default false → failures are retryable. */\n cacheErrors?: boolean;\n /** Poll interval when waiting (ms). Default 50. */\n pollIntervalMs?: number;\n /** Max time to wait for an in-progress op (ms). Default 10_000. */\n waitTimeoutMs?: number;\n /** Override \"now\" (ms) — for tests. */\n now?: number;\n}\n\nexport interface RunResult<T> {\n value: T;\n /** True when the result came from a previous run (a replay), not a fresh execution. */\n replayed: boolean;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/* ------------------------------ the engine ------------------------------ */\n\nexport class Idempotency {\n private readonly store: IdempotencyStore;\n private readonly cacheErrors: boolean;\n private readonly inflight = new Map<string, Promise<unknown>>();\n\n constructor(opts: { store?: IdempotencyStore; cacheErrors?: boolean } = {}) {\n this.store = opts.store ?? new MemoryIdempotencyStore();\n this.cacheErrors = opts.cacheErrors ?? false;\n }\n\n /**\n * Run `fn` at most once for `key`. A repeat call replays the stored result.\n *\n * @example\n * const { value, replayed } = await idem.run(idempotencyKey, () => charge(order));\n */\n async run<T>(key: string, fn: () => Promise<T> | T, opts: RunOptions = {}): Promise<RunResult<T>> {\n // Same-process single-flight: concurrent duplicates await the same execution.\n const flying = this.inflight.get(key) as Promise<{ value: T; fresh: boolean }> | undefined;\n if (flying) return { value: (await flying).value, replayed: true };\n\n const store = opts.store ?? this.store;\n const cacheErrors = opts.cacheErrors ?? this.cacheErrors;\n const now = opts.now ?? Date.now();\n const record: IdempotencyRecord = { status: \"in-progress\", fingerprint: opts.fingerprint, createdAt: now };\n\n // Register the in-flight promise SYNCHRONOUSLY, before any await, so\n // truly-concurrent callers see it and don't race on create().\n const exec = (async (): Promise<{ value: T; fresh: boolean }> => {\n const existing = await store.get(key);\n if (existing) {\n if (opts.fingerprint && existing.fingerprint && existing.fingerprint !== opts.fingerprint) {\n throw new IdempotencyKeyReuseError(key);\n }\n if (existing.status === \"completed\") return { value: existing.value as T, fresh: false };\n if (existing.status === \"failed\") {\n if (cacheErrors) throw new ReplayedError(existing.error ?? \"Operation previously failed.\");\n await store.delete(key); // retryable → clear and re-run below\n } else {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n }\n const created = await store.create(key, record);\n if (!created) {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n try {\n const value = await fn();\n await store.set(key, { status: \"completed\", value, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n return { value, fresh: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (cacheErrors) await store.set(key, { status: \"failed\", error: message, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n else await store.delete(key);\n throw err;\n }\n })().finally(() => this.inflight.delete(key));\n\n this.inflight.set(key, exec);\n const out = await exec;\n return { value: out.value, replayed: !out.fresh };\n }\n\n private async waitFor<T>(store: IdempotencyStore, key: string, opts: RunOptions): Promise<T> {\n const interval = opts.pollIntervalMs ?? 50;\n const timeout = opts.waitTimeoutMs ?? 10_000;\n const start = Date.now();\n for (;;) {\n const rec = await store.get(key);\n if (!rec || rec.status === \"completed\") {\n if (rec?.status === \"completed\") return rec.value as T;\n throw new IdempotencyConflictError(key); // vanished mid-wait\n }\n if (rec.status === \"failed\") throw new ReplayedError(rec.error ?? \"Operation failed.\");\n if (Date.now() - start > timeout) throw new IdempotencyConflictError(key);\n await sleep(interval);\n }\n }\n\n /** Forget a key so its operation can run fresh again. */\n async forget(key: string, store?: IdempotencyStore): Promise<void> {\n await (store ?? this.store).delete(key);\n }\n}\n\n/* ------------------------------ functional default ------------------------------ */\n\nconst shared = new Idempotency();\n\n/**\n * Run `fn` at most once for `key`, using a shared in-memory store (pass\n * `opts.store` for your own). Returns `{ value, replayed }`.\n *\n * @example\n * const { value } = await idempotent(req.headers[\"idempotency-key\"], () => createOrder(body));\n */\nexport function idempotent<T>(key: string, fn: () => Promise<T> | T, opts?: RunOptions): Promise<RunResult<T>> {\n return shared.run(key, fn, opts);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["out"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @lacspace/idempotency\n *\n * Make any operation exactly-once with an idempotency key — the \"run this at\n * most once, and replay the stored result on retries\" pattern that payment APIs\n * and webhook handlers need. Framework-agnostic (every existing lib is locked to\n * Hono / AWS Lambda), zero-dependency, isomorphic.\n *\n * - Replay the cached result for a repeated key (never double-charge / double-send)\n * - Safe under concurrency: in-flight de-dupe in-process, atomic \"create-if-absent\"\n * for shared stores, and a conflict/wait policy for the rest\n * - Optional request fingerprint → detect a key reused with a different payload\n * - Pluggable store (in-memory built in; bring your own Redis/KV/SQL)\n */\n\nexport type RecordStatus = \"in-progress\" | \"completed\" | \"failed\";\n\nexport interface IdempotencyRecord<T = unknown> {\n status: RecordStatus;\n value?: T;\n error?: string;\n /** Optional request signature to detect key reuse with different params. */\n fingerprint?: string;\n createdAt: number;\n completedAt?: number;\n}\n\nexport interface IdempotencyStore {\n get(key: string): IdempotencyRecord | undefined | Promise<IdempotencyRecord | undefined>;\n /** Atomically create an in-progress record only if the key is absent. Returns true when created. */\n create(key: string, record: IdempotencyRecord): boolean | Promise<boolean>;\n set(key: string, record: IdempotencyRecord): void | Promise<void>;\n delete(key: string): void | Promise<void>;\n}\n\n/* ------------------------------ errors ------------------------------ */\n\nexport class IdempotencyConflictError extends Error {\n readonly code = \"conflict\";\n constructor(public readonly key: string) {\n super(`An operation for idempotency key \"${key}\" is already in progress.`);\n this.name = \"IdempotencyConflictError\";\n }\n}\n\nexport class IdempotencyKeyReuseError extends Error {\n readonly code = \"key-reuse\";\n constructor(public readonly key: string) {\n super(`Idempotency key \"${key}\" was reused with a different request payload.`);\n this.name = \"IdempotencyKeyReuseError\";\n }\n}\n\nexport class ReplayedError extends Error {\n readonly code = \"replayed-error\";\n constructor(message: string) {\n super(message);\n this.name = \"ReplayedError\";\n }\n}\n\n/* ------------------------------ in-memory store ------------------------------ */\n\nexport class MemoryIdempotencyStore implements IdempotencyStore {\n private readonly map = new Map<string, { rec: IdempotencyRecord; exp: number }>();\n constructor(private readonly ttlMs = 24 * 60 * 60 * 1000) {}\n\n get(key: string): IdempotencyRecord | undefined {\n const e = this.map.get(key);\n if (!e) return undefined;\n if (Date.now() > e.exp) { this.map.delete(key); return undefined; }\n return e.rec;\n }\n create(key: string, record: IdempotencyRecord): boolean {\n if (this.get(key)) return false;\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n return true;\n }\n set(key: string, record: IdempotencyRecord): void {\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n }\n delete(key: string): void {\n this.map.delete(key);\n }\n}\n\n/* ------------------------------ fingerprint ------------------------------ */\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const keys = Object.keys(value as Record<string, unknown>).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify((value as Record<string, unknown>)[k])}`).join(\",\")}}`;\n}\n\n/** Stable fingerprint of a request payload (order-independent). Pass it as `fingerprint`. */\nexport function fingerprint(value: unknown): string {\n const s = stableStringify(value);\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; }\n return (h >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/* ------------------------------ options & result ------------------------------ */\n\nexport interface RunOptions {\n store?: IdempotencyStore;\n /** Request signature; a mismatch on the same key throws {@link IdempotencyKeyReuseError}. */\n fingerprint?: string;\n /** What to do when another call for the key is in progress. Default \"throw\". */\n onConflict?: \"throw\" | \"wait\";\n /** Cache failures too (replay the error). Default false → failures are retryable. */\n cacheErrors?: boolean;\n /** Poll interval when waiting (ms). Default 50. */\n pollIntervalMs?: number;\n /** Max time to wait for an in-progress op (ms). Default 10_000. */\n waitTimeoutMs?: number;\n /** Override \"now\" (ms) — for tests. */\n now?: number;\n}\n\nexport interface RunResult<T> {\n value: T;\n /** True when the result came from a previous run (a replay), not a fresh execution. */\n replayed: boolean;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/* ------------------------------ the engine ------------------------------ */\n\nexport class Idempotency {\n private readonly store: IdempotencyStore;\n private readonly cacheErrors: boolean;\n private readonly inflight = new Map<string, { promise: Promise<unknown>; fingerprint?: string }>();\n\n constructor(opts: { store?: IdempotencyStore; cacheErrors?: boolean } = {}) {\n this.store = opts.store ?? new MemoryIdempotencyStore();\n this.cacheErrors = opts.cacheErrors ?? false;\n }\n\n /**\n * Run `fn` at most once for `key`. A repeat call replays the stored result.\n *\n * @example\n * const { value, replayed } = await idem.run(idempotencyKey, () => charge(order));\n */\n async run<T>(key: string, fn: () => Promise<T> | T, opts: RunOptions = {}): Promise<RunResult<T>> {\n // Same-process single-flight: concurrent duplicates await the same execution.\n const flying = this.inflight.get(key);\n if (flying) {\n // Apply the same fingerprint check the stored path uses, so a concurrent\n // same-key call with a mismatched fingerprint throws consistently.\n if (opts.fingerprint && flying.fingerprint && flying.fingerprint !== opts.fingerprint) {\n throw new IdempotencyKeyReuseError(key);\n }\n const out = (await flying.promise) as { value: T; fresh: boolean };\n return { value: out.value, replayed: true };\n }\n\n const store = opts.store ?? this.store;\n const cacheErrors = opts.cacheErrors ?? this.cacheErrors;\n const now = opts.now ?? Date.now();\n const record: IdempotencyRecord = { status: \"in-progress\", fingerprint: opts.fingerprint, createdAt: now };\n\n // Register the in-flight promise SYNCHRONOUSLY, before any await, so\n // truly-concurrent callers see it and don't race on create().\n const exec = (async (): Promise<{ value: T; fresh: boolean }> => {\n const existing = await store.get(key);\n if (existing) {\n if (opts.fingerprint && existing.fingerprint && existing.fingerprint !== opts.fingerprint) {\n throw new IdempotencyKeyReuseError(key);\n }\n if (existing.status === \"completed\") return { value: existing.value as T, fresh: false };\n if (existing.status === \"failed\") {\n if (cacheErrors) throw new ReplayedError(existing.error ?? \"Operation previously failed.\");\n await store.delete(key); // retryable → clear and re-run below\n } else {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n }\n const created = await store.create(key, record);\n if (!created) {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n try {\n const value = await fn();\n await store.set(key, { status: \"completed\", value, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n return { value, fresh: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (cacheErrors) await store.set(key, { status: \"failed\", error: message, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n else await store.delete(key);\n throw err;\n }\n })().finally(() => this.inflight.delete(key));\n\n this.inflight.set(key, { promise: exec, fingerprint: opts.fingerprint });\n const out = await exec;\n return { value: out.value, replayed: !out.fresh };\n }\n\n private async waitFor<T>(store: IdempotencyStore, key: string, opts: RunOptions): Promise<T> {\n const interval = opts.pollIntervalMs ?? 50;\n const timeout = opts.waitTimeoutMs ?? 10_000;\n const start = Date.now();\n for (;;) {\n const rec = await store.get(key);\n if (!rec || rec.status === \"completed\") {\n if (rec?.status === \"completed\") return rec.value as T;\n throw new IdempotencyConflictError(key); // vanished mid-wait\n }\n if (rec.status === \"failed\") throw new ReplayedError(rec.error ?? \"Operation failed.\");\n if (Date.now() - start > timeout) throw new IdempotencyConflictError(key);\n await sleep(interval);\n }\n }\n\n /** Forget a key so its operation can run fresh again. */\n async forget(key: string, store?: IdempotencyStore): Promise<void> {\n await (store ?? this.store).delete(key);\n }\n}\n\n/* ------------------------------ functional default ------------------------------ */\n\nconst shared = new Idempotency();\n\n/**\n * Run `fn` at most once for `key`, using a shared in-memory store (pass\n * `opts.store` for your own). Returns `{ value, replayed }`.\n *\n * @example\n * const { value } = await idempotent(req.headers[\"idempotency-key\"], () => createOrder(body));\n */\nexport function idempotent<T>(key: string, fn: () => Promise<T> | T, opts?: RunOptions): Promise<RunResult<T>> {\n return shared.run(key, fn, opts);\n}\n"]}
|
package/dist/index.js
CHANGED
|
@@ -78,7 +78,13 @@ var Idempotency = class {
|
|
|
78
78
|
*/
|
|
79
79
|
async run(key, fn, opts = {}) {
|
|
80
80
|
const flying = this.inflight.get(key);
|
|
81
|
-
if (flying)
|
|
81
|
+
if (flying) {
|
|
82
|
+
if (opts.fingerprint && flying.fingerprint && flying.fingerprint !== opts.fingerprint) {
|
|
83
|
+
throw new IdempotencyKeyReuseError(key);
|
|
84
|
+
}
|
|
85
|
+
const out2 = await flying.promise;
|
|
86
|
+
return { value: out2.value, replayed: true };
|
|
87
|
+
}
|
|
82
88
|
const store = opts.store ?? this.store;
|
|
83
89
|
const cacheErrors = opts.cacheErrors ?? this.cacheErrors;
|
|
84
90
|
const now = opts.now ?? Date.now();
|
|
@@ -114,7 +120,7 @@ var Idempotency = class {
|
|
|
114
120
|
throw err;
|
|
115
121
|
}
|
|
116
122
|
})().finally(() => this.inflight.delete(key));
|
|
117
|
-
this.inflight.set(key, exec);
|
|
123
|
+
this.inflight.set(key, { promise: exec, fingerprint: opts.fingerprint });
|
|
118
124
|
const out = await exec;
|
|
119
125
|
return { value: out.value, replayed: !out.fresh };
|
|
120
126
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/idempotency\n *\n * Make any operation exactly-once with an idempotency key — the \"run this at\n * most once, and replay the stored result on retries\" pattern that payment APIs\n * and webhook handlers need. Framework-agnostic (every existing lib is locked to\n * Hono / AWS Lambda), zero-dependency, isomorphic.\n *\n * - Replay the cached result for a repeated key (never double-charge / double-send)\n * - Safe under concurrency: in-flight de-dupe in-process, atomic \"create-if-absent\"\n * for shared stores, and a conflict/wait policy for the rest\n * - Optional request fingerprint → detect a key reused with a different payload\n * - Pluggable store (in-memory built in; bring your own Redis/KV/SQL)\n */\n\nexport type RecordStatus = \"in-progress\" | \"completed\" | \"failed\";\n\nexport interface IdempotencyRecord<T = unknown> {\n status: RecordStatus;\n value?: T;\n error?: string;\n /** Optional request signature to detect key reuse with different params. */\n fingerprint?: string;\n createdAt: number;\n completedAt?: number;\n}\n\nexport interface IdempotencyStore {\n get(key: string): IdempotencyRecord | undefined | Promise<IdempotencyRecord | undefined>;\n /** Atomically create an in-progress record only if the key is absent. Returns true when created. */\n create(key: string, record: IdempotencyRecord): boolean | Promise<boolean>;\n set(key: string, record: IdempotencyRecord): void | Promise<void>;\n delete(key: string): void | Promise<void>;\n}\n\n/* ------------------------------ errors ------------------------------ */\n\nexport class IdempotencyConflictError extends Error {\n readonly code = \"conflict\";\n constructor(public readonly key: string) {\n super(`An operation for idempotency key \"${key}\" is already in progress.`);\n this.name = \"IdempotencyConflictError\";\n }\n}\n\nexport class IdempotencyKeyReuseError extends Error {\n readonly code = \"key-reuse\";\n constructor(public readonly key: string) {\n super(`Idempotency key \"${key}\" was reused with a different request payload.`);\n this.name = \"IdempotencyKeyReuseError\";\n }\n}\n\nexport class ReplayedError extends Error {\n readonly code = \"replayed-error\";\n constructor(message: string) {\n super(message);\n this.name = \"ReplayedError\";\n }\n}\n\n/* ------------------------------ in-memory store ------------------------------ */\n\nexport class MemoryIdempotencyStore implements IdempotencyStore {\n private readonly map = new Map<string, { rec: IdempotencyRecord; exp: number }>();\n constructor(private readonly ttlMs = 24 * 60 * 60 * 1000) {}\n\n get(key: string): IdempotencyRecord | undefined {\n const e = this.map.get(key);\n if (!e) return undefined;\n if (Date.now() > e.exp) { this.map.delete(key); return undefined; }\n return e.rec;\n }\n create(key: string, record: IdempotencyRecord): boolean {\n if (this.get(key)) return false;\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n return true;\n }\n set(key: string, record: IdempotencyRecord): void {\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n }\n delete(key: string): void {\n this.map.delete(key);\n }\n}\n\n/* ------------------------------ fingerprint ------------------------------ */\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const keys = Object.keys(value as Record<string, unknown>).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify((value as Record<string, unknown>)[k])}`).join(\",\")}}`;\n}\n\n/** Stable fingerprint of a request payload (order-independent). Pass it as `fingerprint`. */\nexport function fingerprint(value: unknown): string {\n const s = stableStringify(value);\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; }\n return (h >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/* ------------------------------ options & result ------------------------------ */\n\nexport interface RunOptions {\n store?: IdempotencyStore;\n /** Request signature; a mismatch on the same key throws {@link IdempotencyKeyReuseError}. */\n fingerprint?: string;\n /** What to do when another call for the key is in progress. Default \"throw\". */\n onConflict?: \"throw\" | \"wait\";\n /** Cache failures too (replay the error). Default false → failures are retryable. */\n cacheErrors?: boolean;\n /** Poll interval when waiting (ms). Default 50. */\n pollIntervalMs?: number;\n /** Max time to wait for an in-progress op (ms). Default 10_000. */\n waitTimeoutMs?: number;\n /** Override \"now\" (ms) — for tests. */\n now?: number;\n}\n\nexport interface RunResult<T> {\n value: T;\n /** True when the result came from a previous run (a replay), not a fresh execution. */\n replayed: boolean;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/* ------------------------------ the engine ------------------------------ */\n\nexport class Idempotency {\n private readonly store: IdempotencyStore;\n private readonly cacheErrors: boolean;\n private readonly inflight = new Map<string, Promise<unknown>>();\n\n constructor(opts: { store?: IdempotencyStore; cacheErrors?: boolean } = {}) {\n this.store = opts.store ?? new MemoryIdempotencyStore();\n this.cacheErrors = opts.cacheErrors ?? false;\n }\n\n /**\n * Run `fn` at most once for `key`. A repeat call replays the stored result.\n *\n * @example\n * const { value, replayed } = await idem.run(idempotencyKey, () => charge(order));\n */\n async run<T>(key: string, fn: () => Promise<T> | T, opts: RunOptions = {}): Promise<RunResult<T>> {\n // Same-process single-flight: concurrent duplicates await the same execution.\n const flying = this.inflight.get(key) as Promise<{ value: T; fresh: boolean }> | undefined;\n if (flying) return { value: (await flying).value, replayed: true };\n\n const store = opts.store ?? this.store;\n const cacheErrors = opts.cacheErrors ?? this.cacheErrors;\n const now = opts.now ?? Date.now();\n const record: IdempotencyRecord = { status: \"in-progress\", fingerprint: opts.fingerprint, createdAt: now };\n\n // Register the in-flight promise SYNCHRONOUSLY, before any await, so\n // truly-concurrent callers see it and don't race on create().\n const exec = (async (): Promise<{ value: T; fresh: boolean }> => {\n const existing = await store.get(key);\n if (existing) {\n if (opts.fingerprint && existing.fingerprint && existing.fingerprint !== opts.fingerprint) {\n throw new IdempotencyKeyReuseError(key);\n }\n if (existing.status === \"completed\") return { value: existing.value as T, fresh: false };\n if (existing.status === \"failed\") {\n if (cacheErrors) throw new ReplayedError(existing.error ?? \"Operation previously failed.\");\n await store.delete(key); // retryable → clear and re-run below\n } else {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n }\n const created = await store.create(key, record);\n if (!created) {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n try {\n const value = await fn();\n await store.set(key, { status: \"completed\", value, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n return { value, fresh: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (cacheErrors) await store.set(key, { status: \"failed\", error: message, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n else await store.delete(key);\n throw err;\n }\n })().finally(() => this.inflight.delete(key));\n\n this.inflight.set(key, exec);\n const out = await exec;\n return { value: out.value, replayed: !out.fresh };\n }\n\n private async waitFor<T>(store: IdempotencyStore, key: string, opts: RunOptions): Promise<T> {\n const interval = opts.pollIntervalMs ?? 50;\n const timeout = opts.waitTimeoutMs ?? 10_000;\n const start = Date.now();\n for (;;) {\n const rec = await store.get(key);\n if (!rec || rec.status === \"completed\") {\n if (rec?.status === \"completed\") return rec.value as T;\n throw new IdempotencyConflictError(key); // vanished mid-wait\n }\n if (rec.status === \"failed\") throw new ReplayedError(rec.error ?? \"Operation failed.\");\n if (Date.now() - start > timeout) throw new IdempotencyConflictError(key);\n await sleep(interval);\n }\n }\n\n /** Forget a key so its operation can run fresh again. */\n async forget(key: string, store?: IdempotencyStore): Promise<void> {\n await (store ?? this.store).delete(key);\n }\n}\n\n/* ------------------------------ functional default ------------------------------ */\n\nconst shared = new Idempotency();\n\n/**\n * Run `fn` at most once for `key`, using a shared in-memory store (pass\n * `opts.store` for your own). Returns `{ value, replayed }`.\n *\n * @example\n * const { value } = await idempotent(req.headers[\"idempotency-key\"], () => createOrder(body));\n */\nexport function idempotent<T>(key: string, fn: () => Promise<T> | T, opts?: RunOptions): Promise<RunResult<T>> {\n return shared.run(key, fn, opts);\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["out"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @lacspace/idempotency\n *\n * Make any operation exactly-once with an idempotency key — the \"run this at\n * most once, and replay the stored result on retries\" pattern that payment APIs\n * and webhook handlers need. Framework-agnostic (every existing lib is locked to\n * Hono / AWS Lambda), zero-dependency, isomorphic.\n *\n * - Replay the cached result for a repeated key (never double-charge / double-send)\n * - Safe under concurrency: in-flight de-dupe in-process, atomic \"create-if-absent\"\n * for shared stores, and a conflict/wait policy for the rest\n * - Optional request fingerprint → detect a key reused with a different payload\n * - Pluggable store (in-memory built in; bring your own Redis/KV/SQL)\n */\n\nexport type RecordStatus = \"in-progress\" | \"completed\" | \"failed\";\n\nexport interface IdempotencyRecord<T = unknown> {\n status: RecordStatus;\n value?: T;\n error?: string;\n /** Optional request signature to detect key reuse with different params. */\n fingerprint?: string;\n createdAt: number;\n completedAt?: number;\n}\n\nexport interface IdempotencyStore {\n get(key: string): IdempotencyRecord | undefined | Promise<IdempotencyRecord | undefined>;\n /** Atomically create an in-progress record only if the key is absent. Returns true when created. */\n create(key: string, record: IdempotencyRecord): boolean | Promise<boolean>;\n set(key: string, record: IdempotencyRecord): void | Promise<void>;\n delete(key: string): void | Promise<void>;\n}\n\n/* ------------------------------ errors ------------------------------ */\n\nexport class IdempotencyConflictError extends Error {\n readonly code = \"conflict\";\n constructor(public readonly key: string) {\n super(`An operation for idempotency key \"${key}\" is already in progress.`);\n this.name = \"IdempotencyConflictError\";\n }\n}\n\nexport class IdempotencyKeyReuseError extends Error {\n readonly code = \"key-reuse\";\n constructor(public readonly key: string) {\n super(`Idempotency key \"${key}\" was reused with a different request payload.`);\n this.name = \"IdempotencyKeyReuseError\";\n }\n}\n\nexport class ReplayedError extends Error {\n readonly code = \"replayed-error\";\n constructor(message: string) {\n super(message);\n this.name = \"ReplayedError\";\n }\n}\n\n/* ------------------------------ in-memory store ------------------------------ */\n\nexport class MemoryIdempotencyStore implements IdempotencyStore {\n private readonly map = new Map<string, { rec: IdempotencyRecord; exp: number }>();\n constructor(private readonly ttlMs = 24 * 60 * 60 * 1000) {}\n\n get(key: string): IdempotencyRecord | undefined {\n const e = this.map.get(key);\n if (!e) return undefined;\n if (Date.now() > e.exp) { this.map.delete(key); return undefined; }\n return e.rec;\n }\n create(key: string, record: IdempotencyRecord): boolean {\n if (this.get(key)) return false;\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n return true;\n }\n set(key: string, record: IdempotencyRecord): void {\n this.map.set(key, { rec: record, exp: Date.now() + this.ttlMs });\n }\n delete(key: string): void {\n this.map.delete(key);\n }\n}\n\n/* ------------------------------ fingerprint ------------------------------ */\n\nfunction stableStringify(value: unknown): string {\n if (value === null || typeof value !== \"object\") return JSON.stringify(value);\n if (Array.isArray(value)) return `[${value.map(stableStringify).join(\",\")}]`;\n const keys = Object.keys(value as Record<string, unknown>).sort();\n return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify((value as Record<string, unknown>)[k])}`).join(\",\")}}`;\n}\n\n/** Stable fingerprint of a request payload (order-independent). Pass it as `fingerprint`. */\nexport function fingerprint(value: unknown): string {\n const s = stableStringify(value);\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0; }\n return (h >>> 0).toString(16).padStart(8, \"0\");\n}\n\n/* ------------------------------ options & result ------------------------------ */\n\nexport interface RunOptions {\n store?: IdempotencyStore;\n /** Request signature; a mismatch on the same key throws {@link IdempotencyKeyReuseError}. */\n fingerprint?: string;\n /** What to do when another call for the key is in progress. Default \"throw\". */\n onConflict?: \"throw\" | \"wait\";\n /** Cache failures too (replay the error). Default false → failures are retryable. */\n cacheErrors?: boolean;\n /** Poll interval when waiting (ms). Default 50. */\n pollIntervalMs?: number;\n /** Max time to wait for an in-progress op (ms). Default 10_000. */\n waitTimeoutMs?: number;\n /** Override \"now\" (ms) — for tests. */\n now?: number;\n}\n\nexport interface RunResult<T> {\n value: T;\n /** True when the result came from a previous run (a replay), not a fresh execution. */\n replayed: boolean;\n}\n\nconst sleep = (ms: number): Promise<void> => new Promise((r) => setTimeout(r, ms));\n\n/* ------------------------------ the engine ------------------------------ */\n\nexport class Idempotency {\n private readonly store: IdempotencyStore;\n private readonly cacheErrors: boolean;\n private readonly inflight = new Map<string, { promise: Promise<unknown>; fingerprint?: string }>();\n\n constructor(opts: { store?: IdempotencyStore; cacheErrors?: boolean } = {}) {\n this.store = opts.store ?? new MemoryIdempotencyStore();\n this.cacheErrors = opts.cacheErrors ?? false;\n }\n\n /**\n * Run `fn` at most once for `key`. A repeat call replays the stored result.\n *\n * @example\n * const { value, replayed } = await idem.run(idempotencyKey, () => charge(order));\n */\n async run<T>(key: string, fn: () => Promise<T> | T, opts: RunOptions = {}): Promise<RunResult<T>> {\n // Same-process single-flight: concurrent duplicates await the same execution.\n const flying = this.inflight.get(key);\n if (flying) {\n // Apply the same fingerprint check the stored path uses, so a concurrent\n // same-key call with a mismatched fingerprint throws consistently.\n if (opts.fingerprint && flying.fingerprint && flying.fingerprint !== opts.fingerprint) {\n throw new IdempotencyKeyReuseError(key);\n }\n const out = (await flying.promise) as { value: T; fresh: boolean };\n return { value: out.value, replayed: true };\n }\n\n const store = opts.store ?? this.store;\n const cacheErrors = opts.cacheErrors ?? this.cacheErrors;\n const now = opts.now ?? Date.now();\n const record: IdempotencyRecord = { status: \"in-progress\", fingerprint: opts.fingerprint, createdAt: now };\n\n // Register the in-flight promise SYNCHRONOUSLY, before any await, so\n // truly-concurrent callers see it and don't race on create().\n const exec = (async (): Promise<{ value: T; fresh: boolean }> => {\n const existing = await store.get(key);\n if (existing) {\n if (opts.fingerprint && existing.fingerprint && existing.fingerprint !== opts.fingerprint) {\n throw new IdempotencyKeyReuseError(key);\n }\n if (existing.status === \"completed\") return { value: existing.value as T, fresh: false };\n if (existing.status === \"failed\") {\n if (cacheErrors) throw new ReplayedError(existing.error ?? \"Operation previously failed.\");\n await store.delete(key); // retryable → clear and re-run below\n } else {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n }\n const created = await store.create(key, record);\n if (!created) {\n if ((opts.onConflict ?? \"throw\") === \"wait\") return { value: await this.waitFor<T>(store, key, opts), fresh: false };\n throw new IdempotencyConflictError(key);\n }\n try {\n const value = await fn();\n await store.set(key, { status: \"completed\", value, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n return { value, fresh: true };\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n if (cacheErrors) await store.set(key, { status: \"failed\", error: message, fingerprint: opts.fingerprint, createdAt: record.createdAt, completedAt: Date.now() });\n else await store.delete(key);\n throw err;\n }\n })().finally(() => this.inflight.delete(key));\n\n this.inflight.set(key, { promise: exec, fingerprint: opts.fingerprint });\n const out = await exec;\n return { value: out.value, replayed: !out.fresh };\n }\n\n private async waitFor<T>(store: IdempotencyStore, key: string, opts: RunOptions): Promise<T> {\n const interval = opts.pollIntervalMs ?? 50;\n const timeout = opts.waitTimeoutMs ?? 10_000;\n const start = Date.now();\n for (;;) {\n const rec = await store.get(key);\n if (!rec || rec.status === \"completed\") {\n if (rec?.status === \"completed\") return rec.value as T;\n throw new IdempotencyConflictError(key); // vanished mid-wait\n }\n if (rec.status === \"failed\") throw new ReplayedError(rec.error ?? \"Operation failed.\");\n if (Date.now() - start > timeout) throw new IdempotencyConflictError(key);\n await sleep(interval);\n }\n }\n\n /** Forget a key so its operation can run fresh again. */\n async forget(key: string, store?: IdempotencyStore): Promise<void> {\n await (store ?? this.store).delete(key);\n }\n}\n\n/* ------------------------------ functional default ------------------------------ */\n\nconst shared = new Idempotency();\n\n/**\n * Run `fn` at most once for `key`, using a shared in-memory store (pass\n * `opts.store` for your own). Returns `{ value, replayed }`.\n *\n * @example\n * const { value } = await idempotent(req.headers[\"idempotency-key\"], () => createOrder(body));\n */\nexport function idempotent<T>(key: string, fn: () => Promise<T> | T, opts?: RunOptions): Promise<RunResult<T>> {\n return shared.run(key, fn, opts);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacspace/idempotency",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Make any operation exactly-once with an idempotency key — replay stored results on retries, safe under concurrency, with optional request fingerprinting. Framework-agnostic, pluggable store, zero-dependency, isomorphic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
],
|
|
44
44
|
"author": "Lacspace <contact@lacspace.com>",
|
|
45
45
|
"license": "SEE LICENSE IN LICENSE",
|
|
46
|
-
"homepage": "https://lacspace.com/packages",
|
|
46
|
+
"homepage": "https://developer.lacspace.com/packages/idempotency",
|
|
47
47
|
"repository": {
|
|
48
48
|
"type": "git",
|
|
49
49
|
"url": "git+https://github.com/lacspace/npm-packages.git",
|