hono-idempotency 0.1.0 → 0.2.0

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 CHANGED
@@ -67,6 +67,56 @@ idempotency({
67
67
 
68
68
  // Custom fingerprint function (default: SHA-256 of method + path + body)
69
69
  fingerprint: (c) => `${c.req.method}:${c.req.path}`,
70
+
71
+ // Skip idempotency for specific requests
72
+ skipRequest: (c) => c.req.path === "/api/health",
73
+
74
+ // Namespace store keys for multi-tenant isolation
75
+ cacheKeyPrefix: (c) => c.req.header("X-Tenant-Id") ?? "default",
76
+
77
+ // Custom error response handler (default: RFC 9457 Problem Details)
78
+ onError: (error, c) => c.json({ error: error.title }, error.status),
79
+ });
80
+ ```
81
+
82
+ ### skipRequest
83
+
84
+ Skip idempotency processing for specific requests. Useful for health checks or internal endpoints.
85
+
86
+ ```ts
87
+ idempotency({
88
+ store: memoryStore(),
89
+ skipRequest: (c) => c.req.path === "/api/health",
90
+ });
91
+ ```
92
+
93
+ ### cacheKeyPrefix
94
+
95
+ Namespace store keys to isolate idempotency state between tenants or environments.
96
+
97
+ ```ts
98
+ idempotency({
99
+ store: memoryStore(),
100
+ // Static prefix
101
+ cacheKeyPrefix: "production",
102
+
103
+ // Or dynamic per-request prefix
104
+ cacheKeyPrefix: (c) => c.req.header("X-Tenant-Id") ?? "default",
105
+ });
106
+ ```
107
+
108
+ ### onError
109
+
110
+ Override the default RFC 9457 error responses with a custom handler.
111
+
112
+ ```ts
113
+ import type { ProblemDetail } from "hono-idempotency";
114
+
115
+ idempotency({
116
+ store: memoryStore(),
117
+ onError: (error: ProblemDetail, c) => {
118
+ return c.json({ code: error.status, message: error.title }, error.status);
119
+ },
70
120
  });
71
121
  ```
72
122
 
@@ -84,6 +134,50 @@ const store = memoryStore({
84
134
  });
85
135
  ```
86
136
 
137
+ ### Cloudflare KV Store
138
+
139
+ For Cloudflare Workers with KV. TTL is handled automatically by KV expiration.
140
+
141
+ ```ts
142
+ import { kvStore } from "hono-idempotency/stores/cloudflare-kv";
143
+
144
+ type Bindings = { IDEMPOTENCY_KV: KVNamespace };
145
+
146
+ const app = new Hono<{ Bindings: Bindings }>();
147
+
148
+ app.use("/api/*", async (c, next) => {
149
+ const store = kvStore({
150
+ namespace: c.env.IDEMPOTENCY_KV,
151
+ ttl: 86400, // 24 hours in seconds (default)
152
+ });
153
+ return idempotency({ store })(c, next);
154
+ });
155
+ ```
156
+
157
+ > **Note:** KV is eventually consistent. In rare cases, concurrent requests to different edge locations may both acquire the lock. This is acceptable for most idempotency use cases.
158
+
159
+ ### Cloudflare D1 Store
160
+
161
+ For Cloudflare Workers with D1. Uses SQL for strong consistency. Table is created automatically.
162
+
163
+ ```ts
164
+ import { d1Store } from "hono-idempotency/stores/cloudflare-d1";
165
+
166
+ type Bindings = { IDEMPOTENCY_DB: D1Database };
167
+
168
+ const app = new Hono<{ Bindings: Bindings }>();
169
+
170
+ app.use("/api/*", async (c, next) => {
171
+ const store = d1Store({
172
+ database: c.env.IDEMPOTENCY_DB,
173
+ tableName: "idempotency_keys", // default
174
+ });
175
+ return idempotency({ store })(c, next);
176
+ });
177
+ ```
178
+
179
+ > **Note:** D1 provides strong consistency, making `lock()` reliable for concurrent request protection. Consider adding a scheduled cleanup for expired rows.
180
+
87
181
  ### Custom Store
88
182
 
89
183
  Implement the `IdempotencyStore` interface:
