hono-idempotency 0.2.1 → 0.3.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 +8 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/stores/cloudflare-d1.d.cts +1 -1
- package/dist/stores/cloudflare-d1.d.ts +1 -1
- package/dist/stores/cloudflare-kv.d.cts +1 -1
- package/dist/stores/cloudflare-kv.d.ts +1 -1
- package/dist/stores/memory.cjs +7 -0
- package/dist/stores/memory.cjs.map +1 -1
- package/dist/stores/memory.d.cts +3 -1
- package/dist/stores/memory.d.ts +3 -1
- package/dist/stores/memory.js +7 -0
- package/dist/stores/memory.js.map +1 -1
- package/dist/{types-YeEt4qLu.d.cts → types-wjiOEpHT.d.cts} +1 -1
- package/dist/{types-YeEt4qLu.d.ts → types-wjiOEpHT.d.ts} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,11 @@ Stripe-style Idempotency-Key middleware for [Hono](https://hono.dev). IETF [draf
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
+
# npm
|
|
19
20
|
npm install hono-idempotency
|
|
21
|
+
|
|
22
|
+
# pnpm
|
|
23
|
+
pnpm add hono-idempotency
|
|
20
24
|
```
|
|
21
25
|
|
|
22
26
|
## Quick Start
|
|
@@ -145,6 +149,7 @@ type Bindings = { IDEMPOTENCY_KV: KVNamespace };
|
|
|
145
149
|
|
|
146
150
|
const app = new Hono<{ Bindings: Bindings }>();
|
|
147
151
|
|
|
152
|
+
// Store must be created per-request since KV binding comes from c.env
|
|
148
153
|
app.use("/api/*", async (c, next) => {
|
|
149
154
|
const store = kvStore({
|
|
150
155
|
namespace: c.env.IDEMPOTENCY_KV,
|
|
@@ -167,10 +172,13 @@ type Bindings = { IDEMPOTENCY_DB: D1Database };
|
|
|
167
172
|
|
|
168
173
|
const app = new Hono<{ Bindings: Bindings }>();
|
|
169
174
|
|
|
175
|
+
// Store must be created per-request since D1 binding comes from c.env.
|
|
176
|
+
// CREATE TABLE IF NOT EXISTS runs each request but is a no-op after the first.
|
|
170
177
|
app.use("/api/*", async (c, next) => {
|
|
171
178
|
const store = d1Store({
|
|
172
179
|
database: c.env.IDEMPOTENCY_DB,
|
|
173
180
|
tableName: "idempotency_keys", // default
|
|
181
|
+
ttl: 86400, // 24 hours in seconds (default)
|
|
174
182
|
});
|
|
175
183
|
return idempotency({ store })(c, next);
|
|
176
184
|
});
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport type {\n\tIdempotencyEnv,\n\tIdempotencyOptions,\n\tIdempotencyRecord,\n\tStoredResponse,\n} from \"./types.js\";\nexport type { IdempotencyStore } from \"./stores/types.js\";\nexport type { ProblemDetail } from \"./errors.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { IdempotencyErrors, type ProblemDetail, problemResponse } from \"./errors.js\";\nimport { generateFingerprint } from \"./fingerprint.js\";\nimport type { IdempotencyEnv, IdempotencyOptions, StoredResponse } from \"./types.js\";\n\nconst DEFAULT_METHODS = [\"POST\", \"PATCH\"];\nconst DEFAULT_MAX_KEY_LENGTH = 256;\n// Headers unsafe to replay — session cookies could leak across users\nconst EXCLUDED_STORE_HEADERS = new Set([\"set-cookie\"]);\n\nexport function idempotency(options: IdempotencyOptions) {\n\tconst {\n\t\tstore,\n\t\theaderName = \"Idempotency-Key\",\n\t\tfingerprint: customFingerprint,\n\t\trequired = false,\n\t\tmethods = DEFAULT_METHODS,\n\t\tmaxKeyLength = DEFAULT_MAX_KEY_LENGTH,\n\t\tskipRequest,\n\t\tonError,\n\t\tcacheKeyPrefix,\n\t} = options;\n\n\treturn createMiddleware<IdempotencyEnv>(async (c, next) => {\n\t\tif (!methods.includes(c.req.method)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tif (skipRequest && (await skipRequest(c))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst errorResponse = (problem: ProblemDetail, extraHeaders?: Record<string, string>) =>\n\t\t\tonError ? onError(problem, c) : problemResponse(problem, extraHeaders);\n\n\t\tconst key = c.req.header(headerName);\n\n\t\tif (!key) {\n\t\t\tif (required) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.missingKey());\n\t\t\t}\n\t\t\treturn next();\n\t\t}\n\n\t\tif (key.length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\tconst body = await c.req.text();\n\t\tconst fp = customFingerprint\n\t\t\t? await customFingerprint(c)\n\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\tconst rawPrefix =\n\t\t\ttypeof cacheKeyPrefix === \"function\" ? await cacheKeyPrefix(c) : cacheKeyPrefix;\n\t\t// Encode user-controlled components to prevent delimiter injection\n\t\tconst encodedKey = encodeURIComponent(key);\n\t\tconst baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;\n\t\tconst storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;\n\n\t\tconst existing = await store.get(storeKey);\n\n\t\tif (existing) {\n\t\t\tif (existing.status === \"processing\") {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), { \"Retry-After\": \"1\" });\n\t\t\t}\n\n\t\t\tif (existing.fingerprint !== fp) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.fingerprintMismatch());\n\t\t\t}\n\n\t\t\tif (existing.response) {\n\t\t\t\treturn replayResponse(existing.response);\n\t\t\t}\n\t\t}\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: \"processing\" as const,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tconst locked = await store.lock(storeKey, record);\n\t\tif (!locked) {\n\t\t\treturn errorResponse(IdempotencyErrors.conflict(), { \"Retry-After\": \"1\" });\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (err) {\n\t\t\tawait store.delete(storeKey);\n\t\t\tthrow err;\n\t\t}\n\n\t\tconst res = c.res;\n\t\tif (!res.ok) {\n\t\t\t// Non-2xx: delete key (Stripe pattern) so client can retry\n\t\t\tawait store.delete(storeKey);\n\t\t\treturn;\n\t\t}\n\n\t\tconst resBody = await res.text();\n\t\tconst resHeaders: Record<string, string> = {};\n\t\tres.headers.forEach((v, k) => {\n\t\t\tif (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {\n\t\t\t\tresHeaders[k] = v;\n\t\t\t}\n\t\t});\n\n\t\tconst storedResponse: StoredResponse = {\n\t\t\tstatus: res.status,\n\t\t\theaders: resHeaders,\n\t\t\tbody: resBody,\n\t\t};\n\n\t\tawait store.complete(storeKey, storedResponse);\n\n\t\t// Rebuild response since we consumed body\n\t\tc.res = new Response(resBody, {\n\t\t\tstatus: res.status,\n\t\t\theaders: res.headers,\n\t\t});\n\t});\n}\n\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: stored.status,\n\t\theaders,\n\t});\n}\n","export interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\treturn new Response(JSON.stringify(problem), {\n\t\tstatus: problem.status,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/problem+json\",\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\nconst BASE_URL = \"https://hono-idempotency.dev/errors\";\n\nexport const IdempotencyErrors = {\n\tmissingKey(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/missing-key`,\n\t\t\ttitle: \"Idempotency-Key header is required\",\n\t\t\tstatus: 400,\n\t\t\tdetail: \"This endpoint requires an Idempotency-Key header\",\n\t\t};\n\t},\n\n\tkeyTooLong(maxLength: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/key-too-long`,\n\t\t\ttitle: \"Idempotency-Key is too long\",\n\t\t\tstatus: 400,\n\t\t\tdetail: `Idempotency-Key must be at most ${maxLength} characters`,\n\t\t};\n\t},\n\n\tfingerprintMismatch(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/fingerprint-mismatch`,\n\t\t\ttitle: \"Idempotency-Key is already used with a different request\",\n\t\t\tstatus: 422,\n\t\t\tdetail:\n\t\t\t\t\"A request with the same idempotency key but different parameters was already processed\",\n\t\t};\n\t},\n\n\tconflict(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/conflict`,\n\t\t\ttitle: \"A request is outstanding for this idempotency key\",\n\t\t\tstatus: 409,\n\t\t\tdetail: \"A request with the same idempotency key is currently being processed\",\n\t\t};\n\t},\n} as const;\n","export async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst encoded = new TextEncoder().encode(data);\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoded);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\treturn Array.from(hashArray)\n\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t.join(\"\");\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/middleware.ts","../src/errors.ts","../src/fingerprint.ts"],"sourcesContent":["export { idempotency } from \"./middleware.js\";\nexport type {\n\tIdempotencyEnv,\n\tIdempotencyOptions,\n\tIdempotencyRecord,\n\tStoredResponse,\n} from \"./types.js\";\nexport type { IdempotencyStore } from \"./stores/types.js\";\nexport type { ProblemDetail } from \"./errors.js\";\nexport type { MemoryStore } from \"./stores/memory.js\";\n","import { createMiddleware } from \"hono/factory\";\nimport { IdempotencyErrors, type ProblemDetail, problemResponse } from \"./errors.js\";\nimport { generateFingerprint } from \"./fingerprint.js\";\nimport type { IdempotencyEnv, IdempotencyOptions, StoredResponse } from \"./types.js\";\n\nconst DEFAULT_METHODS = [\"POST\", \"PATCH\"];\nconst DEFAULT_MAX_KEY_LENGTH = 256;\n// Headers unsafe to replay — session cookies could leak across users\nconst EXCLUDED_STORE_HEADERS = new Set([\"set-cookie\"]);\n\nexport function idempotency(options: IdempotencyOptions) {\n\tconst {\n\t\tstore,\n\t\theaderName = \"Idempotency-Key\",\n\t\tfingerprint: customFingerprint,\n\t\trequired = false,\n\t\tmethods = DEFAULT_METHODS,\n\t\tmaxKeyLength = DEFAULT_MAX_KEY_LENGTH,\n\t\tskipRequest,\n\t\tonError,\n\t\tcacheKeyPrefix,\n\t} = options;\n\n\treturn createMiddleware<IdempotencyEnv>(async (c, next) => {\n\t\tif (!methods.includes(c.req.method)) {\n\t\t\treturn next();\n\t\t}\n\n\t\tif (skipRequest && (await skipRequest(c))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst errorResponse = (problem: ProblemDetail, extraHeaders?: Record<string, string>) =>\n\t\t\tonError ? onError(problem, c) : problemResponse(problem, extraHeaders);\n\n\t\tconst key = c.req.header(headerName);\n\n\t\tif (!key) {\n\t\t\tif (required) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.missingKey());\n\t\t\t}\n\t\t\treturn next();\n\t\t}\n\n\t\tif (key.length > maxKeyLength) {\n\t\t\treturn errorResponse(IdempotencyErrors.keyTooLong(maxKeyLength));\n\t\t}\n\n\t\tconst body = await c.req.text();\n\t\tconst fp = customFingerprint\n\t\t\t? await customFingerprint(c)\n\t\t\t: await generateFingerprint(c.req.method, c.req.path, body);\n\n\t\tconst rawPrefix =\n\t\t\ttypeof cacheKeyPrefix === \"function\" ? await cacheKeyPrefix(c) : cacheKeyPrefix;\n\t\t// Encode user-controlled components to prevent delimiter injection\n\t\tconst encodedKey = encodeURIComponent(key);\n\t\tconst baseKey = `${c.req.method}:${c.req.path}:${encodedKey}`;\n\t\tconst storeKey = rawPrefix ? `${encodeURIComponent(rawPrefix)}:${baseKey}` : baseKey;\n\n\t\tconst existing = await store.get(storeKey);\n\n\t\tif (existing) {\n\t\t\tif (existing.status === \"processing\") {\n\t\t\t\treturn errorResponse(IdempotencyErrors.conflict(), { \"Retry-After\": \"1\" });\n\t\t\t}\n\n\t\t\tif (existing.fingerprint !== fp) {\n\t\t\t\treturn errorResponse(IdempotencyErrors.fingerprintMismatch());\n\t\t\t}\n\n\t\t\tif (existing.response) {\n\t\t\t\treturn replayResponse(existing.response);\n\t\t\t}\n\t\t}\n\n\t\tconst record = {\n\t\t\tkey,\n\t\t\tfingerprint: fp,\n\t\t\tstatus: \"processing\" as const,\n\t\t\tcreatedAt: Date.now(),\n\t\t};\n\n\t\tconst locked = await store.lock(storeKey, record);\n\t\tif (!locked) {\n\t\t\treturn errorResponse(IdempotencyErrors.conflict(), { \"Retry-After\": \"1\" });\n\t\t}\n\n\t\tc.set(\"idempotencyKey\", key);\n\n\t\ttry {\n\t\t\tawait next();\n\t\t} catch (err) {\n\t\t\tawait store.delete(storeKey);\n\t\t\tthrow err;\n\t\t}\n\n\t\tconst res = c.res;\n\t\tif (!res.ok) {\n\t\t\t// Non-2xx: delete key (Stripe pattern) so client can retry\n\t\t\tawait store.delete(storeKey);\n\t\t\treturn;\n\t\t}\n\n\t\tconst resBody = await res.text();\n\t\tconst resHeaders: Record<string, string> = {};\n\t\tres.headers.forEach((v, k) => {\n\t\t\tif (!EXCLUDED_STORE_HEADERS.has(k.toLowerCase())) {\n\t\t\t\tresHeaders[k] = v;\n\t\t\t}\n\t\t});\n\n\t\tconst storedResponse: StoredResponse = {\n\t\t\tstatus: res.status,\n\t\t\theaders: resHeaders,\n\t\t\tbody: resBody,\n\t\t};\n\n\t\tawait store.complete(storeKey, storedResponse);\n\n\t\t// Rebuild response since we consumed body\n\t\tc.res = new Response(resBody, {\n\t\t\tstatus: res.status,\n\t\t\theaders: res.headers,\n\t\t});\n\t});\n}\n\nfunction replayResponse(stored: StoredResponse) {\n\tconst headers = new Headers(stored.headers);\n\theaders.set(\"Idempotency-Replayed\", \"true\");\n\n\treturn new Response(stored.body, {\n\t\tstatus: stored.status,\n\t\theaders,\n\t});\n}\n","export interface ProblemDetail {\n\ttype: string;\n\ttitle: string;\n\tstatus: number;\n\tdetail: string;\n}\n\nexport function problemResponse(\n\tproblem: ProblemDetail,\n\textraHeaders?: Record<string, string>,\n): Response {\n\treturn new Response(JSON.stringify(problem), {\n\t\tstatus: problem.status,\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/problem+json\",\n\t\t\t...extraHeaders,\n\t\t},\n\t});\n}\n\nconst BASE_URL = \"https://hono-idempotency.dev/errors\";\n\nexport const IdempotencyErrors = {\n\tmissingKey(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/missing-key`,\n\t\t\ttitle: \"Idempotency-Key header is required\",\n\t\t\tstatus: 400,\n\t\t\tdetail: \"This endpoint requires an Idempotency-Key header\",\n\t\t};\n\t},\n\n\tkeyTooLong(maxLength: number): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/key-too-long`,\n\t\t\ttitle: \"Idempotency-Key is too long\",\n\t\t\tstatus: 400,\n\t\t\tdetail: `Idempotency-Key must be at most ${maxLength} characters`,\n\t\t};\n\t},\n\n\tfingerprintMismatch(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/fingerprint-mismatch`,\n\t\t\ttitle: \"Idempotency-Key is already used with a different request\",\n\t\t\tstatus: 422,\n\t\t\tdetail:\n\t\t\t\t\"A request with the same idempotency key but different parameters was already processed\",\n\t\t};\n\t},\n\n\tconflict(): ProblemDetail {\n\t\treturn {\n\t\t\ttype: `${BASE_URL}/conflict`,\n\t\t\ttitle: \"A request is outstanding for this idempotency key\",\n\t\t\tstatus: 409,\n\t\t\tdetail: \"A request with the same idempotency key is currently being processed\",\n\t\t};\n\t},\n} as const;\n","export async function generateFingerprint(\n\tmethod: string,\n\tpath: string,\n\tbody: string,\n): Promise<string> {\n\tconst data = `${method}:${path}:${body}`;\n\tconst encoded = new TextEncoder().encode(data);\n\tconst hashBuffer = await crypto.subtle.digest(\"SHA-256\", encoded);\n\tconst hashArray = new Uint8Array(hashBuffer);\n\treturn Array.from(hashArray)\n\t\t.map((b) => b.toString(16).padStart(2, \"0\"))\n\t\t.join(\"\");\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as hono from 'hono';
|
|
2
|
-
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-
|
|
3
|
-
export { b as IdempotencyRecord, c as IdempotencyStore, P as ProblemDetail, S as StoredResponse } from './types-
|
|
2
|
+
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-wjiOEpHT.cjs';
|
|
3
|
+
export { b as IdempotencyRecord, c as IdempotencyStore, P as ProblemDetail, S as StoredResponse } from './types-wjiOEpHT.cjs';
|
|
4
|
+
export { MemoryStore } from './stores/memory.cjs';
|
|
4
5
|
|
|
5
6
|
declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
|
|
6
7
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as hono from 'hono';
|
|
2
|
-
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-
|
|
3
|
-
export { b as IdempotencyRecord, c as IdempotencyStore, P as ProblemDetail, S as StoredResponse } from './types-
|
|
2
|
+
import { I as IdempotencyOptions, a as IdempotencyEnv } from './types-wjiOEpHT.js';
|
|
3
|
+
export { b as IdempotencyRecord, c as IdempotencyStore, P as ProblemDetail, S as StoredResponse } from './types-wjiOEpHT.js';
|
|
4
|
+
export { MemoryStore } from './stores/memory.js';
|
|
4
5
|
|
|
5
6
|
declare function idempotency(options: IdempotencyOptions): hono.MiddlewareHandler<IdempotencyEnv, string, {}, Response>;
|
|
6
7
|
|
package/dist/stores/memory.cjs
CHANGED
|
@@ -26,6 +26,7 @@ module.exports = __toCommonJS(memory_exports);
|
|
|
26
26
|
var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
|
|
27
27
|
function memoryStore(options = {}) {
|
|
28
28
|
const ttl = options.ttl ?? DEFAULT_TTL;
|
|
29
|
+
const maxSize = options.maxSize;
|
|
29
30
|
const map = /* @__PURE__ */ new Map();
|
|
30
31
|
const isExpired = (record) => {
|
|
31
32
|
return Date.now() - record.createdAt >= ttl;
|
|
@@ -57,6 +58,12 @@ function memoryStore(options = {}) {
|
|
|
57
58
|
return false;
|
|
58
59
|
}
|
|
59
60
|
map.set(key, record);
|
|
61
|
+
if (maxSize !== void 0) {
|
|
62
|
+
while (map.size > maxSize) {
|
|
63
|
+
const oldest = map.keys().next().value;
|
|
64
|
+
if (oldest !== void 0) map.delete(oldest);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
60
67
|
return true;
|
|
61
68
|
},
|
|
62
69
|
async complete(key, response) {
|
|
@@ -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 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;
|
|
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\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\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,EACD;AACD;","names":[]}
|
package/dist/stores/memory.d.cts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { c as IdempotencyStore } from '../types-
|
|
1
|
+
import { c as IdempotencyStore } from '../types-wjiOEpHT.cjs';
|
|
2
2
|
import 'hono';
|
|
3
3
|
|
|
4
4
|
interface MemoryStoreOptions {
|
|
5
5
|
ttl?: number;
|
|
6
|
+
/** Maximum number of entries. Oldest entries are evicted when exceeded. */
|
|
7
|
+
maxSize?: number;
|
|
6
8
|
}
|
|
7
9
|
interface MemoryStore extends IdempotencyStore {
|
|
8
10
|
/** Number of entries currently in the store (including expired but not yet swept). */
|
package/dist/stores/memory.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { c as IdempotencyStore } from '../types-
|
|
1
|
+
import { c as IdempotencyStore } from '../types-wjiOEpHT.js';
|
|
2
2
|
import 'hono';
|
|
3
3
|
|
|
4
4
|
interface MemoryStoreOptions {
|
|
5
5
|
ttl?: number;
|
|
6
|
+
/** Maximum number of entries. Oldest entries are evicted when exceeded. */
|
|
7
|
+
maxSize?: number;
|
|
6
8
|
}
|
|
7
9
|
interface MemoryStore extends IdempotencyStore {
|
|
8
10
|
/** Number of entries currently in the store (including expired but not yet swept). */
|
package/dist/stores/memory.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
var DEFAULT_TTL = 24 * 60 * 60 * 1e3;
|
|
3
3
|
function memoryStore(options = {}) {
|
|
4
4
|
const ttl = options.ttl ?? DEFAULT_TTL;
|
|
5
|
+
const maxSize = options.maxSize;
|
|
5
6
|
const map = /* @__PURE__ */ new Map();
|
|
6
7
|
const isExpired = (record) => {
|
|
7
8
|
return Date.now() - record.createdAt >= ttl;
|
|
@@ -33,6 +34,12 @@ function memoryStore(options = {}) {
|
|
|
33
34
|
return false;
|
|
34
35
|
}
|
|
35
36
|
map.set(key, record);
|
|
37
|
+
if (maxSize !== void 0) {
|
|
38
|
+
while (map.size > maxSize) {
|
|
39
|
+
const oldest = map.keys().next().value;
|
|
40
|
+
if (oldest !== void 0) map.delete(oldest);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
36
43
|
return true;
|
|
37
44
|
},
|
|
38
45
|
async complete(key, response) {
|
|
@@ -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 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;
|
|
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\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\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,EACD;AACD;","names":[]}
|
package/package.json
CHANGED