hono-idempotency 0.7.2 → 0.8.1

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.
Files changed (41) hide show
  1. package/dist/chunk-C2JZZSOP.js +9 -0
  2. package/dist/chunk-C2JZZSOP.js.map +1 -0
  3. package/dist/index.cjs +49 -12
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.cts +2 -2
  6. package/dist/index.d.ts +2 -2
  7. package/dist/index.js +47 -12
  8. package/dist/index.js.map +1 -1
  9. package/dist/stores/cloudflare-d1.cjs +28 -8
  10. package/dist/stores/cloudflare-d1.cjs.map +1 -1
  11. package/dist/stores/cloudflare-d1.d.cts +1 -1
  12. package/dist/stores/cloudflare-d1.d.ts +1 -1
  13. package/dist/stores/cloudflare-d1.js +27 -8
  14. package/dist/stores/cloudflare-d1.js.map +1 -1
  15. package/dist/stores/cloudflare-kv.cjs +32 -8
  16. package/dist/stores/cloudflare-kv.cjs.map +1 -1
  17. package/dist/stores/cloudflare-kv.d.cts +1 -1
  18. package/dist/stores/cloudflare-kv.d.ts +1 -1
  19. package/dist/stores/cloudflare-kv.js +31 -8
  20. package/dist/stores/cloudflare-kv.js.map +1 -1
  21. package/dist/stores/durable-objects.cjs +10 -2
  22. package/dist/stores/durable-objects.cjs.map +1 -1
  23. package/dist/stores/durable-objects.d.cts +1 -1
  24. package/dist/stores/durable-objects.d.ts +1 -1
  25. package/dist/stores/durable-objects.js +9 -2
  26. package/dist/stores/durable-objects.js.map +1 -1
  27. package/dist/stores/memory.cjs +16 -3
  28. package/dist/stores/memory.cjs.map +1 -1
  29. package/dist/stores/memory.d.cts +1 -1
  30. package/dist/stores/memory.d.ts +1 -1
  31. package/dist/stores/memory.js +15 -3
  32. package/dist/stores/memory.js.map +1 -1
  33. package/dist/stores/redis.cjs +33 -5
  34. package/dist/stores/redis.cjs.map +1 -1
  35. package/dist/stores/redis.d.cts +1 -1
  36. package/dist/stores/redis.d.ts +1 -1
  37. package/dist/stores/redis.js +32 -5
  38. package/dist/stores/redis.js.map +1 -1
  39. package/dist/{types-C_OW_leh.d.cts → types-7IwFeI0l.d.cts} +6 -2
  40. package/dist/{types-C_OW_leh.d.ts → types-7IwFeI0l.d.ts} +6 -2
  41. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TABLE = \"idempotency_keys\";\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\nconst TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */\nexport interface D1DatabaseLike {\n\tprepare(sql: string): D1PreparedStatementLike;\n}\n\nexport interface D1PreparedStatementLike {\n\tbind(...params: unknown[]): D1PreparedStatementLike;\n\trun(): Promise<{ success: boolean; meta: { changes: number } }>;\n\tfirst(): Promise<Record<string, unknown> | null>;\n}\n\nexport interface D1StoreOptions {\n\t/** Cloudflare D1 database binding. */\n\tdatabase: D1DatabaseLike;\n\t/** Table name (default: \"idempotency_keys\"). Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/. */\n\ttableName?: string;\n\t/** TTL in seconds (default: 86400 = 24h). Expired rows are ignored by get/lock. */\n\tttl?: number;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE, ttl = DEFAULT_TTL } = options;\n\n\tif (!TABLE_NAME_RE.test(tableName)) {\n\t\tthrow new Error(`Invalid table name: \"${tableName}\". Must match ${TABLE_NAME_RE}`);\n\t}\n\n\tlet initialized = false;\n\n\tconst ensureTable = async (): Promise<void> => {\n\t\tif (initialized) return;\n\t\tawait db\n\t\t\t.prepare(\n\t\t\t\t`CREATE TABLE IF NOT EXISTS ${tableName} (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tfingerprint TEXT NOT NULL,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tresponse TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t)`,\n\t\t\t)\n\t\t\t.run();\n\t\tinitialized = true;\n\t};\n\n\tconst ttlThreshold = (): number => {\n\t\treturn Date.now() - ttl * 1000;\n\t};\n\n\tconst toRecord = (row: Record<string, unknown>): IdempotencyRecord => ({\n\t\tkey: row.key as string,\n\t\tfingerprint: row.fingerprint as string,\n\t\tstatus: row.status as \"processing\" | \"completed\",\n\t\tresponse: row.response ? (JSON.parse(row.response as string) as StoredResponse) : undefined,\n\t\tcreatedAt: row.created_at as number,\n\t});\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tawait ensureTable();\n\t\t\tconst row = await db\n\t\t\t\t.prepare(`SELECT * FROM ${tableName} WHERE key = ? AND created_at >= ?`)\n\t\t\t\t.bind(key, ttlThreshold())\n\t\t\t\t.first();\n\t\t\tif (!row) return undefined;\n\t\t\treturn toRecord(row);\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(\n\t\t\t\t\t`INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at)\n\t\t\t\t\tSELECT ?, ?, ?, ?, ?\n\t\t\t\t\tWHERE NOT EXISTS (SELECT 1 FROM ${tableName} WHERE key = ? AND created_at >= ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt, key, ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes > 0;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tawait ensureTable();\n\t\t\tawait db\n\t\t\t\t.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`)\n\t\t\t\t.bind(\"completed\", JSON.stringify(response), key)\n\t\t\t\t.run();\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait ensureTable();\n\t\t\tawait db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();\n\t\t},\n\n\t\tasync purge() {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(`DELETE FROM ${tableName} WHERE created_at < ?`)\n\t\t\t\t.bind(ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes;\n\t\t},\n\t};\n}\n"],"mappings":";AAGA,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAsBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,eAAe,MAAM,YAAY,IAAI;AAEvE,MAAI,CAAC,cAAc,KAAK,SAAS,GAAG;AACnC,UAAM,IAAI,MAAM,wBAAwB,SAAS,iBAAiB,aAAa,EAAE;AAAA,EAClF;AAEA,MAAI,cAAc;AAElB,QAAM,cAAc,YAA2B;AAC9C,QAAI,YAAa;AACjB,UAAM,GACJ;AAAA,MACA,8BAA8B,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOxC,EACC,IAAI;AACN,kBAAc;AAAA,EACf;AAEA,QAAM,eAAe,MAAc;AAClC,WAAO,KAAK,IAAI,IAAI,MAAM;AAAA,EAC3B;AAEA,QAAM,WAAW,CAAC,SAAqD;AAAA,IACtE,KAAK,IAAI;AAAA,IACT,aAAa,IAAI;AAAA,IACjB,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI,WAAY,KAAK,MAAM,IAAI,QAAkB,IAAuB;AAAA,IAClF,WAAW,IAAI;AAAA,EAChB;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,YAAY;AAClB,YAAM,MAAM,MAAM,GAChB,QAAQ,iBAAiB,SAAS,oCAAoC,EACtE,KAAK,KAAK,aAAa,CAAC,EACxB,MAAM;AACR,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,SAAS,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB;AAAA,QACA,yBAAyB,SAAS;AAAA;AAAA,uCAEA,SAAS;AAAA,MAC5C,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,WAAW,KAAK,aAAa,CAAC,EACxF,IAAI;AACN,aAAO,OAAO,KAAK,UAAU;AAAA,IAC9B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,YAAY;AAClB,YAAM,GACJ,QAAQ,UAAU,SAAS,6CAA6C,EACxE,KAAK,aAAa,KAAK,UAAU,QAAQ,GAAG,GAAG,EAC/C,IAAI;AAAA,IACP;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,YAAY;AAClB,YAAM,GAAG,QAAQ,eAAe,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,IAAI;AAAA,IAC1E;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,eAAe,SAAS,uBAAuB,EACvD,KAAK,aAAa,CAAC,EACnB,IAAI;AACN,aAAO,OAAO,KAAK;AAAA,IACpB;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\ttype RECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TABLE = \"idempotency_keys\";\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\nconst TABLE_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */\nexport interface D1DatabaseLike {\n\tprepare(sql: string): D1PreparedStatementLike;\n}\n\nexport interface D1PreparedStatementLike {\n\tbind(...params: unknown[]): D1PreparedStatementLike;\n\trun(): Promise<{ success: boolean; meta: { changes: number } }>;\n\tfirst(): Promise<Record<string, unknown> | null>;\n}\n\nexport interface D1StoreOptions {\n\t/** Cloudflare D1 database binding. */\n\tdatabase: D1DatabaseLike;\n\t/** Table name (default: \"idempotency_keys\"). Must match /^[a-zA-Z_][a-zA-Z0-9_]*$/. */\n\ttableName?: string;\n\t/** TTL in seconds (default: 86400 = 24h). Expired rows are ignored by get/lock. */\n\tttl?: number;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE, ttl = DEFAULT_TTL } = options;\n\n\tif (!TABLE_NAME_RE.test(tableName)) {\n\t\tthrow new Error(`Invalid table name: \"${tableName}\". Must match ${TABLE_NAME_RE}`);\n\t}\n\n\tlet initialized = false;\n\n\tconst ensureTable = async (): Promise<void> => {\n\t\tif (initialized) return;\n\t\tawait db\n\t\t\t.prepare(\n\t\t\t\t`CREATE TABLE IF NOT EXISTS ${tableName} (\n\t\t\t\tkey TEXT PRIMARY KEY,\n\t\t\t\tfingerprint TEXT NOT NULL,\n\t\t\t\tstatus TEXT NOT NULL,\n\t\t\t\tresponse TEXT,\n\t\t\t\tcreated_at INTEGER NOT NULL\n\t\t\t)`,\n\t\t\t)\n\t\t\t.run();\n\t\tinitialized = true;\n\t};\n\n\tconst ttlThreshold = (): number => {\n\t\treturn Date.now() - ttl * 1000;\n\t};\n\n\tconst toRecord = (row: Record<string, unknown>): IdempotencyRecord => {\n\t\tlet response: StoredResponse | undefined;\n\t\tif (row.response) {\n\t\t\ttry {\n\t\t\t\tresponse = JSON.parse(row.response as string) as StoredResponse;\n\t\t\t} catch {\n\t\t\t\t// Corrupt JSON in storage — degrade gracefully like other stores\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tkey: row.key as string,\n\t\t\tfingerprint: row.fingerprint as string,\n\t\t\tstatus: row.status as typeof RECORD_STATUS_PROCESSING | typeof RECORD_STATUS_COMPLETED,\n\t\t\tresponse,\n\t\t\tcreatedAt: row.created_at as number,\n\t\t};\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tawait ensureTable();\n\t\t\tconst row = await db\n\t\t\t\t.prepare(`SELECT * FROM ${tableName} WHERE key = ? AND created_at >= ?`)\n\t\t\t\t.bind(key, ttlThreshold())\n\t\t\t\t.first();\n\t\t\tif (!row) return undefined;\n\t\t\treturn toRecord(row);\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(\n\t\t\t\t\t`INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at)\n\t\t\t\t\tSELECT ?, ?, ?, ?, ?\n\t\t\t\t\tWHERE NOT EXISTS (SELECT 1 FROM ${tableName} WHERE key = ? AND created_at >= ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt, key, ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes > 0;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tawait ensureTable();\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(response);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait db\n\t\t\t\t.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`)\n\t\t\t\t.bind(RECORD_STATUS_COMPLETED, serialized, key)\n\t\t\t\t.run();\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait ensureTable();\n\t\t\tawait db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();\n\t\t},\n\n\t\tasync purge() {\n\t\t\tawait ensureTable();\n\t\t\tconst result = await db\n\t\t\t\t.prepare(`DELETE FROM ${tableName} WHERE created_at < ?`)\n\t\t\t\t.bind(ttlThreshold())\n\t\t\t\t.run();\n\t\t\treturn result.meta.changes;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;AAQA,IAAM,gBAAgB;AACtB,IAAM,cAAc;AACpB,IAAM,gBAAgB;AAsBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,eAAe,MAAM,YAAY,IAAI;AAEvE,MAAI,CAAC,cAAc,KAAK,SAAS,GAAG;AACnC,UAAM,IAAI,MAAM,wBAAwB,SAAS,iBAAiB,aAAa,EAAE;AAAA,EAClF;AAEA,MAAI,cAAc;AAElB,QAAM,cAAc,YAA2B;AAC9C,QAAI,YAAa;AACjB,UAAM,GACJ;AAAA,MACA,8BAA8B,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAOxC,EACC,IAAI;AACN,kBAAc;AAAA,EACf;AAEA,QAAM,eAAe,MAAc;AAClC,WAAO,KAAK,IAAI,IAAI,MAAM;AAAA,EAC3B;AAEA,QAAM,WAAW,CAAC,QAAoD;AACrE,QAAI;AACJ,QAAI,IAAI,UAAU;AACjB,UAAI;AACH,mBAAW,KAAK,MAAM,IAAI,QAAkB;AAAA,MAC7C,QAAQ;AAAA,MAER;AAAA,IACD;AACA,WAAO;AAAA,MACN,KAAK,IAAI;AAAA,MACT,aAAa,IAAI;AAAA,MACjB,QAAQ,IAAI;AAAA,MACZ;AAAA,MACA,WAAW,IAAI;AAAA,IAChB;AAAA,EACD;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,YAAY;AAClB,YAAM,MAAM,MAAM,GAChB,QAAQ,iBAAiB,SAAS,oCAAoC,EACtE,KAAK,KAAK,aAAa,CAAC,EACxB,MAAM;AACR,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,SAAS,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB;AAAA,QACA,yBAAyB,SAAS;AAAA;AAAA,uCAEA,SAAS;AAAA,MAC5C,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,WAAW,KAAK,aAAa,CAAC,EACxF,IAAI;AACN,aAAO,OAAO,KAAK,UAAU;AAAA,IAC9B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,YAAY;AAClB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GACJ,QAAQ,UAAU,SAAS,6CAA6C,EACxE,KAAK,yBAAyB,YAAY,GAAG,EAC7C,IAAI;AAAA,IACP;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,YAAY;AAClB,YAAM,GAAG,QAAQ,eAAe,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,IAAI;AAAA,IAC1E;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,YAAY;AAClB,YAAM,SAAS,MAAM,GACnB,QAAQ,eAAe,SAAS,uBAAuB,EACvD,KAAK,aAAa,CAAC,EACnB,IAAI;AACN,aAAO,OAAO,KAAK;AAAA,IACpB;AAAA,EACD;AACD;","names":[]}