@@ -23,8 +23,53 @@ __export(cloudflare_d1_exports, {
23
23
  d1Store: () => d1Store
24
24
  });
25
25
  module.exports = __toCommonJS(cloudflare_d1_exports);
26
- function d1Store(_options) {
27
- throw new Error("cloudflare-d1 store is not yet implemented. Coming in Phase 2.");
26
+ var DEFAULT_TABLE = "idempotency_keys";
27
+ function d1Store(options) {
28
+ const { database: db, tableName = DEFAULT_TABLE } = options;
29
+ let initialized = false;
30
+ const ensureTable = async () => {
31
+ if (initialized) return;
32
+ await db.prepare(
33
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
34
+ key TEXT PRIMARY KEY,
35
+ fingerprint TEXT NOT NULL,
36
+ status TEXT NOT NULL,
37
+ response TEXT,
38
+ created_at INTEGER NOT NULL
39
+ )`
40
+ ).run();
41
+ initialized = true;
42
+ };
43
+ const toRecord = (row) => ({
44
+ key: row.key,
45
+ fingerprint: row.fingerprint,
46
+ status: row.status,
47
+ response: row.response ? JSON.parse(row.response) : void 0,
48
+ createdAt: row.created_at
49
+ });
50
+ return {
51
+ async get(key) {
52
+ await ensureTable();
53
+ const row = await db.prepare(`SELECT * FROM ${tableName} WHERE key = ?`).bind(key).first();
54
+ if (!row) return void 0;
55
+ return toRecord(row);
56
+ },
57
+ async lock(key, record) {
58
+ await ensureTable();
59
+ const result = await db.prepare(
60
+ `INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at) VALUES (?, ?, ?, ?, ?)`
61
+ ).bind(key, record.fingerprint, record.status, null, record.createdAt).run();
62
+ return result.meta.changes > 0;
63
+ },
64
+ async complete(key, response) {
65
+ await ensureTable();
66
+ await db.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`).bind("completed", JSON.stringify(response), key).run();
67
+ },
68
+ async delete(key) {
69
+ await ensureTable();
70
+ await db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();
71
+ }
72
+ };
28
73
  }
29
74
  // Annotate the CommonJS export names for ESM import in node:
30
75
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface D1StoreOptions {\n\tbinding: string;\n\ttableName?: string;\n}\n\n// Phase 2: Cloudflare D1 store implementation\nexport function d1Store(_options: D1StoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-d1 store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
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\";\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\"). */\n\ttableName?: string;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE } = options;\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 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.prepare(`SELECT * FROM ${tableName} WHERE key = ?`).bind(key).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) VALUES (?, ?, ?, ?, ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt)\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\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,gBAAgB;AAoBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,cAAc,IAAI;AACpD,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,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,GAAG,QAAQ,iBAAiB,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,MAAM;AACzF,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,MACnC,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,SAAS,EACnE,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,EACD;AACD;","names":[]}
@@ -1,10 +1,26 @@
1
1
  import { c as IdempotencyStore } from '../types-YeEt4qLu.cjs';
2
2
  import 'hono';
3
3
 
4
+ /** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */
5
+ interface D1DatabaseLike {
6
+ prepare(sql: string): D1PreparedStatementLike;
7
+ }
8
+ interface D1PreparedStatementLike {
9
+ bind(...params: unknown[]): D1PreparedStatementLike;
10
+ run(): Promise<{
11
+ success: boolean;
12
+ meta: {
13
+ changes: number;
14
+ };
15
+ }>;
16
+ first(): Promise<Record<string, unknown> | null>;
17
+ }
4
18
  interface D1StoreOptions {
5
- binding: string;
19
+ /** Cloudflare D1 database binding. */
20
+ database: D1DatabaseLike;
21
+ /** Table name (default: "idempotency_keys"). */
6
22
  tableName?: string;
7
23
  }
8
- declare function d1Store(_options: D1StoreOptions): IdempotencyStore;
24
+ declare function d1Store(options: D1StoreOptions): IdempotencyStore;
9
25
 
