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 +94 -0
- package/dist/stores/cloudflare-d1.cjs +47 -2
- package/dist/stores/cloudflare-d1.cjs.map +1 -1
- package/dist/stores/cloudflare-d1.d.cts +19 -3
- package/dist/stores/cloudflare-d1.d.ts +19 -3
- package/dist/stores/cloudflare-d1.js +47 -2
- package/dist/stores/cloudflare-d1.js.map +1 -1
- package/dist/stores/cloudflare-kv.cjs +27 -2
- package/dist/stores/cloudflare-kv.cjs.map +1 -1
- package/dist/stores/cloudflare-kv.d.cts +15 -3
- package/dist/stores/cloudflare-kv.d.ts +15 -3
- package/dist/stores/cloudflare-kv.js +27 -2
- package/dist/stores/cloudflare-kv.js.map +1 -1
- package/dist/stores/memory.cjs +11 -0
- package/dist/stores/memory.cjs.map +1 -1
- package/dist/stores/memory.d.cts +6 -2
- package/dist/stores/memory.d.ts +6 -2
- package/dist/stores/memory.js +11 -0
- package/dist/stores/memory.js.map +1 -1
- package/package.json +3 -2
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
|
-
|
|
27
|
-
|
|
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\
|
|
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
|
|
19
|
+
/** Cloudflare D1 database binding. */
|
|
20
|
+
database: D1DatabaseLike;
|
|
21
|
+
/** Table name (default: "idempotency_keys"). */
|
|
6
22
|
tableName?: string;
|
|
7
23
|
}
|
|
8
|
-
declare function d1Store(
|
|
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
|
|
19
|
+
/** Cloudflare D1 database binding. */
|
|
20
|
+
database: D1DatabaseLike;
|
|
21
|
+
/** Table name (default: "idempotency_keys"). */
|
|
6
22
|
tableName?: string;
|
|
7
23
|
}
|
|
8
|
-
declare function d1Store(
|
|
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
|
-
|
|
3
|
-
|
|
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\
|
|
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
|
-
|
|
27
|
-
|
|
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\
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
3
|
-
|
|
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\
|
|
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":[]}
|
package/dist/stores/memory.cjs
CHANGED
|
@@ -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 = {}):
|
|
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":[]}
|
package/dist/stores/memory.d.cts
CHANGED
|
@@ -4,6 +4,10 @@ import 'hono';
|
|
|
4
4
|
interface MemoryStoreOptions {
|
|
5
5
|
ttl?: number;
|
|
6
6
|
}
|
|
7
|
-
|
|
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 };
|
package/dist/stores/memory.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ import 'hono';
|
|
|
4
4
|
interface MemoryStoreOptions {
|
|
5
5
|
ttl?: number;
|
|
6
6
|
}
|
|
7
|
-
|
|
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 };
|
package/dist/stores/memory.js
CHANGED
|
@@ -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 = {}):
|
|
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.
|
|
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
|
}
|