@@ -23,27 +23,51 @@ __export(cloudflare_kv_exports, {
23
23
  kvStore: () => kvStore
24
24
  });
25
25
  module.exports = __toCommonJS(cloudflare_kv_exports);
26
+
27
+ // src/types.ts
28
+ var RECORD_STATUS_COMPLETED = "completed";
29
+
30
+ // src/stores/cloudflare-kv.ts
26
31
  var DEFAULT_TTL = 86400;
27
32
  function kvStore(options) {
28
33
  const { namespace: kv, ttl = DEFAULT_TTL } = options;
29
34
  return {
30
35
  async get(key) {
31
- const record = await kv.get(key, { type: "json" });
32
- return record ?? void 0;
36
+ const raw = await kv.get(key, { type: "json" });
37
+ if (!raw) return void 0;
38
+ const { lockId: _, ...record } = raw;
39
+ return record;
33
40
  },
34
41
  async lock(key, record) {
35
42
  const existing = await kv.get(key, { type: "json" });
36
43
  if (existing) return false;
37
- await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
44
+ const lockId = crypto.randomUUID();
45
+ const withLock = { ...record, lockId };
46
+ let serialized;
47
+ try {
48
+ serialized = JSON.stringify(withLock);
49
+ } catch {
50
+ return false;
51
+ }
52
+ await kv.put(key, serialized, { expirationTtl: ttl });
38
53
  const stored = await kv.get(key, { type: "json" });
39
- return stored?.fingerprint === record.fingerprint;
54
+ return stored?.lockId === lockId;
40
55
  },
41
56
  async complete(key, response) {
42
- const record = await kv.get(key, { type: "json" });
43
- if (!record) return;
44
- record.status = "completed";
57
+ const raw = await kv.get(key, { type: "json" });
58
+ if (!raw) return;
59
+ const { lockId: _, ...record } = raw;
60
+ record.status = RECORD_STATUS_COMPLETED;
45
61
  record.response = response;
46
- await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
62
+ const elapsed = Math.floor((Date.now() - record.createdAt) / 1e3);
63
+ const remaining = Math.max(1, ttl - elapsed);
64
+ let serialized;
65
+ try {
66
+ serialized = JSON.stringify(record);
67
+ } catch {
68
+ return;
69
+ }
70
+ await kv.put(key, serialized, { expirationTtl: remaining });
47
71
  },
48
72
  async delete(key) {
49
73
  await kv.delete(key);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn record ?? undefined;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\n\t\t\t// Read-back verification: detect if a concurrent writer overwrote our record\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn stored?.fingerprint === record.fingerprint;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (!record) return;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,UAAU;AAAA,IAClB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAErB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAGhE,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,QAAQ,gBAAgB,OAAO;AAAA,IACvC;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAAA,IACjE;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/cloudflare-kv.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return undefined;\n\t\t\t// Strip internal lockId before returning to consumers\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\t// Embed a unique lockId to distinguish concurrent writers with the same fingerprint\n\t\t\tconst lockId = crypto.randomUUID();\n\t\t\tconst withLock = { ...record, lockId };\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(withLock);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: ttl });\n\n\t\t\t// Read-back verification using lockId (not fingerprint) for reliable race detection\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\treturn stored?.lockId === lockId;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return;\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/** Maximum request body size in bytes. Pre-checked via Content-Length header, then enforced against actual body byte length. */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK,QAAO;AAEjB,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAGrB,YAAM,SAAS,OAAO,WAAW;AACjC,YAAM,WAAW,EAAE,GAAG,QAAQ,OAAO;AACrC,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,IAAI,CAAC;AAGpD,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAGlD,aAAO,QAAQ,WAAW;AAAA,IAC3B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK;AACV,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,UAAU,CAAC;AAAA,IAC3D;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.cjs';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.js';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
@@ -1,25 +1,48 @@
1
+ import {
2
+ RECORD_STATUS_COMPLETED
3
+ } from "../chunk-C2JZZSOP.js";
4
+
1
5
  // src/stores/cloudflare-kv.ts
2
6
  var DEFAULT_TTL = 86400;
3
7
  function kvStore(options) {
4
8
  const { namespace: kv, ttl = DEFAULT_TTL } = options;
5
9
  return {
6
10
  async get(key) {
7
- const record = await kv.get(key, { type: "json" });
8
- return record ?? void 0;
11
+ const raw = await kv.get(key, { type: "json" });
12
+ if (!raw) return void 0;
13
+ const { lockId: _, ...record } = raw;
14
+ return record;
9
15
  },
10
16
  async lock(key, record) {
11
17
  const existing = await kv.get(key, { type: "json" });
12
18
  if (existing) return false;
13
- await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
19
+ const lockId = crypto.randomUUID();
20
+ const withLock = { ...record, lockId };
21
+ let serialized;
22
+ try {
23
+ serialized = JSON.stringify(withLock);
24
+ } catch {
25
+ return false;
26
+ }
27
+ await kv.put(key, serialized, { expirationTtl: ttl });
14
28
  const stored = await kv.get(key, { type: "json" });
15
- return stored?.fingerprint === record.fingerprint;
29
+ return stored?.lockId === lockId;
16
30
  },
17
31
  async complete(key, response) {
18
- const record = await kv.get(key, { type: "json" });
19
- if (!record) return;
20
- record.status = "completed";
32
+ const raw = await kv.get(key, { type: "json" });
33
+ if (!raw) return;
34
+ const { lockId: _, ...record } = raw;
35
+ record.status = RECORD_STATUS_COMPLETED;
21
36
  record.response = response;
22
- await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
37
+ const elapsed = Math.floor((Date.now() - record.createdAt) / 1e3);
38
+ const remaining = Math.max(1, ttl - elapsed);
39
+ let serialized;
40
+ try {
41
+ serialized = JSON.stringify(record);
42
+ } catch {
43
+ return;
44
+ }
45
+ await kv.put(key, serialized, { expirationTtl: remaining });
23
46
  },
24
47
  async delete(key) {
25
48
  await kv.delete(key);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn record ?? undefined;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\n\t\t\t// Read-back verification: detect if a concurrent writer overwrote our record\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\treturn stored?.fingerprint === record.fingerprint;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (!record) return;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,UAAU;AAAA,IAClB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAErB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAGhE,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,aAAO,QAAQ,gBAAgB,OAAO;AAAA,IACvC;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAClD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAAA,IACjE;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */\nexport interface KVNamespaceLike {\n\tget(key: string, options: { type: \"json\" }): Promise<unknown>;\n\tput(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;\n\tdelete(key: string): Promise<void>;\n}\n\nexport interface KVStoreOptions {\n\t/** Cloudflare Workers KV namespace binding. */\n\tnamespace: KVNamespaceLike;\n\t/** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */\n\tttl?: number;\n}\n\n/**\n * KV is eventually consistent — lock() uses write-first with read-back\n * verification for best-effort race detection but cannot guarantee atomicity.\n * For strict concurrency guarantees, use d1Store or durableObjectStore.\n */\nexport function kvStore(options: KVStoreOptions): IdempotencyStore {\n\tconst { namespace: kv, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return undefined;\n\t\t\t// Strip internal lockId before returning to consumers\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = (await kv.get(key, { type: \"json\" })) as IdempotencyRecord | null;\n\t\t\tif (existing) return false;\n\n\t\t\t// Embed a unique lockId to distinguish concurrent writers with the same fingerprint\n\t\t\tconst lockId = crypto.randomUUID();\n\t\t\tconst withLock = { ...record, lockId };\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(withLock);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: ttl });\n\n\t\t\t// Read-back verification using lockId (not fingerprint) for reliable race detection\n\t\t\tconst stored = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\treturn stored?.lockId === lockId;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = (await kv.get(key, { type: \"json\" })) as\n\t\t\t\t| (IdempotencyRecord & { lockId?: string })\n\t\t\t\t| null;\n\t\t\tif (!raw) return;\n\t\t\tconst { lockId: _, ...record } = raw;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait kv.put(key, serialized, { expirationTtl: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait kv.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// KV handles expiration automatically via expirationTtl — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;AAGA,IAAM,cAAc;AAqBb,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,WAAW,IAAI,MAAM,YAAY,IAAI;AAE7C,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK,QAAO;AAEjB,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAY,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AACpD,UAAI,SAAU,QAAO;AAGrB,YAAM,SAAS,OAAO,WAAW;AACjC,YAAM,WAAW,EAAE,GAAG,QAAQ,OAAO;AACrC,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,QAAQ;AAAA,MACrC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,IAAI,CAAC;AAGpD,YAAM,SAAU,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAGlD,aAAO,QAAQ,WAAW;AAAA,IAC3B;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAO,MAAM,GAAG,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC;AAG/C,UAAI,CAAC,IAAK;AACV,YAAM,EAAE,QAAQ,GAAG,GAAG,OAAO,IAAI;AACjC,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,GAAG,IAAI,KAAK,YAAY,EAAE,eAAe,UAAU,CAAC;AAAA,IAC3D;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,GAAG,OAAO,GAAG;AAAA,IACpB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -23,6 +23,11 @@ __export(durable_objects_exports, {
23
23
  durableObjectStore: () => durableObjectStore
24
24
  });
25
25
  module.exports = __toCommonJS(durable_objects_exports);
26
+
27
+ // src/types.ts
28
+ var RECORD_STATUS_COMPLETED = "completed";
29
+
30
+ // src/stores/durable-objects.ts
26
31
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
27
32
  function durableObjectStore(options) {
28
33
  const { storage, ttl = DEFAULT_TTL } = options;
@@ -33,7 +38,10 @@ function durableObjectStore(options) {
33
38
  async get(key) {
34
39
  const record = await storage.get(key);
35
40
  if (!record) return void 0;
36
- if (isExpired(record)) return void 0;
41
+ if (isExpired(record)) {
42
+ await storage.delete(key);
43
+ return void 0;
44
+ }
37
45
  return record;
38
46
  },
39
47
  async lock(key, record) {
@@ -47,7 +55,7 @@ function durableObjectStore(options) {
47
55
  async complete(key, response) {
48
56
  const record = await storage.get(key);
49
57
  if (!record) return;
50
- record.status = "completed";
58
+ record.status = RECORD_STATUS_COMPLETED;
51
59
  record.response = response;
52
60
  await storage.put(key, record);
53
61
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/durable-objects.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms\n\n/** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */\nexport interface DurableObjectStorageLike {\n\tget<T>(key: string): Promise<T | undefined>;\n\tput<T>(key: string, value: T): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(options?: { prefix?: string }): Promise<Map<string, unknown>>;\n}\n\nexport interface DurableObjectStoreOptions {\n\t/** Durable Object storage instance (from `this.ctx.storage` inside a DO class). */\n\tstorage: DurableObjectStorageLike;\n\t/** TTL in milliseconds (default: 86400000 = 24h). */\n\tttl?: number;\n}\n\nexport function durableObjectStore(options: DurableObjectStoreOptions): IdempotencyStore {\n\tconst { storage, ttl = DEFAULT_TTL } = options;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) return undefined;\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait storage.put(key, record);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait storage.put(key, record);\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait storage.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tconst entries = await storage.list();\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tconst record = value as IdempotencyRecord;\n\t\t\t\tif (record.createdAt !== undefined && isExpired(record)) {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAiB5B,SAAS,mBAAmB,SAAsD;AACxF,QAAM,EAAE,SAAS,MAAM,YAAY,IAAI;AAEvC,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,EAAG,QAAO;AAC9B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAW,MAAM,QAAQ,IAAuB,GAAG;AACzD,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,YAAM,QAAQ,IAAI,KAAK,MAAM;AAC7B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,QAAQ,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,cAAM,SAAS;AACf,YAAI,OAAO,cAAc,UAAa,UAAU,MAAM,GAAG;AACxD,gBAAM,QAAQ,OAAO,GAAG;AACxB;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/durable-objects.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms\n\n/** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */\nexport interface DurableObjectStorageLike {\n\tget<T>(key: string): Promise<T | undefined>;\n\tput<T>(key: string, value: T): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(options?: { prefix?: string }): Promise<Map<string, unknown>>;\n}\n\nexport interface DurableObjectStoreOptions {\n\t/** Durable Object storage instance (from `this.ctx.storage` inside a DO class). */\n\tstorage: DurableObjectStorageLike;\n\t/** TTL in milliseconds (default: 86400000 = 24h). */\n\tttl?: number;\n}\n\nexport function durableObjectStore(options: DurableObjectStoreOptions): IdempotencyStore {\n\tconst { storage, ttl = DEFAULT_TTL } = options;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tawait storage.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait storage.put(key, record);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tawait storage.put(key, record);\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait storage.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tconst entries = await storage.list();\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tconst record = value as IdempotencyRecord;\n\t\t\t\tif (record.createdAt !== undefined && isExpired(record)) {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/** Maximum request body size in bytes. Pre-checked via Content-Length header, then enforced against actual body byte length. */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc,KAAK,KAAK,KAAK;AAiB5B,SAAS,mBAAmB,SAAsD;AACxF,QAAM,EAAE,SAAS,MAAM,YAAY,IAAI;AAEvC,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,cAAM,QAAQ,OAAO,GAAG;AACxB,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAW,MAAM,QAAQ,IAAuB,GAAG;AACzD,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,YAAM,QAAQ,IAAI,KAAK,MAAM;AAC7B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,QAAQ,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,cAAM,SAAS;AACf,YAAI,OAAO,cAAc,UAAa,UAAU,MAAM,GAAG;AACxD,gBAAM,QAAQ,OAAO,GAAG;AACxB;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.cjs';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.js';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */
@@ -1,3 +1,7 @@
1
+ import {
2
+ RECORD_STATUS_COMPLETED
3
+ } from "../chunk-C2JZZSOP.js";
4
+
1
5
  // src/stores/durable-objects.ts
2
6
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
3
7
  function durableObjectStore(options) {
@@ -9,7 +13,10 @@ function durableObjectStore(options) {
9
13
  async get(key) {
10
14
  const record = await storage.get(key);
11
15
  if (!record) return void 0;
12
- if (isExpired(record)) return void 0;
16
+ if (isExpired(record)) {
17
+ await storage.delete(key);
18
+ return void 0;
19
+ }
13
20
  return record;
14
21
  },
15
22
  async lock(key, record) {
@@ -23,7 +30,7 @@ function durableObjectStore(options) {
23
30
  async complete(key, response) {
24
31
  const record = await storage.get(key);
25
32
  if (!record) return;
26
- record.status = "completed";
33
+ record.status = RECORD_STATUS_COMPLETED;
27
34
  record.response = response;
28
35
  await storage.put(key, record);
29
36
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/durable-objects.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms\n\n/** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */\nexport interface DurableObjectStorageLike {\n\tget<T>(key: string): Promise<T | undefined>;\n\tput<T>(key: string, value: T): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(options?: { prefix?: string }): Promise<Map<string, unknown>>;\n}\n\nexport interface DurableObjectStoreOptions {\n\t/** Durable Object storage instance (from `this.ctx.storage` inside a DO class). */\n\tstorage: DurableObjectStorageLike;\n\t/** TTL in milliseconds (default: 86400000 = 24h). */\n\tttl?: number;\n}\n\nexport function durableObjectStore(options: DurableObjectStoreOptions): IdempotencyStore {\n\tconst { storage, ttl = DEFAULT_TTL } = options;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) return undefined;\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait storage.put(key, record);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait storage.put(key, record);\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait storage.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tconst entries = await storage.list();\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tconst record = value as IdempotencyRecord;\n\t\t\t\tif (record.createdAt !== undefined && isExpired(record)) {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAiB5B,SAAS,mBAAmB,SAAsD;AACxF,QAAM,EAAE,SAAS,MAAM,YAAY,IAAI;AAEvC,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,EAAG,QAAO;AAC9B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAW,MAAM,QAAQ,IAAuB,GAAG;AACzD,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,YAAM,QAAQ,IAAI,KAAK,MAAM;AAC7B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,QAAQ,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,cAAM,SAAS;AACf,YAAI,OAAO,cAAc,UAAa,UAAU,MAAM,GAAG;AACxD,gBAAM,QAAQ,OAAO,GAAG;AACxB;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/durable-objects.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours in ms\n\n/** Minimal DurableObjectStorage subset (avoids @cloudflare/workers-types dependency). */\nexport interface DurableObjectStorageLike {\n\tget<T>(key: string): Promise<T | undefined>;\n\tput<T>(key: string, value: T): Promise<void>;\n\tdelete(key: string): Promise<boolean>;\n\tlist(options?: { prefix?: string }): Promise<Map<string, unknown>>;\n}\n\nexport interface DurableObjectStoreOptions {\n\t/** Durable Object storage instance (from `this.ctx.storage` inside a DO class). */\n\tstorage: DurableObjectStorageLike;\n\t/** TTL in milliseconds (default: 86400000 = 24h). */\n\tttl?: number;\n}\n\nexport function durableObjectStore(options: DurableObjectStoreOptions): IdempotencyStore {\n\tconst { storage, ttl = DEFAULT_TTL } = options;\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tawait storage.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst existing = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait storage.put(key, record);\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = await storage.get<IdempotencyRecord>(key);\n\t\t\tif (!record) return;\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tawait storage.put(key, record);\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait storage.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tconst entries = await storage.list();\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, value] of entries) {\n\t\t\t\tconst record = value as IdempotencyRecord;\n\t\t\t\tif (record.createdAt !== undefined && isExpired(record)) {\n\t\t\t\t\tawait storage.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAiB5B,SAAS,mBAAmB,SAAsD;AACxF,QAAM,EAAE,SAAS,MAAM,YAAY,IAAI;AAEvC,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,cAAM,QAAQ,OAAO,GAAG;AACxB,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,WAAW,MAAM,QAAQ,IAAuB,GAAG;AACzD,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,YAAM,QAAQ,IAAI,KAAK,MAAM;AAC7B,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,MAAM,QAAQ,IAAuB,GAAG;AACvD,UAAI,CAAC,OAAQ;AACb,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,QAAQ,IAAI,KAAK,MAAM;AAAA,IAC9B;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,QAAQ,OAAO,GAAG;AAAA,IACzB;AAAA,IAEA,MAAM,QAAQ;AACb,YAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,KAAK,KAAK,SAAS;AACnC,cAAM,SAAS;AACf,YAAI,OAAO,cAAc,UAAa,UAAU,MAAM,GAAG;AACxD,gBAAM,QAAQ,OAAO,GAAG;AACxB;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -23,6 +23,12 @@ __export(memory_exports, {
23
23
  memoryStore: () => memoryStore
24
24
  });
25
25
  module.exports = __toCommonJS(memory_exports);
26
+
27
+ // src/types.ts
28
+ var RECORD_STATUS_PROCESSING = "processing";
29
+ var RECORD_STATUS_COMPLETED = "completed";
30
+
31
+ // src/stores/memory.ts
26
32
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
27
33
  function memoryStore(options = {}) {
28
34
  const ttl = options.ttl ?? DEFAULT_TTL;
@@ -60,8 +66,15 @@ function memoryStore(options = {}) {
60
66
  map.set(key, record);
61
67
  if (maxSize !== void 0) {
62
68
  while (map.size > maxSize) {
63
- const oldest = map.keys().next().value;
64
- if (oldest !== void 0) map.delete(oldest);
69
+ let evicted = false;
70
+ for (const [k, r] of map) {
71
+ if (r.status !== RECORD_STATUS_PROCESSING) {
72
+ map.delete(k);
73
+ evicted = true;
74
+ break;
75
+ }
76
+ }
77
+ if (!evicted) break;
65
78
  }
66
79
  }
67
80
  return true;
@@ -69,7 +82,7 @@ function memoryStore(options = {}) {
69
82
  async complete(key, response) {
70
83
  const record = map.get(key);
71
84
  if (record) {
72
- record.status = "completed";
85
+ record.status = RECORD_STATUS_COMPLETED;
73
86
  record.response = response;
74
87
  }
75
88
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/memory.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface MemoryStoreOptions {\n\tttl?: number;\n\t/** Maximum number of entries. Oldest entries are evicted when exceeded. */\n\tmaxSize?: number;\n}\n\nexport interface MemoryStore extends IdempotencyStore {\n\t/** Number of entries currently in the store (including expired but not yet swept). */\n\treadonly size: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): MemoryStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst maxSize = options.maxSize;\n\tconst map = new Map<string, IdempotencyRecord>();\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\tconst sweep = (): void => {\n\t\tfor (const [key, record] of map) {\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tget size() {\n\t\t\treturn map.size;\n\t\t},\n\n\t\tasync get(key) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tsweep();\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tmap.set(key, record);\n\t\t\tif (maxSize !== undefined) {\n\t\t\t\twhile (map.size > maxSize) {\n\t\t\t\t\tconst oldest = map.keys().next().value;\n\t\t\t\t\tif (oldest !== undefined) map.delete(oldest);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (record) {\n\t\t\t\trecord.status = \"completed\";\n\t\t\t\trecord.response = response;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tmap.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, record] of map) {\n\t\t\t\tif (isExpired(record)) {\n\t\t\t\t\tmap.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAa5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ;AACxB,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,QAAM,QAAQ,MAAY;AACzB,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,IAAI,OAAO;AACV,aAAO,IAAI;AAAA,IACZ;AAAA,IAEA,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AACd,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM;AACN,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,UAAI,YAAY,QAAW;AAC1B,eAAO,IAAI,OAAO,SAAS;AAC1B,gBAAM,SAAS,IAAI,KAAK,EAAE,KAAK,EAAE;AACjC,cAAI,WAAW,OAAW,KAAI,OAAO,MAAM;AAAA,QAC5C;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,QAAQ;AACX,eAAO,SAAS;AAChB,eAAO,WAAW;AAAA,MACnB;AAAA,IACD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,UAAI,OAAO,GAAG;AAAA,IACf;AAAA,IAEA,MAAM,QAAQ;AACb,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,YAAI,UAAU,MAAM,GAAG;AACtB,cAAI,OAAO,GAAG;AACd;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/memory.ts","../../src/types.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface MemoryStoreOptions {\n\tttl?: number;\n\t/** Maximum number of entries. Oldest entries are evicted when exceeded. */\n\tmaxSize?: number;\n}\n\nexport interface MemoryStore extends IdempotencyStore {\n\t/** Number of entries currently in the store (including expired but not yet swept). */\n\treadonly size: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): MemoryStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst maxSize = options.maxSize;\n\tconst map = new Map<string, IdempotencyRecord>();\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\tconst sweep = (): void => {\n\t\tfor (const [key, record] of map) {\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tget size() {\n\t\t\treturn map.size;\n\t\t},\n\n\t\tasync get(key) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tsweep();\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tmap.set(key, record);\n\t\t\tif (maxSize !== undefined) {\n\t\t\t\twhile (map.size > maxSize) {\n\t\t\t\t\tlet evicted = false;\n\t\t\t\t\tfor (const [k, r] of map) {\n\t\t\t\t\t\tif (r.status !== RECORD_STATUS_PROCESSING) {\n\t\t\t\t\t\t\tmap.delete(k);\n\t\t\t\t\t\t\tevicted = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!evicted) break;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (record) {\n\t\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\t\trecord.response = response;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tmap.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, record] of map) {\n\t\t\t\tif (isExpired(record)) {\n\t\t\t\t\tmap.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/** Maximum request body size in bytes. Pre-checked via Content-Length header, then enforced against actual body byte length. */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACIO,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;;;ADGvC,IAAM,cAAc,KAAK,KAAK,KAAK;AAa5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ;AACxB,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,QAAM,QAAQ,MAAY;AACzB,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,IAAI,OAAO;AACV,aAAO,IAAI;AAAA,IACZ;AAAA,IAEA,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AACd,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM;AACN,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,UAAI,YAAY,QAAW;AAC1B,eAAO,IAAI,OAAO,SAAS;AAC1B,cAAI,UAAU;AACd,qBAAW,CAAC,GAAG,CAAC,KAAK,KAAK;AACzB,gBAAI,EAAE,WAAW,0BAA0B;AAC1C,kBAAI,OAAO,CAAC;AACZ,wBAAU;AACV;AAAA,YACD;AAAA,UACD;AACA,cAAI,CAAC,QAAS;AAAA,QACf;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,QAAQ;AACX,eAAO,SAAS;AAChB,eAAO,WAAW;AAAA,MACnB;AAAA,IACD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,UAAI,OAAO,GAAG;AAAA,IACf;AAAA,IAEA,MAAM,QAAQ;AACb,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,YAAI,UAAU,MAAM,GAAG;AACtB,cAAI,OAAO,GAAG;AACd;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.cjs';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.cjs';
2
2
  import 'hono';
3
3
 
4
4
  interface MemoryStoreOptions {
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.js';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.js';
2
2
  import 'hono';
3
3
 
4
4
  interface MemoryStoreOptions {
@@ -1,3 +1,8 @@
1
+ import {
2
+ RECORD_STATUS_COMPLETED,
3
+ RECORD_STATUS_PROCESSING
4
+ } from "../chunk-C2JZZSOP.js";
5
+
1
6
  // src/stores/memory.ts
2
7
  var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
3
8
  function memoryStore(options = {}) {
@@ -36,8 +41,15 @@ function memoryStore(options = {}) {
36
41
  map.set(key, record);
37
42
  if (maxSize !== void 0) {
38
43
  while (map.size > maxSize) {
39
- const oldest = map.keys().next().value;
40
- if (oldest !== void 0) map.delete(oldest);
44
+ let evicted = false;
45
+ for (const [k, r] of map) {
46
+ if (r.status !== RECORD_STATUS_PROCESSING) {
47
+ map.delete(k);
48
+ evicted = true;
49
+ break;
50
+ }
51
+ }
52
+ if (!evicted) break;
41
53
  }
42
54
  }
43
55
  return true;
@@ -45,7 +57,7 @@ function memoryStore(options = {}) {
45
57
  async complete(key, response) {
46
58
  const record = map.get(key);
47
59
  if (record) {
48
- record.status = "completed";
60
+ record.status = RECORD_STATUS_COMPLETED;
49
61
  record.response = response;
50
62
  }
51
63
  },
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/memory.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface MemoryStoreOptions {\n\tttl?: number;\n\t/** Maximum number of entries. Oldest entries are evicted when exceeded. */\n\tmaxSize?: number;\n}\n\nexport interface MemoryStore extends IdempotencyStore {\n\t/** Number of entries currently in the store (including expired but not yet swept). */\n\treadonly size: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): MemoryStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst maxSize = options.maxSize;\n\tconst map = new Map<string, IdempotencyRecord>();\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\tconst sweep = (): void => {\n\t\tfor (const [key, record] of map) {\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tget size() {\n\t\t\treturn map.size;\n\t\t},\n\n\t\tasync get(key) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tsweep();\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tmap.set(key, record);\n\t\t\tif (maxSize !== undefined) {\n\t\t\t\twhile (map.size > maxSize) {\n\t\t\t\t\tconst oldest = map.keys().next().value;\n\t\t\t\t\tif (oldest !== undefined) map.delete(oldest);\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (record) {\n\t\t\t\trecord.status = \"completed\";\n\t\t\t\trecord.response = response;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tmap.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, record] of map) {\n\t\t\t\tif (isExpired(record)) {\n\t\t\t\t\tmap.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAa5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ;AACxB,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,QAAM,QAAQ,MAAY;AACzB,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,IAAI,OAAO;AACV,aAAO,IAAI;AAAA,IACZ;AAAA,IAEA,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AACd,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM;AACN,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,UAAI,YAAY,QAAW;AAC1B,eAAO,IAAI,OAAO,SAAS;AAC1B,gBAAM,SAAS,IAAI,KAAK,EAAE,KAAK,EAAE;AACjC,cAAI,WAAW,OAAW,KAAI,OAAO,MAAM;AAAA,QAC5C;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,QAAQ;AACX,eAAO,SAAS;AAChB,eAAO,WAAW;AAAA,MACnB;AAAA,IACD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,UAAI,OAAO,GAAG;AAAA,IACf;AAAA,IAEA,MAAM,QAAQ;AACb,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,YAAI,UAAU,MAAM,GAAG;AACtB,cAAI,OAAO,GAAG;AACd;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/memory.ts"],"sourcesContent":["import {\n\ttype IdempotencyRecord,\n\tRECORD_STATUS_COMPLETED,\n\tRECORD_STATUS_PROCESSING,\n\ttype StoredResponse,\n} from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 24 * 60 * 60 * 1000; // 24 hours\n\nexport interface MemoryStoreOptions {\n\tttl?: number;\n\t/** Maximum number of entries. Oldest entries are evicted when exceeded. */\n\tmaxSize?: number;\n}\n\nexport interface MemoryStore extends IdempotencyStore {\n\t/** Number of entries currently in the store (including expired but not yet swept). */\n\treadonly size: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): MemoryStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\n\tconst maxSize = options.maxSize;\n\tconst map = new Map<string, IdempotencyRecord>();\n\n\tconst isExpired = (record: IdempotencyRecord): boolean => {\n\t\treturn Date.now() - record.createdAt >= ttl;\n\t};\n\n\tconst sweep = (): void => {\n\t\tfor (const [key, record] of map) {\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t}\n\t\t}\n\t};\n\n\treturn {\n\t\tget size() {\n\t\t\treturn map.size;\n\t\t},\n\n\t\tasync get(key) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (!record) return undefined;\n\t\t\tif (isExpired(record)) {\n\t\t\t\tmap.delete(key);\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn record;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tsweep();\n\t\t\tconst existing = map.get(key);\n\t\t\tif (existing && !isExpired(existing)) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tmap.set(key, record);\n\t\t\tif (maxSize !== undefined) {\n\t\t\t\twhile (map.size > maxSize) {\n\t\t\t\t\tlet evicted = false;\n\t\t\t\t\tfor (const [k, r] of map) {\n\t\t\t\t\t\tif (r.status !== RECORD_STATUS_PROCESSING) {\n\t\t\t\t\t\t\tmap.delete(k);\n\t\t\t\t\t\t\tevicted = true;\n\t\t\t\t\t\t\tbreak;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (!evicted) break;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true;\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst record = map.get(key);\n\t\t\tif (record) {\n\t\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\t\trecord.response = response;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tmap.delete(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\tlet count = 0;\n\t\t\tfor (const [key, record] of map) {\n\t\t\t\tif (isExpired(record)) {\n\t\t\t\t\tmap.delete(key);\n\t\t\t\t\tcount++;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn count;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;AAQA,IAAM,cAAc,KAAK,KAAK,KAAK;AAa5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,UAAU,QAAQ;AACxB,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,QAAM,QAAQ,MAAY;AACzB,eAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAEA,SAAO;AAAA,IACN,IAAI,OAAO;AACV,aAAO,IAAI;AAAA,IACZ;AAAA,IAEA,MAAM,IAAI,KAAK;AACd,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,CAAC,OAAQ,QAAO;AACpB,UAAI,UAAU,MAAM,GAAG;AACtB,YAAI,OAAO,GAAG;AACd,eAAO;AAAA,MACR;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM;AACN,YAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,UAAI,YAAY,QAAW;AAC1B,eAAO,IAAI,OAAO,SAAS;AAC1B,cAAI,UAAU;AACd,qBAAW,CAAC,GAAG,CAAC,KAAK,KAAK;AACzB,gBAAI,EAAE,WAAW,0BAA0B;AAC1C,kBAAI,OAAO,CAAC;AACZ,wBAAU;AACV;AAAA,YACD;AAAA,UACD;AACA,cAAI,CAAC,QAAS;AAAA,QACf;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAI,QAAQ;AACX,eAAO,SAAS;AAChB,eAAO,WAAW;AAAA,MACnB;AAAA,IACD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,UAAI,OAAO,GAAG;AAAA,IACf;AAAA,IAEA,MAAM,QAAQ;AACb,UAAI,QAAQ;AACZ,iBAAW,CAAC,KAAK,MAAM,KAAK,KAAK;AAChC,YAAI,UAAU,MAAM,GAAG;AACtB,cAAI,OAAO,GAAG;AACd;AAAA,QACD;AAAA,MACD;AACA,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -23,6 +23,11 @@ __export(redis_exports, {
23
23
  redisStore: () => redisStore
24
24
  });
25
25
  module.exports = __toCommonJS(redis_exports);
26
+
27
+ // src/types.ts
28
+ var RECORD_STATUS_COMPLETED = "completed";
29
+
30
+ // src/stores/redis.ts
26
31
  var DEFAULT_TTL = 86400;
27
32
  function redisStore(options) {
28
33
  const { client, ttl = DEFAULT_TTL } = options;
@@ -30,19 +35,42 @@ function redisStore(options) {
30
35
  async get(key) {
31
36
  const raw = await client.get(key);
32
37
  if (!raw) return void 0;
33
- return JSON.parse(raw);
38
+ try {
39
+ return JSON.parse(raw);
40
+ } catch {
41
+ return void 0;
42
+ }
34
43
  },
35
44
  async lock(key, record) {
36
- const result = await client.set(key, JSON.stringify(record), { NX: true, EX: ttl });
45
+ let serialized;
46
+ try {
47
+ serialized = JSON.stringify(record);
48
+ } catch {
49
+ return false;
50
+ }
51
+ const result = await client.set(key, serialized, { NX: true, EX: ttl });
37
52
  return result === "OK";
38
53
  },
39
54
  async complete(key, response) {
40
55
  const raw = await client.get(key);
41
56
  if (!raw) return;
42
- const record = JSON.parse(raw);
43
- record.status = "completed";
57
+ let record;
58
+ try {
59
+ record = JSON.parse(raw);
60
+ } catch {
61
+ return;
62
+ }
63
+ record.status = RECORD_STATUS_COMPLETED;
44
64
  record.response = response;
45
- await client.set(key, JSON.stringify(record), { EX: ttl });
65
+ const elapsed = Math.floor((Date.now() - record.createdAt) / 1e3);
66
+ const remaining = Math.max(1, ttl - elapsed);
67
+ let serialized;
68
+ try {
69
+ serialized = JSON.stringify(record);
70
+ } catch {
71
+ return;
72
+ }
73
+ await client.set(key, serialized, { EX: remaining });
46
74
  },
47
75
  async delete(key) {
48
76
  await client.del(key);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/redis.ts"],"sourcesContent":["import type { IdempotencyRecord, StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */\nexport interface RedisClientLike {\n\tget(key: string): Promise<string | null>;\n\tset(key: string, value: string, options?: { NX?: boolean; EX?: number }): Promise<string | null>;\n\tdel(...keys: string[]): Promise<number>;\n}\n\nexport interface RedisStoreOptions {\n\t/** Redis client instance (ioredis, node-redis, or @upstash/redis). */\n\tclient: RedisClientLike;\n\t/** TTL in seconds (default: 86400 = 24h). Passed as Redis EX option. */\n\tttl?: number;\n}\n\nexport function redisStore(options: RedisStoreOptions): IdempotencyStore {\n\tconst { client, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return undefined;\n\t\t\treturn JSON.parse(raw) as IdempotencyRecord;\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tconst result = await client.set(key, JSON.stringify(record), { NX: true, EX: ttl });\n\t\t\treturn result === \"OK\";\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return;\n\t\t\tconst record = JSON.parse(raw) as IdempotencyRecord;\n\t\t\trecord.status = \"completed\";\n\t\t\trecord.response = response;\n\t\t\tawait client.set(key, JSON.stringify(record), { EX: ttl });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait client.del(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// Redis handles expiration automatically via EX — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc;AAgBb,SAAS,WAAW,SAA8C;AACxE,QAAM,EAAE,QAAQ,MAAM,YAAY,IAAI;AAEtC,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK,QAAO;AACjB,aAAO,KAAK,MAAM,GAAG;AAAA,IACtB;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,YAAM,SAAS,MAAM,OAAO,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,IAAI,MAAM,IAAI,IAAI,CAAC;AAClF,aAAO,WAAW;AAAA,IACnB;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK;AACV,YAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,OAAO,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,IAAI,IAAI,CAAC;AAAA,IAC1D;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,OAAO,IAAI,GAAG;AAAA,IACrB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/stores/redis.ts","../../src/types.ts"],"sourcesContent":["import { type IdempotencyRecord, RECORD_STATUS_COMPLETED, type StoredResponse } from \"../types.js\";\nimport type { IdempotencyStore } from \"./types.js\";\n\nconst DEFAULT_TTL = 86400; // 24 hours in seconds\n\n/** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */\nexport interface RedisClientLike {\n\tget(key: string): Promise<string | null>;\n\tset(key: string, value: string, options?: { NX?: boolean; EX?: number }): Promise<string | null>;\n\tdel(...keys: string[]): Promise<number>;\n}\n\nexport interface RedisStoreOptions {\n\t/** Redis client instance (ioredis, node-redis, or @upstash/redis). */\n\tclient: RedisClientLike;\n\t/** TTL in seconds (default: 86400 = 24h). Passed as Redis EX option. */\n\tttl?: number;\n}\n\nexport function redisStore(options: RedisStoreOptions): IdempotencyStore {\n\tconst { client, ttl = DEFAULT_TTL } = options;\n\n\treturn {\n\t\tasync get(key) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return undefined;\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t},\n\n\t\tasync lock(key, record) {\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst result = await client.set(key, serialized, { NX: true, EX: ttl });\n\t\t\treturn result === \"OK\";\n\t\t},\n\n\t\tasync complete(key, response) {\n\t\t\tconst raw = await client.get(key);\n\t\t\tif (!raw) return;\n\t\t\tlet record: IdempotencyRecord;\n\t\t\ttry {\n\t\t\t\trecord = JSON.parse(raw) as IdempotencyRecord;\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\trecord.status = RECORD_STATUS_COMPLETED;\n\t\t\trecord.response = response;\n\t\t\tconst elapsed = Math.floor((Date.now() - record.createdAt) / 1000);\n\t\t\tconst remaining = Math.max(1, ttl - elapsed);\n\t\t\tlet serialized: string;\n\t\t\ttry {\n\t\t\t\tserialized = JSON.stringify(record);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tawait client.set(key, serialized, { EX: remaining });\n\t\t},\n\n\t\tasync delete(key) {\n\t\t\tawait client.del(key);\n\t\t},\n\n\t\tasync purge() {\n\t\t\t// Redis handles expiration automatically via EX — no manual purge needed\n\t\t\treturn 0;\n\t\t},\n\t};\n}\n","import type { Context, Env } from \"hono\";\nimport type { ProblemDetail } from \"./errors.js\";\nimport type { IdempotencyStore } from \"./stores/types.js\";\n\nexport const RECORD_STATUS_PROCESSING = \"processing\" as const;\nexport const RECORD_STATUS_COMPLETED = \"completed\" as const;\n\nexport interface IdempotencyEnv extends Env {\n\tVariables: {\n\t\tidempotencyKey: string | undefined;\n\t};\n}\n\nexport interface StoredResponse {\n\tstatus: number;\n\theaders: Record<string, string>;\n\tbody: string;\n}\n\nexport interface IdempotencyRecord {\n\tkey: string;\n\tfingerprint: string;\n\tstatus: \"processing\" | \"completed\";\n\tresponse?: StoredResponse;\n\tcreatedAt: number;\n}\n\nexport interface IdempotencyOptions {\n\tstore: IdempotencyStore;\n\theaderName?: string;\n\tfingerprint?: (c: Context) => string | Promise<string>;\n\trequired?: boolean;\n\tmethods?: string[];\n\tmaxKeyLength?: number;\n\t/** Maximum request body size in bytes. Pre-checked via Content-Length header, then enforced against actual body byte length. */\n\tmaxBodySize?: number;\n\t/** Should be a lightweight, side-effect-free predicate. Avoid reading the request body. */\n\tskipRequest?: (c: Context) => boolean | Promise<boolean>;\n\t/** Return a Response with an error status (4xx/5xx). Returning 2xx bypasses idempotency guarantees. */\n\tonError?: (error: ProblemDetail, c: Context) => Response | Promise<Response>;\n\tcacheKeyPrefix?: string | ((c: Context) => string | Promise<string>);\n\t/**\n\t * Called when a cached response is about to be replayed.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t * `key` is the raw header value; sanitize before logging to prevent log injection.\n\t */\n\tonCacheHit?: (key: string, c: Context) => void | Promise<void>;\n\t/**\n\t * Called when a new request acquires the lock (before the handler runs).\n\t * Fires on each lock acquisition, including retries after prior failures.\n\t * Errors are swallowed — hooks must not affect request processing.\n\t */\n\tonCacheMiss?: (key: string, c: Context) => void | Promise<void>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACKO,IAAM,0BAA0B;;;ADFvC,IAAM,cAAc;AAgBb,SAAS,WAAW,SAA8C;AACxE,QAAM,EAAE,QAAQ,MAAM,YAAY,IAAI;AAEtC,SAAO;AAAA,IACN,MAAM,IAAI,KAAK;AACd,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK,QAAO;AACjB,UAAI;AACH,eAAO,KAAK,MAAM,GAAG;AAAA,MACtB,QAAQ;AACP,eAAO;AAAA,MACR;AAAA,IACD;AAAA,IAEA,MAAM,KAAK,KAAK,QAAQ;AACvB,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP,eAAO;AAAA,MACR;AACA,YAAM,SAAS,MAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,MAAM,IAAI,IAAI,CAAC;AACtE,aAAO,WAAW;AAAA,IACnB;AAAA,IAEA,MAAM,SAAS,KAAK,UAAU;AAC7B,YAAM,MAAM,MAAM,OAAO,IAAI,GAAG;AAChC,UAAI,CAAC,IAAK;AACV,UAAI;AACJ,UAAI;AACH,iBAAS,KAAK,MAAM,GAAG;AAAA,MACxB,QAAQ;AACP;AAAA,MACD;AACA,aAAO,SAAS;AAChB,aAAO,WAAW;AAClB,YAAM,UAAU,KAAK,OAAO,KAAK,IAAI,IAAI,OAAO,aAAa,GAAI;AACjE,YAAM,YAAY,KAAK,IAAI,GAAG,MAAM,OAAO;AAC3C,UAAI;AACJ,UAAI;AACH,qBAAa,KAAK,UAAU,MAAM;AAAA,MACnC,QAAQ;AACP;AAAA,MACD;AACA,YAAM,OAAO,IAAI,KAAK,YAAY,EAAE,IAAI,UAAU,CAAC;AAAA,IACpD;AAAA,IAEA,MAAM,OAAO,KAAK;AACjB,YAAM,OAAO,IAAI,GAAG;AAAA,IACrB;AAAA,IAEA,MAAM,QAAQ;AAEb,aAAO;AAAA,IACR;AAAA,EACD;AACD;","names":[]}
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.cjs';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.cjs';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */
@@ -1,4 +1,4 @@
1
- import { e as IdempotencyStore } from '../types-C_OW_leh.js';
1
+ import { e as IdempotencyStore } from '../types-7IwFeI0l.js';
2
2
  import 'hono';
3
3
 
4
4
  /** Minimal Redis client subset compatible with ioredis, node-redis, and @upstash/redis. */