10
- export { d1Store };
26
+ export { type D1DatabaseLike, type D1PreparedStatementLike, type D1StoreOptions, d1Store };
@@ -1,10 +1,26 @@
1
1
  import { c as IdempotencyStore } from '../types-YeEt4qLu.js';
2
2
  import 'hono';
3
3
 
4
+ /** Minimal D1Database subset used by d1Store (avoids @cloudflare/workers-types dependency). */
5
+ interface D1DatabaseLike {
6
+ prepare(sql: string): D1PreparedStatementLike;
7
+ }
8
+ interface D1PreparedStatementLike {
9
+ bind(...params: unknown[]): D1PreparedStatementLike;
10
+ run(): Promise<{
11
+ success: boolean;
12
+ meta: {
13
+ changes: number;
14
+ };
15
+ }>;
16
+ first(): Promise<Record<string, unknown> | null>;
17
+ }
4
18
  interface D1StoreOptions {
5
- binding: string;
19
+ /** Cloudflare D1 database binding. */
20
+ database: D1DatabaseLike;
21
+ /** Table name (default: "idempotency_keys"). */
6
22
  tableName?: string;
7
23
  }
8
- declare function d1Store(_options: D1StoreOptions): IdempotencyStore;
24
+ declare function d1Store(options: D1StoreOptions): IdempotencyStore;
9
25
 
10
- export { d1Store };
26
+ export { type D1DatabaseLike, type D1PreparedStatementLike, type D1StoreOptions, d1Store };
@@ -1,6 +1,51 @@
1
1
  // src/stores/cloudflare-d1.ts
2
- function d1Store(_options) {
3
- throw new Error("cloudflare-d1 store is not yet implemented. Coming in Phase 2.");
2
+ var DEFAULT_TABLE = "idempotency_keys";
3
+ function d1Store(options) {
4
+ const { database: db, tableName = DEFAULT_TABLE } = options;
5
+ let initialized = false;
6
+ const ensureTable = async () => {
7
+ if (initialized) return;
8
+ await db.prepare(
9
+ `CREATE TABLE IF NOT EXISTS ${tableName} (
10
+ key TEXT PRIMARY KEY,
11
+ fingerprint TEXT NOT NULL,
12
+ status TEXT NOT NULL,
13
+ response TEXT,
14
+ created_at INTEGER NOT NULL
15
+ )`
16
+ ).run();
17
+ initialized = true;
18
+ };
19
+ const toRecord = (row) => ({
20
+ key: row.key,
21
+ fingerprint: row.fingerprint,
22
+ status: row.status,
23
+ response: row.response ? JSON.parse(row.response) : void 0,
24
+ createdAt: row.created_at
25
+ });
26
+ return {
27
+ async get(key) {
28
+ await ensureTable();
29
+ const row = await db.prepare(`SELECT * FROM ${tableName} WHERE key = ?`).bind(key).first();
30
+ if (!row) return void 0;
31
+ return toRecord(row);
32
+ },
33
+ async lock(key, record) {
34
+ await ensureTable();
35
+ const result = await db.prepare(
36
+ `INSERT OR IGNORE INTO ${tableName} (key, fingerprint, status, response, created_at) VALUES (?, ?, ?, ?, ?)`
37
+ ).bind(key, record.fingerprint, record.status, null, record.createdAt).run();
38
+ return result.meta.changes > 0;
39
+ },
40
+ async complete(key, response) {
41
+ await ensureTable();
42
+ await db.prepare(`UPDATE ${tableName} SET status = ?, response = ? WHERE key = ?`).bind("completed", JSON.stringify(response), key).run();
43
+ },
44
+ async delete(key) {
45
+ await ensureTable();
46
+ await db.prepare(`DELETE FROM ${tableName} WHERE key = ?`).bind(key).run();
47
+ }
48
+ };
4
49
  }
5
50
  export {
6
51
  d1Store
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-d1.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface D1StoreOptions {\n\tbinding: string;\n\ttableName?: string;\n}\n\n// Phase 2: Cloudflare D1 store implementation\nexport function d1Store(_options: D1StoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-d1 store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
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\";\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\"). */\n\ttableName?: string;\n}\n\nexport function d1Store(options: D1StoreOptions): IdempotencyStore {\n\tconst { database: db, tableName = DEFAULT_TABLE } = options;\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 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.prepare(`SELECT * FROM ${tableName} WHERE key = ?`).bind(key).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) VALUES (?, ?, ?, ?, ?)`,\n\t\t\t\t)\n\t\t\t\t.bind(key, record.fingerprint, record.status, null, record.createdAt)\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\t};\n}\n"],"mappings":";AAGA,IAAM,gBAAgB;AAoBf,SAAS,QAAQ,SAA2C;AAClE,QAAM,EAAE,UAAU,IAAI,YAAY,cAAc,IAAI;AACpD,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,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,GAAG,QAAQ,iBAAiB,SAAS,gBAAgB,EAAE,KAAK,GAAG,EAAE,MAAM;AACzF,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,MACnC,EACC,KAAK,KAAK,OAAO,aAAa,OAAO,QAAQ,MAAM,OAAO,SAAS,EACnE,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,EACD;AACD;","names":[]}
@@ -23,8 +23,33 @@ __export(cloudflare_kv_exports, {
23
23
  kvStore: () => kvStore
24
24
  });
