@oncely/client 1.0.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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/index.cjs +268 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +325 -0
- package/dist/index.d.ts +325 -0
- package/dist/index.js +252 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 stacks0x
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# @oncely/client
|
|
2
|
+
|
|
3
|
+
Client-side utilities for idempotency key generation and response handling.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@oncely/client)
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @oncely/client
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { generateKey } from '@oncely/client';
|
|
17
|
+
|
|
18
|
+
const response = await fetch('/api/orders', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'Content-Type': 'application/json',
|
|
22
|
+
'Idempotency-Key': generateKey(),
|
|
23
|
+
},
|
|
24
|
+
body: JSON.stringify(order),
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Key Generation
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { generateKey, generatePrefixedKey, createKeyGenerator } from '@oncely/client';
|
|
32
|
+
|
|
33
|
+
// UUID v4
|
|
34
|
+
const key = generateKey();
|
|
35
|
+
// => '550e8400-e29b-41d4-a716-446655440000'
|
|
36
|
+
|
|
37
|
+
// Prefixed UUID
|
|
38
|
+
const key = generatePrefixedKey('ord');
|
|
39
|
+
// => 'ord_550e8400-e29b-41d4-a716-446655440000'
|
|
40
|
+
|
|
41
|
+
// Deterministic key from components
|
|
42
|
+
const key = generateKey('user', userId, 'create-order');
|
|
43
|
+
// => 'f8e9b4a2' (consistent hash for same inputs)
|
|
44
|
+
|
|
45
|
+
// Key generator namespace
|
|
46
|
+
const key = createKeyGenerator();
|
|
47
|
+
key(); // UUID
|
|
48
|
+
key('a', 'b', 'c'); // Deterministic hash
|
|
49
|
+
key.prefixed('txn'); // Prefixed UUID
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Key Store
|
|
53
|
+
|
|
54
|
+
Track pending requests to prevent duplicate submissions:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { createStore } from '@oncely/client';
|
|
58
|
+
|
|
59
|
+
const store = createStore({
|
|
60
|
+
prefix: 'oncely:',
|
|
61
|
+
ttl: '5m',
|
|
62
|
+
storage: localStorage,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Mark request as pending
|
|
66
|
+
store.pending('key-123');
|
|
67
|
+
|
|
68
|
+
// Check if request is pending
|
|
69
|
+
if (store.isPending('key-123')) {
|
|
70
|
+
throw new Error('Request already in progress');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Mark as completed
|
|
74
|
+
store.complete('key-123');
|
|
75
|
+
|
|
76
|
+
// Clear a specific key
|
|
77
|
+
store.clear('key-123');
|
|
78
|
+
|
|
79
|
+
// Clear all oncely keys
|
|
80
|
+
store.clearAll();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Response Helpers
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
import { isReplay, isConflict, isMismatch, getRetryAfter } from '@oncely/client';
|
|
87
|
+
|
|
88
|
+
const response = await fetch('/api/orders', { ... });
|
|
89
|
+
|
|
90
|
+
// Check if response was served from cache
|
|
91
|
+
if (isReplay(response)) {
|
|
92
|
+
console.log('Cached response');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Handle 409 Conflict
|
|
96
|
+
if (isConflict(response)) {
|
|
97
|
+
const seconds = getRetryAfter(response);
|
|
98
|
+
await delay(seconds * 1000);
|
|
99
|
+
// Retry with same key...
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Handle 422 Mismatch
|
|
103
|
+
if (isMismatch(response)) {
|
|
104
|
+
// Key was reused with different body - generate new key
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Usage with Fetch Libraries
|
|
109
|
+
|
|
110
|
+
### ky
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import ky from 'ky';
|
|
114
|
+
import { generateKey, IDEMPOTENCY_KEY_HEADER } from '@oncely/client';
|
|
115
|
+
|
|
116
|
+
const api = ky.extend({
|
|
117
|
+
hooks: {
|
|
118
|
+
beforeRequest: [
|
|
119
|
+
(request) => {
|
|
120
|
+
if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
|
|
121
|
+
request.headers.set(IDEMPOTENCY_KEY_HEADER, generateKey());
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### axios
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import axios from 'axios';
|
|
133
|
+
import { generateKey, IDEMPOTENCY_KEY_HEADER } from '@oncely/client';
|
|
134
|
+
|
|
135
|
+
const api = axios.create();
|
|
136
|
+
|
|
137
|
+
api.interceptors.request.use((config) => {
|
|
138
|
+
if (['post', 'put', 'patch'].includes(config.method ?? '')) {
|
|
139
|
+
config.headers[IDEMPOTENCY_KEY_HEADER] = generateKey();
|
|
140
|
+
}
|
|
141
|
+
return config;
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Constants
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
import { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER } from '@oncely/client';
|
|
149
|
+
|
|
150
|
+
IDEMPOTENCY_KEY_HEADER; // 'Idempotency-Key'
|
|
151
|
+
IDEMPOTENCY_REPLAY_HEADER; // 'Idempotency-Replay'
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/constants.ts
|
|
4
|
+
var HEADER = "Idempotency-Key";
|
|
5
|
+
var HEADER_REPLAY = "Idempotency-Replay";
|
|
6
|
+
|
|
7
|
+
// src/key.ts
|
|
8
|
+
function generateKey(...components) {
|
|
9
|
+
if (components.length === 0) {
|
|
10
|
+
return generateUUID();
|
|
11
|
+
}
|
|
12
|
+
const input = components.map(String).join(":");
|
|
13
|
+
return simpleHash(input);
|
|
14
|
+
}
|
|
15
|
+
function generatePrefixedKey(prefix) {
|
|
16
|
+
return `${prefix}_${generateUUID()}`;
|
|
17
|
+
}
|
|
18
|
+
function generateUUID() {
|
|
19
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
20
|
+
return crypto.randomUUID();
|
|
21
|
+
}
|
|
22
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
23
|
+
let r;
|
|
24
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
25
|
+
const arr = new Uint8Array(1);
|
|
26
|
+
crypto.getRandomValues(arr);
|
|
27
|
+
r = arr[0] & 15;
|
|
28
|
+
} else {
|
|
29
|
+
r = Math.random() * 16 | 0;
|
|
30
|
+
}
|
|
31
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
32
|
+
return v.toString(16);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function simpleHash(str) {
|
|
36
|
+
let hash = 5381;
|
|
37
|
+
for (let i = 0; i < str.length; i++) {
|
|
38
|
+
hash = hash * 33 ^ str.charCodeAt(i);
|
|
39
|
+
}
|
|
40
|
+
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
41
|
+
}
|
|
42
|
+
function createKeyGenerator() {
|
|
43
|
+
const key = ((...components) => {
|
|
44
|
+
return generateKey(...components);
|
|
45
|
+
});
|
|
46
|
+
key.prefixed = generatePrefixedKey;
|
|
47
|
+
return key;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/store.ts
|
|
51
|
+
var MemoryStorage = class {
|
|
52
|
+
data = /* @__PURE__ */ new Map();
|
|
53
|
+
getItem(key) {
|
|
54
|
+
return this.data.get(key) ?? null;
|
|
55
|
+
}
|
|
56
|
+
setItem(key, value) {
|
|
57
|
+
this.data.set(key, value);
|
|
58
|
+
}
|
|
59
|
+
removeItem(key) {
|
|
60
|
+
this.data.delete(key);
|
|
61
|
+
}
|
|
62
|
+
clear() {
|
|
63
|
+
this.data.clear();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function parseTtl(ttl) {
|
|
67
|
+
if (typeof ttl === "number") {
|
|
68
|
+
return ttl;
|
|
69
|
+
}
|
|
70
|
+
const match = ttl.match(/^(\d+)(ms|s|m|h|d)$/);
|
|
71
|
+
if (!match) {
|
|
72
|
+
throw new Error(`Invalid TTL format: ${ttl}. Use format like '5m', '1h', '30s'`);
|
|
73
|
+
}
|
|
74
|
+
const value = parseInt(match[1], 10);
|
|
75
|
+
const unit = match[2];
|
|
76
|
+
const multipliers = {
|
|
77
|
+
ms: 1,
|
|
78
|
+
s: 1e3,
|
|
79
|
+
m: 60 * 1e3,
|
|
80
|
+
h: 60 * 60 * 1e3,
|
|
81
|
+
d: 24 * 60 * 60 * 1e3
|
|
82
|
+
};
|
|
83
|
+
return value * multipliers[unit];
|
|
84
|
+
}
|
|
85
|
+
function createStore(options = {}) {
|
|
86
|
+
const prefix = options.prefix ?? "oncely:";
|
|
87
|
+
const ttl = parseTtl(options.ttl ?? "5m");
|
|
88
|
+
let storage;
|
|
89
|
+
if (options.storage) {
|
|
90
|
+
storage = options.storage;
|
|
91
|
+
} else if (typeof globalThis !== "undefined" && "localStorage" in globalThis) {
|
|
92
|
+
storage = globalThis.localStorage;
|
|
93
|
+
} else {
|
|
94
|
+
storage = new MemoryStorage();
|
|
95
|
+
}
|
|
96
|
+
const prefixedKey = (key) => `${prefix}${key}`;
|
|
97
|
+
return {
|
|
98
|
+
pending(key) {
|
|
99
|
+
const expires = Date.now() + ttl;
|
|
100
|
+
storage.setItem(prefixedKey(key), JSON.stringify({ expires }));
|
|
101
|
+
},
|
|
102
|
+
isPending(key) {
|
|
103
|
+
const value = storage.getItem(prefixedKey(key));
|
|
104
|
+
if (!value) return false;
|
|
105
|
+
try {
|
|
106
|
+
const { expires } = JSON.parse(value);
|
|
107
|
+
if (Date.now() > expires) {
|
|
108
|
+
storage.removeItem(prefixedKey(key));
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
} catch {
|
|
113
|
+
storage.removeItem(prefixedKey(key));
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
clear(key) {
|
|
118
|
+
storage.removeItem(prefixedKey(key));
|
|
119
|
+
},
|
|
120
|
+
clearAll() {
|
|
121
|
+
if (storage instanceof MemoryStorage) {
|
|
122
|
+
storage.clear();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const keysToRemove = [];
|
|
126
|
+
try {
|
|
127
|
+
const browserStorage = storage;
|
|
128
|
+
if (typeof browserStorage.length === "number" && typeof browserStorage.key === "function") {
|
|
129
|
+
for (let i = 0; i < browserStorage.length; i++) {
|
|
130
|
+
const key = browserStorage.key(i);
|
|
131
|
+
if (key?.startsWith(prefix)) {
|
|
132
|
+
keysToRemove.push(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
keysToRemove.forEach((key) => storage.removeItem(key));
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/response.ts
|
|
145
|
+
function isReplay(response) {
|
|
146
|
+
return response.headers.get(HEADER_REPLAY)?.toLowerCase() === "true";
|
|
147
|
+
}
|
|
148
|
+
function isConflict(response) {
|
|
149
|
+
return response.status === 409;
|
|
150
|
+
}
|
|
151
|
+
function isMismatch(response) {
|
|
152
|
+
return response.status === 422;
|
|
153
|
+
}
|
|
154
|
+
function isMissingKey(response) {
|
|
155
|
+
return response.status === 400;
|
|
156
|
+
}
|
|
157
|
+
function getRetryAfter(response, defaultValue = 1) {
|
|
158
|
+
const header = response.headers.get("Retry-After");
|
|
159
|
+
if (!header) return defaultValue;
|
|
160
|
+
const seconds = parseInt(header, 10);
|
|
161
|
+
if (isNaN(seconds) || seconds < 0) return defaultValue;
|
|
162
|
+
return seconds;
|
|
163
|
+
}
|
|
164
|
+
async function getProblem(response) {
|
|
165
|
+
try {
|
|
166
|
+
const responseToRead = response.clone ? response.clone() : response;
|
|
167
|
+
if (!responseToRead.json) return null;
|
|
168
|
+
const body = await responseToRead.json();
|
|
169
|
+
if (typeof body === "object" && body !== null && "type" in body && "title" in body && "status" in body) {
|
|
170
|
+
return body;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function isIdempotencyError(response) {
|
|
178
|
+
return isMissingKey(response) || isConflict(response) || isMismatch(response);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/index.ts
|
|
182
|
+
var oncely = {
|
|
183
|
+
/**
|
|
184
|
+
* Generate a unique idempotency key.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```typescript
|
|
188
|
+
* // Random UUID key
|
|
189
|
+
* const key = oncely.key();
|
|
190
|
+
*
|
|
191
|
+
* // Deterministic key from components
|
|
192
|
+
* const key = oncely.key('user-123', 'action', timestamp);
|
|
193
|
+
*
|
|
194
|
+
* // Prefixed key
|
|
195
|
+
* const key = oncely.key.prefixed('ord');
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
key: createKeyGenerator(),
|
|
199
|
+
/**
|
|
200
|
+
* Create a key store for managing pending requests.
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* const store = oncely.store();
|
|
205
|
+
*
|
|
206
|
+
* if (store.isPending(key)) {
|
|
207
|
+
* throw new Error('Request in progress');
|
|
208
|
+
* }
|
|
209
|
+
* store.pending(key);
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
store: createStore,
|
|
213
|
+
/**
|
|
214
|
+
* Check if response is a replay (served from cache).
|
|
215
|
+
*/
|
|
216
|
+
isReplay,
|
|
217
|
+
/**
|
|
218
|
+
* Check if response is a conflict (409).
|
|
219
|
+
*/
|
|
220
|
+
isConflict,
|
|
221
|
+
/**
|
|
222
|
+
* Check if response is a mismatch (422).
|
|
223
|
+
*/
|
|
224
|
+
isMismatch,
|
|
225
|
+
/**
|
|
226
|
+
* Check if response is a missing key error (400).
|
|
227
|
+
*/
|
|
228
|
+
isMissingKey,
|
|
229
|
+
/**
|
|
230
|
+
* Check if response is any idempotency error (400, 409, 422).
|
|
231
|
+
*/
|
|
232
|
+
isIdempotencyError,
|
|
233
|
+
/**
|
|
234
|
+
* Get Retry-After value from response.
|
|
235
|
+
*/
|
|
236
|
+
getRetryAfter,
|
|
237
|
+
/**
|
|
238
|
+
* Parse RFC 7807 Problem Details from response.
|
|
239
|
+
*/
|
|
240
|
+
getProblem,
|
|
241
|
+
/**
|
|
242
|
+
* Standard header name for idempotency keys.
|
|
243
|
+
* @see https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header
|
|
244
|
+
*/
|
|
245
|
+
HEADER,
|
|
246
|
+
/**
|
|
247
|
+
* Header indicating a response was replayed from cache.
|
|
248
|
+
*/
|
|
249
|
+
HEADER_REPLAY
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
exports.HEADER = HEADER;
|
|
253
|
+
exports.HEADER_REPLAY = HEADER_REPLAY;
|
|
254
|
+
exports.MemoryStorage = MemoryStorage;
|
|
255
|
+
exports.createKeyGenerator = createKeyGenerator;
|
|
256
|
+
exports.createStore = createStore;
|
|
257
|
+
exports.generateKey = generateKey;
|
|
258
|
+
exports.generatePrefixedKey = generatePrefixedKey;
|
|
259
|
+
exports.getProblem = getProblem;
|
|
260
|
+
exports.getRetryAfter = getRetryAfter;
|
|
261
|
+
exports.isConflict = isConflict;
|
|
262
|
+
exports.isIdempotencyError = isIdempotencyError;
|
|
263
|
+
exports.isMismatch = isMismatch;
|
|
264
|
+
exports.isMissingKey = isMissingKey;
|
|
265
|
+
exports.isReplay = isReplay;
|
|
266
|
+
exports.oncely = oncely;
|
|
267
|
+
//# sourceMappingURL=index.cjs.map
|
|
268
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants.ts","../src/key.ts","../src/store.ts","../src/response.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * HTTP header constants following IETF draft-ietf-httpapi-idempotency-key-header.\n * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header\n */\n\n/** Standard header name for idempotency keys */\nexport const HEADER = 'Idempotency-Key';\n\n/** Header indicating a response was replayed from cache */\nexport const HEADER_REPLAY = 'Idempotency-Replay';\n","/**\n * Key generation utilities for idempotency keys.\n */\n\n/**\n * Generate a unique idempotency key.\n *\n * When called with no arguments, generates a random UUID v4.\n * When called with components, generates a deterministic hash-based key.\n *\n * @example\n * ```typescript\n * // Random key (UUID v4)\n * const key = generateKey();\n * // => \"550e8400-e29b-41d4-a716-446655440000\"\n *\n * // Deterministic key from components\n * const key = generateKey('user-123', 'create-order', 1234567890);\n * // => \"f7c3bc1d...\" (consistent for same inputs)\n * ```\n */\nexport function generateKey(...components: (string | number | boolean)[]): string {\n if (components.length === 0) {\n return generateUUID();\n }\n\n // Create deterministic key from components\n const input = components.map(String).join(':');\n return simpleHash(input);\n}\n\n/**\n * Generate a prefixed idempotency key.\n *\n * @example\n * ```typescript\n * const key = generatePrefixedKey('ord');\n * // => \"ord_550e8400-e29b-41d4-a716-446655440000\"\n * ```\n */\nexport function generatePrefixedKey(prefix: string): string {\n return `${prefix}_${generateUUID()}`;\n}\n\n/**\n * Generate a UUID v4.\n * Uses crypto.randomUUID() in modern environments, falls back to custom implementation.\n */\nfunction generateUUID(): string {\n // Modern browsers and Node.js 19+\n if (typeof crypto !== 'undefined' && crypto.randomUUID) {\n return crypto.randomUUID();\n }\n\n // Fallback for older environments\n return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {\n let r: number;\n if (typeof crypto !== 'undefined' && crypto.getRandomValues) {\n const arr = new Uint8Array(1);\n crypto.getRandomValues(arr);\n r = arr[0]! & 15;\n } else {\n r = (Math.random() * 16) | 0;\n }\n const v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Simple hash function for deterministic key generation.\n * Uses djb2 algorithm for fast, reasonably distributed hashes.\n */\nfunction simpleHash(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 33) ^ str.charCodeAt(i);\n }\n // Convert to hex and pad to 16 chars\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\n/**\n * Key generator interface for the namespace.\n */\nexport interface KeyGenerator {\n /**\n * Generate a unique key, optionally from components.\n */\n (...components: (string | number | boolean)[]): string;\n\n /**\n * Generate a prefixed key.\n */\n prefixed(prefix: string): string;\n}\n\n/**\n * Create the key generator function with attached methods.\n */\nexport function createKeyGenerator(): KeyGenerator {\n const key = ((...components: (string | number | boolean)[]) => {\n return generateKey(...components);\n }) as KeyGenerator;\n\n key.prefixed = generatePrefixedKey;\n\n return key;\n}\n","/**\n * Key store for managing pending idempotency keys.\n */\n\n/**\n * Options for creating a key store.\n */\nexport interface StoreOptions {\n /**\n * Storage backend (localStorage, sessionStorage, or custom).\n * @default localStorage in browser, in-memory in Node.js\n */\n storage?: Storage | MemoryStorage;\n\n /**\n * Key prefix for namespacing.\n * @default 'oncely:'\n */\n prefix?: string;\n\n /**\n * TTL for pending keys in milliseconds or string format.\n * After this time, pending keys are considered expired.\n * @default '5m'\n */\n ttl?: number | string;\n}\n\n/**\n * Browser localStorage interface for type checking.\n */\ninterface BrowserStorage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n clear(): void;\n length: number;\n key(index: number): string | null;\n}\n\n/**\n * Minimal storage interface compatible with localStorage/sessionStorage.\n */\nexport interface Storage {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n clear(): void;\n}\n\n/**\n * Key store for managing pending idempotency keys.\n */\nexport interface KeyStore {\n /**\n * Mark a key as pending (request in flight).\n */\n pending(key: string): void;\n\n /**\n * Check if a key is currently pending.\n */\n isPending(key: string): boolean;\n\n /**\n * Clear a pending key.\n */\n clear(key: string): void;\n\n /**\n * Clear all pending keys.\n */\n clearAll(): void;\n}\n\n/**\n * In-memory storage implementation.\n */\nexport class MemoryStorage implements Storage {\n private data = new Map<string, string>();\n\n getItem(key: string): string | null {\n return this.data.get(key) ?? null;\n }\n\n setItem(key: string, value: string): void {\n this.data.set(key, value);\n }\n\n removeItem(key: string): void {\n this.data.delete(key);\n }\n\n clear(): void {\n this.data.clear();\n }\n}\n\n/**\n * Parse TTL string to milliseconds.\n */\nfunction parseTtl(ttl: number | string): number {\n if (typeof ttl === 'number') {\n return ttl;\n }\n\n const match = ttl.match(/^(\\d+)(ms|s|m|h|d)$/);\n if (!match) {\n throw new Error(`Invalid TTL format: ${ttl}. Use format like '5m', '1h', '30s'`);\n }\n\n const value = parseInt(match[1]!, 10);\n const unit = match[2]!;\n\n const multipliers: Record<string, number> = {\n ms: 1,\n s: 1000,\n m: 60 * 1000,\n h: 60 * 60 * 1000,\n d: 24 * 60 * 60 * 1000,\n };\n\n return value * multipliers[unit]!;\n}\n\n/**\n * Create a key store for managing pending idempotency keys.\n *\n * @example\n * ```typescript\n * const store = createStore();\n *\n * // Before making request\n * if (store.isPending('key-123')) {\n * throw new Error('Request already in progress');\n * }\n * store.pending('key-123');\n *\n * try {\n * await fetch('/api/orders', { ... });\n * } finally {\n * store.clear('key-123');\n * }\n * ```\n */\nexport function createStore(options: StoreOptions = {}): KeyStore {\n const prefix = options.prefix ?? 'oncely:';\n const ttl = parseTtl(options.ttl ?? '5m');\n\n // Determine storage backend\n let storage: Storage;\n if (options.storage) {\n storage = options.storage;\n } else if (typeof globalThis !== 'undefined' && 'localStorage' in globalThis) {\n storage = (globalThis as unknown as { localStorage: Storage }).localStorage;\n } else {\n storage = new MemoryStorage();\n }\n\n const prefixedKey = (key: string) => `${prefix}${key}`;\n\n return {\n pending(key: string): void {\n const expires = Date.now() + ttl;\n storage.setItem(prefixedKey(key), JSON.stringify({ expires }));\n },\n\n isPending(key: string): boolean {\n const value = storage.getItem(prefixedKey(key));\n if (!value) return false;\n\n try {\n const { expires } = JSON.parse(value);\n if (Date.now() > expires) {\n // Key has expired, clean it up\n storage.removeItem(prefixedKey(key));\n return false;\n }\n return true;\n } catch {\n // Invalid data, remove it\n storage.removeItem(prefixedKey(key));\n return false;\n }\n },\n\n clear(key: string): void {\n storage.removeItem(prefixedKey(key));\n },\n\n clearAll(): void {\n // For custom storage with proper clear, use it\n // For localStorage/sessionStorage, we need to only clear our keys\n if (storage instanceof MemoryStorage) {\n storage.clear();\n return;\n }\n\n // For browser storage, we need to iterate and find our keys\n // This is a workaround since Storage doesn't expose keys()\n const keysToRemove: string[] = [];\n try {\n const browserStorage = storage as BrowserStorage;\n if (typeof browserStorage.length === 'number' && typeof browserStorage.key === 'function') {\n for (let i = 0; i < browserStorage.length; i++) {\n const key = browserStorage.key(i);\n if (key?.startsWith(prefix)) {\n keysToRemove.push(key);\n }\n }\n }\n } catch {\n // If we can't iterate, just return\n return;\n }\n\n keysToRemove.forEach((key) => storage.removeItem(key));\n },\n };\n}\n","/**\n * Response helper utilities for detecting idempotency-related responses.\n */\n\nimport { HEADER_REPLAY } from './constants.js';\n\n/**\n * RFC 7807 Problem Details response format.\n * @see https://www.rfc-editor.org/rfc/rfc7807\n */\nexport interface ProblemDetails {\n /** URI reference identifying the problem type */\n type: string;\n /** Short human-readable summary */\n title: string;\n /** HTTP status code */\n status: number;\n /** Detailed human-readable explanation */\n detail: string;\n /** URI reference to the specific occurrence (optional) */\n instance?: string;\n /** Retry-After value in seconds (for conflict errors) */\n retryAfter?: number;\n /** Additional properties */\n [key: string]: unknown;\n}\n\n/**\n * Response-like object for compatibility with various fetch implementations.\n */\nexport interface ResponseLike {\n status: number;\n headers: {\n get(name: string): string | null;\n };\n json?(): Promise<unknown>;\n clone?(): ResponseLike;\n}\n\n/**\n * Check if a response is a replay (cached response).\n *\n * @example\n * ```typescript\n * const response = await fetch('/api/orders', { ... });\n * if (isReplay(response)) {\n * console.log('Response was served from cache');\n * }\n * ```\n */\nexport function isReplay(response: ResponseLike): boolean {\n return response.headers.get(HEADER_REPLAY)?.toLowerCase() === 'true';\n}\n\n/**\n * Check if a response indicates a conflict (409).\n * This means a request with the same key is already in progress.\n *\n * @example\n * ```typescript\n * if (isConflict(response)) {\n * const retryAfter = getRetryAfter(response);\n * await delay(retryAfter * 1000);\n * // Retry the request\n * }\n * ```\n */\nexport function isConflict(response: ResponseLike): boolean {\n return response.status === 409;\n}\n\n/**\n * Check if a response indicates a mismatch (422).\n * This means the idempotency key was reused with a different request payload.\n *\n * @example\n * ```typescript\n * if (isMismatch(response)) {\n * // Generate a new key and retry\n * const newKey = oncely.key();\n * // Retry with new key\n * }\n * ```\n */\nexport function isMismatch(response: ResponseLike): boolean {\n return response.status === 422;\n}\n\n/**\n * Check if a response indicates a missing key error (400).\n * This means the idempotency key header was required but not provided.\n */\nexport function isMissingKey(response: ResponseLike): boolean {\n return response.status === 400;\n}\n\n/**\n * Get the Retry-After value from a response.\n * Returns the value in seconds, or the default if not present.\n *\n * @param response - The response to check\n * @param defaultValue - Default value if header is not present (default: 1)\n * @returns Retry delay in seconds\n */\nexport function getRetryAfter(response: ResponseLike, defaultValue = 1): number {\n const header = response.headers.get('Retry-After');\n if (!header) return defaultValue;\n\n const seconds = parseInt(header, 10);\n if (isNaN(seconds) || seconds < 0) return defaultValue;\n\n return seconds;\n}\n\n/**\n * Parse RFC 7807 Problem Details from an error response.\n *\n * @example\n * ```typescript\n * if (!response.ok) {\n * const problem = await getProblem(response);\n * console.error(`${problem.title}: ${problem.detail}`);\n * }\n * ```\n */\nexport async function getProblem(response: ResponseLike): Promise<ProblemDetails | null> {\n try {\n // Clone to avoid consuming the body\n const responseToRead = response.clone ? response.clone() : response;\n if (!responseToRead.json) return null;\n\n const body = await responseToRead.json();\n\n // Validate it looks like Problem Details\n if (\n typeof body === 'object' &&\n body !== null &&\n 'type' in body &&\n 'title' in body &&\n 'status' in body\n ) {\n return body as ProblemDetails;\n }\n\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Check if a response is an idempotency error (400, 409, or 422).\n */\nexport function isIdempotencyError(response: ResponseLike): boolean {\n return isMissingKey(response) || isConflict(response) || isMismatch(response);\n}\n","/**\n * @oncely/client - Client-side idempotency helpers\n *\n * @example\n * ```typescript\n * import { oncely } from '@oncely/client';\n *\n * // Generate a unique key\n * const key = oncely.key();\n *\n * // Make request with idempotency\n * const response = await fetch('/api/orders', {\n * method: 'POST',\n * headers: { [oncely.HEADER]: key },\n * body: JSON.stringify(data),\n * });\n *\n * // Check response type\n * if (oncely.isReplay(response)) {\n * console.log('Cached response');\n * }\n * ```\n */\n\nimport { HEADER, HEADER_REPLAY } from './constants.js';\nimport { createKeyGenerator } from './key.js';\nimport { createStore } from './store.js';\nimport {\n isReplay,\n isConflict,\n isMismatch,\n isMissingKey,\n isIdempotencyError,\n getRetryAfter,\n getProblem,\n} from './response.js';\n\n/**\n * Oncely client namespace.\n * All client-side functionality is accessed through this namespace.\n */\nexport const oncely = {\n /**\n * Generate a unique idempotency key.\n *\n * @example\n * ```typescript\n * // Random UUID key\n * const key = oncely.key();\n *\n * // Deterministic key from components\n * const key = oncely.key('user-123', 'action', timestamp);\n *\n * // Prefixed key\n * const key = oncely.key.prefixed('ord');\n * ```\n */\n key: createKeyGenerator(),\n\n /**\n * Create a key store for managing pending requests.\n *\n * @example\n * ```typescript\n * const store = oncely.store();\n *\n * if (store.isPending(key)) {\n * throw new Error('Request in progress');\n * }\n * store.pending(key);\n * ```\n */\n store: createStore,\n\n /**\n * Check if response is a replay (served from cache).\n */\n isReplay,\n\n /**\n * Check if response is a conflict (409).\n */\n isConflict,\n\n /**\n * Check if response is a mismatch (422).\n */\n isMismatch,\n\n /**\n * Check if response is a missing key error (400).\n */\n isMissingKey,\n\n /**\n * Check if response is any idempotency error (400, 409, 422).\n */\n isIdempotencyError,\n\n /**\n * Get Retry-After value from response.\n */\n getRetryAfter,\n\n /**\n * Parse RFC 7807 Problem Details from response.\n */\n getProblem,\n\n /**\n * Standard header name for idempotency keys.\n * @see https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-idempotency-key-header\n */\n HEADER,\n\n /**\n * Header indicating a response was replayed from cache.\n */\n HEADER_REPLAY,\n} as const;\n\n// Type for the oncely namespace\nexport type OncelyClient = typeof oncely;\n\n// Re-export individual components for direct imports\nexport { HEADER, HEADER_REPLAY } from './constants.js';\nexport { generateKey, generatePrefixedKey, createKeyGenerator, type KeyGenerator } from './key.js';\nexport {\n createStore,\n MemoryStorage,\n type KeyStore,\n type StoreOptions,\n type Storage,\n} from './store.js';\nexport {\n isReplay,\n isConflict,\n isMismatch,\n isMissingKey,\n isIdempotencyError,\n getRetryAfter,\n getProblem,\n type ProblemDetails,\n type ResponseLike,\n} from './response.js';\n"]}
|