25
25
  module.exports = __toCommonJS(cloudflare_kv_exports);
26
- function kvStore(_options) {
27
- throw new Error("cloudflare-kv store is not yet implemented. Coming in Phase 2.");
26
+ var DEFAULT_TTL = 86400;
27
+ function kvStore(options) {
28
+ const { namespace: kv, ttl = DEFAULT_TTL } = options;
29
+ return {
30
+ async get(key) {
31
+ const record = await kv.get(key, { type: "json" });
32
+ return record ?? void 0;
33
+ },
34
+ async lock(key, record) {
35
+ const existing = await kv.get(key, { type: "json" });
36
+ if (existing) {
37
+ return false;
38
+ }
39
+ await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
40
+ return true;
41
+ },
42
+ async complete(key, response) {
43
+ const record = await kv.get(key, { type: "json" });
44
+ if (!record) return;
45
+ record.status = "completed";
46
+ record.response = response;
47
+ await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
48
+ },
49
+ async delete(key) {
50
+ await kv.delete(key);
51
+ }
52
+ };
28
53
  }
29
54
  // Annotate the CommonJS export names for ESM import in node:
30
55
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface KVStoreOptions {\n\tbinding: string;\n\tttl?: number;\n}\n\n// Phase 2: Cloudflare KV store implementation\nexport function kvStore(_options: KVStoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-kv store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
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\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) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\t\t\treturn true;\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\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc;AAgBb,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,UAAU;AACb,eAAO;AAAA,MACR;AACA,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAChE,aAAO;AAAA,IACR;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,EACD;AACD;","names":[]}
@@ -1,10 +1,22 @@
1
1
  import { c as IdempotencyStore } from '../types-YeEt4qLu.cjs';
2
2
  import 'hono';
3
3
 
4
+ /** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
5
+ interface KVNamespaceLike {
6
+ get(key: string, options: {
7
+ type: "json";
8
+ }): Promise<unknown>;
9
+ put(key: string, value: string, options?: {
10
+ expirationTtl?: number;
11
+ }): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ }
4
14
  interface KVStoreOptions {
5
- binding: string;
15
+ /** Cloudflare Workers KV namespace binding. */
16
+ namespace: KVNamespaceLike;
17
+ /** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */
6
18
  ttl?: number;
7
19
  }
8
- declare function kvStore(_options: KVStoreOptions): IdempotencyStore;
20
+ declare function kvStore(options: KVStoreOptions): IdempotencyStore;
9
21
 
10
- export { kvStore };
22
+ export { type KVNamespaceLike, type KVStoreOptions, kvStore };
@@ -1,10 +1,22 @@
1
1
  import { c as IdempotencyStore } from '../types-YeEt4qLu.js';
2
2
  import 'hono';
3
3
 
4
+ /** Minimal KVNamespace subset used by kvStore (avoids @cloudflare/workers-types dependency). */
5
+ interface KVNamespaceLike {
6
+ get(key: string, options: {
7
+ type: "json";
8
+ }): Promise<unknown>;
9
+ put(key: string, value: string, options?: {
10
+ expirationTtl?: number;
11
+ }): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ }
4
14
  interface KVStoreOptions {
5
- binding: string;
15
+ /** Cloudflare Workers KV namespace binding. */
16
+ namespace: KVNamespaceLike;
17
+ /** TTL in seconds (default: 86400 = 24h). KV minimum is 60 seconds. */
6
18
  ttl?: number;
7
19
  }
8
- declare function kvStore(_options: KVStoreOptions): IdempotencyStore;
20
+ declare function kvStore(options: KVStoreOptions): IdempotencyStore;
9
21
 
10
- export { kvStore };
22
+ export { type KVNamespaceLike, type KVStoreOptions, kvStore };
@@ -1,6 +1,31 @@
1
1
  // src/stores/cloudflare-kv.ts
2
- function kvStore(_options) {
3
- throw new Error("cloudflare-kv store is not yet implemented. Coming in Phase 2.");
2
+ var DEFAULT_TTL = 86400;
3
+ function kvStore(options) {
4
+ const { namespace: kv, ttl = DEFAULT_TTL } = options;
5
+ return {
6
+ async get(key) {
7
+ const record = await kv.get(key, { type: "json" });
8
+ return record ?? void 0;
9
+ },
10
+ async lock(key, record) {
11
+ const existing = await kv.get(key, { type: "json" });
12
+ if (existing) {
13
+ return false;
14
+ }
15
+ await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
16
+ return true;
17
+ },
18
+ async complete(key, response) {
19
+ const record = await kv.get(key, { type: "json" });
20
+ if (!record) return;
21
+ record.status = "completed";
22
+ record.response = response;
23
+ await kv.put(key, JSON.stringify(record), { expirationTtl: ttl });
24
+ },
25
+ async delete(key) {
26
+ await kv.delete(key);
27
+ }
28
+ };
4
29
  }
5
30
  export {
6
31
  kvStore
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/stores/cloudflare-kv.ts"],"sourcesContent":["import type { IdempotencyStore } from \"./types.js\";\n\ninterface KVStoreOptions {\n\tbinding: string;\n\tttl?: number;\n}\n\n// Phase 2: Cloudflare KV store implementation\nexport function kvStore(_options: KVStoreOptions): IdempotencyStore {\n\tthrow new Error(\"cloudflare-kv store is not yet implemented. Coming in Phase 2.\");\n}\n"],"mappings":";AAQO,SAAS,QAAQ,UAA4C;AACnE,QAAM,IAAI,MAAM,gEAAgE;AACjF;","names":[]}
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\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) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tawait kv.put(key, JSON.stringify(record), { expirationTtl: ttl });\n\t\t\treturn true;\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\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc;AAgBb,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,UAAU;AACb,eAAO;AAAA,MACR;AACA,YAAM,GAAG,IAAI,KAAK,KAAK,UAAU,MAAM,GAAG,EAAE,eAAe,IAAI,CAAC;AAChE,aAAO;AAAA,IACR;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,EACD;AACD;","names":[]}
@@ -30,7 +30,17 @@ function memoryStore(options = {}) {
30
30
  const isExpired = (record) => {
31
31
  return Date.now() - record.createdAt >= ttl;
32
32
  };
33
+ const sweep = () => {
34
+ for (const [key, record] of map) {
35
+ if (isExpired(record)) {
36
+ map.delete(key);
37
+ }
38
+ }
39
+ };
33
40
  return {
41
+ get size() {
42
+ return map.size;
43
+ },
34
44
  async get(key) {
35
45
  const record = map.get(key);
36
46
  if (!record) return void 0;
@@ -41,6 +51,7 @@ function memoryStore(options = {}) {
41
51
  return record;
42
52
  },
43
53
  async lock(key, record) {
54
+ sweep();
44
55
  const existing = map.get(key);
45
56
  if (existing && !isExpired(existing)) {
46
57
  return false;
@@ -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\ninterface MemoryStoreOptions {\n\tttl?: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): IdempotencyStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\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\treturn {\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\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\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\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAM5B,SAAS,YAAY,UAA8B,CAAC,GAAqB;AAC/E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,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,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,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,EACD;AACD;","names":[]}
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\ninterface MemoryStoreOptions {\n\tttl?: 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 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\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\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAW5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,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,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,EACD;AACD;","names":[]}
@@ -4,6 +4,10 @@ import 'hono';
4
4
  interface MemoryStoreOptions {
5
5
  ttl?: number;
6
6
  }
7
- declare function memoryStore(options?: MemoryStoreOptions): IdempotencyStore;
7
+ interface MemoryStore extends IdempotencyStore {
8
+ /** Number of entries currently in the store (including expired but not yet swept). */
9
+ readonly size: number;
10
+ }
11
+ declare function memoryStore(options?: MemoryStoreOptions): MemoryStore;
8
12
 
9
- export { memoryStore };
13
+ export { type MemoryStore, memoryStore };
@@ -4,6 +4,10 @@ import 'hono';
4
4
  interface MemoryStoreOptions {
5
5
  ttl?: number;
6
6
  }
7
- declare function memoryStore(options?: MemoryStoreOptions): IdempotencyStore;
7
+ interface MemoryStore extends IdempotencyStore {
8
+ /** Number of entries currently in the store (including expired but not yet swept). */
9
+ readonly size: number;
10
+ }
11
+ declare function memoryStore(options?: MemoryStoreOptions): MemoryStore;
8
12
 
9
- export { memoryStore };
13
+ export { type MemoryStore, memoryStore };
@@ -6,7 +6,17 @@ function memoryStore(options = {}) {
6
6
  const isExpired = (record) => {
7
7
  return Date.now() - record.createdAt >= ttl;
8
8
  };
9
+ const sweep = () => {
10
+ for (const [key, record] of map) {
11
+ if (isExpired(record)) {
12
+ map.delete(key);
13
+ }
14
+ }
15
+ };
9
16
  return {
17
+ get size() {
18
+ return map.size;
19
+ },
10
20
  async get(key) {
11
21
  const record = map.get(key);
12
22
  if (!record) return void 0;
@@ -17,6 +27,7 @@ function memoryStore(options = {}) {
17
27
  return record;
18
28
  },
19
29
  async lock(key, record) {
30
+ sweep();
20
31
  const existing = map.get(key);
21
32
  if (existing && !isExpired(existing)) {
22
33
  return false;
@@ -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\ninterface MemoryStoreOptions {\n\tttl?: number;\n}\n\nexport function memoryStore(options: MemoryStoreOptions = {}): IdempotencyStore {\n\tconst ttl = options.ttl ?? DEFAULT_TTL;\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\treturn {\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\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\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\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAM5B,SAAS,YAAY,UAA8B,CAAC,GAAqB;AAC/E,QAAM,MAAM,QAAQ,OAAO;AAC3B,QAAM,MAAM,oBAAI,IAA+B;AAE/C,QAAM,YAAY,CAAC,WAAuC;AACzD,WAAO,KAAK,IAAI,IAAI,OAAO,aAAa;AAAA,EACzC;AAEA,SAAO;AAAA,IACN,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,WAAW,IAAI,IAAI,GAAG;AAC5B,UAAI,YAAY,CAAC,UAAU,QAAQ,GAAG;AACrC,eAAO;AAAA,MACR;AACA,UAAI,IAAI,KAAK,MAAM;AACnB,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,EACD;AACD;","names":[]}
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\ninterface MemoryStoreOptions {\n\tttl?: 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 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\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\t};\n}\n"],"mappings":";AAGA,IAAM,cAAc,KAAK,KAAK,KAAK;AAW5B,SAAS,YAAY,UAA8B,CAAC,GAAgB;AAC1E,QAAM,MAAM,QAAQ,OAAO;AAC3B,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,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,EACD;AACD;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hono-idempotency",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Stripe-style Idempotency-Key middleware for Hono. IETF draft-ietf-httpapi-idempotency-key-header compliant.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -98,6 +98,7 @@
98
98
  "lint:fix": "biome check --write .",
99
99
  "format": "biome format --write .",
100
100
  "typecheck": "tsc --noEmit",
101
- "release": "pnpm build && changeset publish"
101
+ "release": "pnpm build && changeset publish",
102
+ "version-packages": "changeset version && pnpm lint:fix"
102
103
  }
103
104
  }