@oncely/core 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 +661 -0
- package/dist/errors-BUehgS6t.d.cts +310 -0
- package/dist/errors-BUehgS6t.d.ts +310 -0
- package/dist/index.cjs +470 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +183 -0
- package/dist/index.d.ts +183 -0
- package/dist/index.js +447 -0
- package/dist/index.js.map +1 -0
- package/dist/testing.cjs +518 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +119 -0
- package/dist/testing.d.ts +119 -0
- package/dist/testing.js +508 -0
- package/dist/testing.js.map +1 -0
- package/package.json +59 -0
package/dist/testing.cjs
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('crypto');
|
|
4
|
+
|
|
5
|
+
// src/memory.ts
|
|
6
|
+
var MemoryStorage = class {
|
|
7
|
+
store = /* @__PURE__ */ new Map();
|
|
8
|
+
cleanupInterval = null;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.cleanupInterval = setInterval(() => this.cleanup(), 6e4);
|
|
11
|
+
if (this.cleanupInterval.unref) {
|
|
12
|
+
this.cleanupInterval.unref();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
async acquire(key, hash, ttl) {
|
|
16
|
+
const now = Date.now();
|
|
17
|
+
let existing = this.store.get(key);
|
|
18
|
+
if (existing && existing.expiresAt <= now) {
|
|
19
|
+
this.store.delete(key);
|
|
20
|
+
existing = void 0;
|
|
21
|
+
}
|
|
22
|
+
if (existing) {
|
|
23
|
+
const existingHash = existing.hash;
|
|
24
|
+
if (hash && existingHash && existingHash !== hash) {
|
|
25
|
+
return {
|
|
26
|
+
status: "mismatch",
|
|
27
|
+
existingHash,
|
|
28
|
+
providedHash: hash
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
if (existing.status === "completed") {
|
|
32
|
+
const response = existing.response;
|
|
33
|
+
if (response) {
|
|
34
|
+
return { status: "hit", response };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (existing.status === "in_progress") {
|
|
38
|
+
return { status: "conflict", startedAt: existing.startedAt };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.store.set(key, {
|
|
42
|
+
status: "in_progress",
|
|
43
|
+
hash,
|
|
44
|
+
startedAt: now,
|
|
45
|
+
expiresAt: now + ttl
|
|
46
|
+
});
|
|
47
|
+
return { status: "acquired" };
|
|
48
|
+
}
|
|
49
|
+
async save(key, response) {
|
|
50
|
+
const existing = this.store.get(key);
|
|
51
|
+
if (existing) {
|
|
52
|
+
existing.status = "completed";
|
|
53
|
+
existing.response = response;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async release(key) {
|
|
57
|
+
this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
async delete(key) {
|
|
60
|
+
this.store.delete(key);
|
|
61
|
+
}
|
|
62
|
+
async clear() {
|
|
63
|
+
this.store.clear();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stop the cleanup interval.
|
|
67
|
+
* Call this when shutting down to prevent memory leaks in tests.
|
|
68
|
+
*/
|
|
69
|
+
destroy() {
|
|
70
|
+
if (this.cleanupInterval) {
|
|
71
|
+
clearInterval(this.cleanupInterval);
|
|
72
|
+
this.cleanupInterval = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
cleanup() {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
for (const [key, record] of this.store) {
|
|
78
|
+
if (record.expiresAt <= now) {
|
|
79
|
+
this.store.delete(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
new MemoryStorage();
|
|
85
|
+
|
|
86
|
+
// src/constants.ts
|
|
87
|
+
var DOCS_BASE_URL = "https://oncely.dev/errors";
|
|
88
|
+
|
|
89
|
+
// src/errors.ts
|
|
90
|
+
var IdempotencyError = class extends Error {
|
|
91
|
+
/** HTTP status code for this error */
|
|
92
|
+
statusCode;
|
|
93
|
+
/** Error type identifier (URL) */
|
|
94
|
+
type;
|
|
95
|
+
/** Short title for the error */
|
|
96
|
+
title;
|
|
97
|
+
constructor(message, statusCode, type, title) {
|
|
98
|
+
super(message);
|
|
99
|
+
this.name = "IdempotencyError";
|
|
100
|
+
this.statusCode = statusCode;
|
|
101
|
+
this.type = type;
|
|
102
|
+
this.title = title;
|
|
103
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Convert to RFC 7807 Problem Details format.
|
|
107
|
+
*/
|
|
108
|
+
toProblemDetails() {
|
|
109
|
+
return {
|
|
110
|
+
type: this.type,
|
|
111
|
+
title: this.title,
|
|
112
|
+
status: this.statusCode,
|
|
113
|
+
detail: this.message
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Convert to JSON (RFC 7807 format).
|
|
118
|
+
*/
|
|
119
|
+
toJSON() {
|
|
120
|
+
return this.toProblemDetails();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var MissingKeyError = class extends IdempotencyError {
|
|
124
|
+
constructor() {
|
|
125
|
+
super(
|
|
126
|
+
"This operation requires an Idempotency-Key header.",
|
|
127
|
+
400,
|
|
128
|
+
`${DOCS_BASE_URL}/missing-key`,
|
|
129
|
+
"Idempotency-Key is missing"
|
|
130
|
+
);
|
|
131
|
+
this.name = "MissingKeyError";
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
var ConflictError = class extends IdempotencyError {
|
|
135
|
+
/** When the in-progress request started */
|
|
136
|
+
startedAt;
|
|
137
|
+
/** Suggested retry delay in seconds */
|
|
138
|
+
retryAfter;
|
|
139
|
+
constructor(startedAt) {
|
|
140
|
+
const ageSeconds = Math.round((Date.now() - startedAt) / 1e3);
|
|
141
|
+
super(
|
|
142
|
+
"A request with the same Idempotency-Key is currently being processed.",
|
|
143
|
+
409,
|
|
144
|
+
`${DOCS_BASE_URL}/conflict`,
|
|
145
|
+
"Request in progress"
|
|
146
|
+
);
|
|
147
|
+
this.name = "ConflictError";
|
|
148
|
+
this.startedAt = startedAt;
|
|
149
|
+
this.retryAfter = Math.max(1, Math.min(5, ageSeconds));
|
|
150
|
+
}
|
|
151
|
+
toProblemDetails() {
|
|
152
|
+
return {
|
|
153
|
+
...super.toProblemDetails(),
|
|
154
|
+
retryAfter: this.retryAfter
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
var MismatchError = class extends IdempotencyError {
|
|
159
|
+
/** Hash of the original request */
|
|
160
|
+
existingHash;
|
|
161
|
+
/** Hash of the current request */
|
|
162
|
+
providedHash;
|
|
163
|
+
constructor(existingHash, providedHash) {
|
|
164
|
+
super(
|
|
165
|
+
"This Idempotency-Key was used with a different request payload.",
|
|
166
|
+
422,
|
|
167
|
+
`${DOCS_BASE_URL}/mismatch`,
|
|
168
|
+
"Idempotency-Key reused"
|
|
169
|
+
);
|
|
170
|
+
this.name = "MismatchError";
|
|
171
|
+
this.existingHash = existingHash;
|
|
172
|
+
this.providedHash = providedHash;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
var StorageError = class extends IdempotencyError {
|
|
176
|
+
/** The underlying error from the storage adapter */
|
|
177
|
+
cause;
|
|
178
|
+
constructor(message, cause) {
|
|
179
|
+
super(`Storage error: ${message}`, 500, `${DOCS_BASE_URL}/storage-error`, "Storage error");
|
|
180
|
+
this.name = "StorageError";
|
|
181
|
+
this.cause = cause;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
function parseTtl(ttl) {
|
|
185
|
+
if (typeof ttl === "number") {
|
|
186
|
+
return ttl;
|
|
187
|
+
}
|
|
188
|
+
if (ttl.length < 2) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Invalid TTL format: "${ttl}". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const unit = ttl[ttl.length - 1];
|
|
194
|
+
if (unit !== "s" && unit !== "m" && unit !== "h" && unit !== "d") {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Invalid TTL format: "${ttl}". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const valueStr = ttl.slice(0, -1);
|
|
200
|
+
if (!valueStr) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Invalid TTL format: "${ttl}". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
for (let i = 0; i < valueStr.length; i++) {
|
|
206
|
+
const code = valueStr.charCodeAt(i);
|
|
207
|
+
if (code < 48 || code > 57) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`Invalid TTL format: "${ttl}". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const value = parseInt(valueStr, 10);
|
|
214
|
+
switch (unit) {
|
|
215
|
+
case "s":
|
|
216
|
+
return value * 1e3;
|
|
217
|
+
case "m":
|
|
218
|
+
return value * 60 * 1e3;
|
|
219
|
+
case "h":
|
|
220
|
+
return value * 60 * 60 * 1e3;
|
|
221
|
+
case "d":
|
|
222
|
+
return value * 24 * 60 * 60 * 1e3;
|
|
223
|
+
default:
|
|
224
|
+
throw new Error(`Invalid TTL unit: "${unit}"`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/config.ts
|
|
229
|
+
var globalConfig = {};
|
|
230
|
+
var defaultMemoryStorage = null;
|
|
231
|
+
function configure(config) {
|
|
232
|
+
globalConfig = { ...config };
|
|
233
|
+
}
|
|
234
|
+
function getConfig() {
|
|
235
|
+
return globalConfig;
|
|
236
|
+
}
|
|
237
|
+
function resetConfig() {
|
|
238
|
+
globalConfig = {};
|
|
239
|
+
defaultMemoryStorage = null;
|
|
240
|
+
}
|
|
241
|
+
function getDefaultStorage() {
|
|
242
|
+
if (globalConfig.storage) {
|
|
243
|
+
return globalConfig.storage;
|
|
244
|
+
}
|
|
245
|
+
if (!defaultMemoryStorage) {
|
|
246
|
+
defaultMemoryStorage = new MemoryStorage();
|
|
247
|
+
}
|
|
248
|
+
return defaultMemoryStorage;
|
|
249
|
+
}
|
|
250
|
+
function resolveOptions(options) {
|
|
251
|
+
const config = getConfig();
|
|
252
|
+
return {
|
|
253
|
+
storage: options?.storage ?? config.storage ?? getDefaultStorage(),
|
|
254
|
+
ttl: options?.ttl ?? config.ttl ?? "24h",
|
|
255
|
+
fingerprint: options?.fingerprint ?? config.fingerprint ?? true,
|
|
256
|
+
debug: options?.debug ?? config.debug ?? false,
|
|
257
|
+
onHit: options?.onHit ?? config.onHit,
|
|
258
|
+
onMiss: options?.onMiss ?? config.onMiss,
|
|
259
|
+
onConflict: options?.onConflict ?? config.onConflict,
|
|
260
|
+
onError: options?.onError ?? config.onError
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/oncely.ts
|
|
265
|
+
var Oncely = class {
|
|
266
|
+
storage;
|
|
267
|
+
ttl;
|
|
268
|
+
debug;
|
|
269
|
+
onHit;
|
|
270
|
+
onMiss;
|
|
271
|
+
onConflict;
|
|
272
|
+
onError;
|
|
273
|
+
constructor(options) {
|
|
274
|
+
this.storage = options.storage;
|
|
275
|
+
this.ttl = parseTtl(options.ttl ?? "24h");
|
|
276
|
+
this.debug = options.debug ?? false;
|
|
277
|
+
this.onHit = options.onHit;
|
|
278
|
+
this.onMiss = options.onMiss;
|
|
279
|
+
this.onConflict = options.onConflict;
|
|
280
|
+
this.onError = options.onError;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Run an operation with idempotency protection.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* ```typescript
|
|
287
|
+
* const result = await idempotency.run({
|
|
288
|
+
* key: 'order-123',
|
|
289
|
+
* handler: async () => {
|
|
290
|
+
* const order = await createOrder(data);
|
|
291
|
+
* return order;
|
|
292
|
+
* },
|
|
293
|
+
* });
|
|
294
|
+
*
|
|
295
|
+
* if (result.cached) {
|
|
296
|
+
* console.log('Returned cached response');
|
|
297
|
+
* }
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
async run(options) {
|
|
301
|
+
const { key, hash = null, handler } = options;
|
|
302
|
+
if (!key) {
|
|
303
|
+
throw new MissingKeyError();
|
|
304
|
+
}
|
|
305
|
+
this.log(`Acquiring lock for key: ${key}`);
|
|
306
|
+
let acquireResult;
|
|
307
|
+
try {
|
|
308
|
+
acquireResult = await this.storage.acquire(key, hash, this.ttl);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
throw new StorageError("Failed to acquire lock", err);
|
|
311
|
+
}
|
|
312
|
+
switch (acquireResult.status) {
|
|
313
|
+
case "hit": {
|
|
314
|
+
this.log(`Cache hit for key: ${key}`);
|
|
315
|
+
this.onHit?.(key, acquireResult.response);
|
|
316
|
+
return {
|
|
317
|
+
data: acquireResult.response.data,
|
|
318
|
+
cached: true,
|
|
319
|
+
status: "hit",
|
|
320
|
+
createdAt: acquireResult.response.createdAt
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
case "conflict": {
|
|
324
|
+
this.log(`Conflict for key: ${key}`);
|
|
325
|
+
const error = new ConflictError(acquireResult.startedAt);
|
|
326
|
+
this.onConflict?.(key);
|
|
327
|
+
this.onError?.(key, error);
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
case "mismatch": {
|
|
331
|
+
this.log(`Hash mismatch for key: ${key}`);
|
|
332
|
+
const error = new MismatchError(acquireResult.existingHash, acquireResult.providedHash);
|
|
333
|
+
this.onError?.(key, error);
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
case "acquired": {
|
|
337
|
+
this.log(`Lock acquired for key: ${key}`);
|
|
338
|
+
this.onMiss?.(key);
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const data = await handler();
|
|
344
|
+
const now = Date.now();
|
|
345
|
+
const storedResponse = {
|
|
346
|
+
data,
|
|
347
|
+
createdAt: now,
|
|
348
|
+
hash
|
|
349
|
+
};
|
|
350
|
+
try {
|
|
351
|
+
await this.storage.save(key, storedResponse);
|
|
352
|
+
this.log(`Saved response for key: ${key}`);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
this.log(`Failed to save response for key: ${key} - ${err}`);
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
data,
|
|
358
|
+
cached: false,
|
|
359
|
+
status: "created",
|
|
360
|
+
createdAt: now
|
|
361
|
+
};
|
|
362
|
+
} catch (err) {
|
|
363
|
+
this.log(`Handler failed for key: ${key}, releasing lock`);
|
|
364
|
+
try {
|
|
365
|
+
await this.storage.release(key);
|
|
366
|
+
} catch (releaseErr) {
|
|
367
|
+
this.log(`Failed to release lock for key: ${key} - ${releaseErr}`);
|
|
368
|
+
}
|
|
369
|
+
throw err;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
log(message) {
|
|
373
|
+
if (this.debug) {
|
|
374
|
+
console.log(`[oncely] ${message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function createInstance(options) {
|
|
379
|
+
return new Oncely(resolveOptions(options));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// src/testing.ts
|
|
383
|
+
var MockStorage = class extends MemoryStorage {
|
|
384
|
+
/** Track all operations for assertions */
|
|
385
|
+
operations = [];
|
|
386
|
+
/** Simulated conflict keys */
|
|
387
|
+
simulatedConflicts = /* @__PURE__ */ new Set();
|
|
388
|
+
/** Simulated mismatch keys */
|
|
389
|
+
simulatedMismatches = /* @__PURE__ */ new Map();
|
|
390
|
+
async acquire(key, hash, ttl) {
|
|
391
|
+
this.operations.push({ type: "acquire", key, timestamp: Date.now() });
|
|
392
|
+
if (this.simulatedConflicts.has(key)) {
|
|
393
|
+
return { status: "conflict", startedAt: Date.now() - 1e3 };
|
|
394
|
+
}
|
|
395
|
+
const mismatch = this.simulatedMismatches.get(key);
|
|
396
|
+
if (mismatch) {
|
|
397
|
+
return { status: "mismatch", ...mismatch };
|
|
398
|
+
}
|
|
399
|
+
return super.acquire(key, hash, ttl);
|
|
400
|
+
}
|
|
401
|
+
async save(key, response) {
|
|
402
|
+
this.operations.push({ type: "save", key, timestamp: Date.now() });
|
|
403
|
+
return super.save(key, response);
|
|
404
|
+
}
|
|
405
|
+
async release(key) {
|
|
406
|
+
this.operations.push({ type: "release", key, timestamp: Date.now() });
|
|
407
|
+
return super.release(key);
|
|
408
|
+
}
|
|
409
|
+
async delete(key) {
|
|
410
|
+
this.operations.push({ type: "delete", key, timestamp: Date.now() });
|
|
411
|
+
return super.delete(key);
|
|
412
|
+
}
|
|
413
|
+
async clear() {
|
|
414
|
+
this.operations.length = 0;
|
|
415
|
+
this.simulatedConflicts.clear();
|
|
416
|
+
this.simulatedMismatches.clear();
|
|
417
|
+
return super.clear();
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get operations for a specific key.
|
|
421
|
+
*/
|
|
422
|
+
getOperationsForKey(key) {
|
|
423
|
+
return this.operations.filter((op) => op.key === key);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Assert that a key was acquired.
|
|
427
|
+
*/
|
|
428
|
+
assertAcquired(key) {
|
|
429
|
+
const acquired = this.operations.some((op) => op.type === "acquire" && op.key === key);
|
|
430
|
+
if (!acquired) {
|
|
431
|
+
throw new Error(`Expected key "${key}" to be acquired, but it was not.`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Assert that a key was saved (cached).
|
|
436
|
+
*/
|
|
437
|
+
assertCached(key) {
|
|
438
|
+
const saved = this.operations.some((op) => op.type === "save" && op.key === key);
|
|
439
|
+
if (!saved) {
|
|
440
|
+
throw new Error(`Expected key "${key}" to be cached, but it was not.`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Assert that a key was NOT saved (not cached).
|
|
445
|
+
*/
|
|
446
|
+
assertNotCached(key) {
|
|
447
|
+
const saved = this.operations.some((op) => op.type === "save" && op.key === key);
|
|
448
|
+
if (saved) {
|
|
449
|
+
throw new Error(`Expected key "${key}" to NOT be cached, but it was.`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Assert that a key was released.
|
|
454
|
+
*/
|
|
455
|
+
assertReleased(key) {
|
|
456
|
+
const released = this.operations.some((op) => op.type === "release" && op.key === key);
|
|
457
|
+
if (!released) {
|
|
458
|
+
throw new Error(`Expected key "${key}" to be released, but it was not.`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Simulate a conflict error for a specific key.
|
|
463
|
+
* The next acquire for this key will return conflict status.
|
|
464
|
+
*/
|
|
465
|
+
simulateConflict(key) {
|
|
466
|
+
this.simulatedConflicts.add(key);
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Simulate a mismatch error for a specific key.
|
|
470
|
+
* The next acquire for this key will return mismatch status.
|
|
471
|
+
*/
|
|
472
|
+
simulateMismatch(key, existingHash = "existing-hash", providedHash = "provided-hash") {
|
|
473
|
+
this.simulatedMismatches.set(key, { existingHash, providedHash });
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Clear all simulations.
|
|
477
|
+
*/
|
|
478
|
+
clearSimulations() {
|
|
479
|
+
this.simulatedConflicts.clear();
|
|
480
|
+
this.simulatedMismatches.clear();
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
function createTestInstance(options) {
|
|
484
|
+
return createInstance({
|
|
485
|
+
storage: options?.storage ?? new MockStorage(),
|
|
486
|
+
ttl: options?.ttl ?? "1h",
|
|
487
|
+
debug: options?.debug ?? false,
|
|
488
|
+
...options
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
var createTestOncely = createTestInstance;
|
|
492
|
+
function setupTest(config) {
|
|
493
|
+
const storage = new MockStorage();
|
|
494
|
+
const instance = createInstance({ storage, ...config });
|
|
495
|
+
return {
|
|
496
|
+
storage,
|
|
497
|
+
instance,
|
|
498
|
+
reset: () => {
|
|
499
|
+
storage.clear();
|
|
500
|
+
resetConfig();
|
|
501
|
+
if (config) {
|
|
502
|
+
configure(config);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
exports.ConflictError = ConflictError;
|
|
509
|
+
exports.IdempotencyError = IdempotencyError;
|
|
510
|
+
exports.MismatchError = MismatchError;
|
|
511
|
+
exports.MissingKeyError = MissingKeyError;
|
|
512
|
+
exports.MockStorage = MockStorage;
|
|
513
|
+
exports.StorageError = StorageError;
|
|
514
|
+
exports.createTestInstance = createTestInstance;
|
|
515
|
+
exports.createTestOncely = createTestOncely;
|
|
516
|
+
exports.setupTest = setupTest;
|
|
517
|
+
//# sourceMappingURL=testing.cjs.map
|
|
518
|
+
//# sourceMappingURL=testing.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/memory.ts","../src/constants.ts","../src/errors.ts","../src/utils.ts","../src/config.ts","../src/oncely.ts","../src/testing.ts"],"names":[],"mappings":"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","file":"testing.cjs","sourcesContent":["import type { StorageAdapter, AcquireResult, StoredResponse } from './types.js';\n\ninterface MemoryRecord {\n status: 'in_progress' | 'completed';\n hash: string | null;\n response?: StoredResponse;\n startedAt: number;\n expiresAt: number;\n}\n\n/**\n * In-memory storage adapter.\n * Suitable for development, testing, and single-instance deployments.\n *\n * **Warning:** Data is lost on restart and not shared across instances.\n * Use Redis for production multi-instance deployments.\n */\nexport class MemoryStorage implements StorageAdapter {\n private store = new Map<string, MemoryRecord>();\n private cleanupInterval: ReturnType<typeof setInterval> | null = null;\n\n constructor() {\n // Cleanup expired entries every 60 seconds\n this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);\n // Don't prevent Node from exiting\n if (this.cleanupInterval.unref) {\n this.cleanupInterval.unref();\n }\n }\n\n async acquire(key: string, hash: string | null, ttl: number): Promise<AcquireResult> {\n const now = Date.now();\n let existing = this.store.get(key);\n\n if (existing && existing.expiresAt <= now) {\n this.store.delete(key);\n existing = undefined;\n }\n\n // Check if key exists and hasn't expired\n if (existing) {\n const existingHash = existing.hash;\n // Check for hash mismatch\n if (hash && existingHash && existingHash !== hash) {\n return {\n status: 'mismatch',\n existingHash,\n providedHash: hash,\n };\n }\n\n // If completed, return cached response\n if (existing.status === 'completed') {\n const response = existing.response;\n if (response) {\n return { status: 'hit', response };\n }\n }\n\n // If in progress, return conflict\n if (existing.status === 'in_progress') {\n return { status: 'conflict', startedAt: existing.startedAt };\n }\n }\n\n // Acquire lock\n this.store.set(key, {\n status: 'in_progress',\n hash,\n startedAt: now,\n expiresAt: now + ttl,\n });\n\n return { status: 'acquired' };\n }\n\n async save(key: string, response: StoredResponse): Promise<void> {\n const existing = this.store.get(key);\n if (existing) {\n existing.status = 'completed';\n existing.response = response;\n }\n }\n\n async release(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async delete(key: string): Promise<void> {\n this.store.delete(key);\n }\n\n async clear(): Promise<void> {\n this.store.clear();\n }\n\n /**\n * Stop the cleanup interval.\n * Call this when shutting down to prevent memory leaks in tests.\n */\n destroy(): void {\n if (this.cleanupInterval) {\n clearInterval(this.cleanupInterval);\n this.cleanupInterval = null;\n }\n }\n\n private cleanup(): void {\n const now = Date.now();\n for (const [key, record] of this.store) {\n if (record.expiresAt <= now) {\n this.store.delete(key);\n }\n }\n }\n}\n\n/**\n * Pre-configured memory storage instance.\n * Use this for quick setup in development.\n *\n * @example\n * ```typescript\n * import { oncely, memory } from 'oncely';\n *\n * const idempotency = oncely({ storage: memory });\n * ```\n */\nexport const memory: StorageAdapter = new MemoryStorage();\n","/**\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/** Base URL for error documentation */\nexport const DOCS_BASE_URL = 'https://oncely.dev/errors';\n","import { DOCS_BASE_URL } 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 /** Additional properties */\n [key: string]: unknown;\n}\n\n/**\n * Base class for all oncely errors.\n * Provides RFC 7807 Problem Details format for HTTP responses.\n */\nexport class IdempotencyError extends Error {\n /** HTTP status code for this error */\n readonly statusCode: number;\n /** Error type identifier (URL) */\n readonly type: string;\n /** Short title for the error */\n readonly title: string;\n\n constructor(message: string, statusCode: number, type: string, title: string) {\n super(message);\n this.name = 'IdempotencyError';\n this.statusCode = statusCode;\n this.type = type;\n this.title = title;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n\n /**\n * Convert to RFC 7807 Problem Details format.\n */\n toProblemDetails(): ProblemDetails {\n return {\n type: this.type,\n title: this.title,\n status: this.statusCode,\n detail: this.message,\n };\n }\n\n /**\n * Convert to JSON (RFC 7807 format).\n */\n toJSON(): ProblemDetails {\n return this.toProblemDetails();\n }\n}\n\n/**\n * Thrown when an idempotency key is required but not provided.\n * HTTP 400 Bad Request\n */\nexport class MissingKeyError extends IdempotencyError {\n constructor() {\n super(\n 'This operation requires an Idempotency-Key header.',\n 400,\n `${DOCS_BASE_URL}/missing-key`,\n 'Idempotency-Key is missing'\n );\n this.name = 'MissingKeyError';\n }\n}\n\n/**\n * Thrown when a request with the same key is already being processed.\n * HTTP 409 Conflict\n */\nexport class ConflictError extends IdempotencyError {\n /** When the in-progress request started */\n readonly startedAt: number;\n /** Suggested retry delay in seconds */\n readonly retryAfter: number;\n\n constructor(startedAt: number) {\n const ageSeconds = Math.round((Date.now() - startedAt) / 1000);\n super(\n 'A request with the same Idempotency-Key is currently being processed.',\n 409,\n `${DOCS_BASE_URL}/conflict`,\n 'Request in progress'\n );\n this.name = 'ConflictError';\n this.startedAt = startedAt;\n this.retryAfter = Math.max(1, Math.min(5, ageSeconds));\n }\n\n override toProblemDetails(): ProblemDetails {\n return {\n ...super.toProblemDetails(),\n retryAfter: this.retryAfter,\n };\n }\n}\n\n/**\n * Thrown when the same idempotency key is used with a different request payload.\n * HTTP 422 Unprocessable Content\n */\nexport class MismatchError extends IdempotencyError {\n /** Hash of the original request */\n readonly existingHash: string;\n /** Hash of the current request */\n readonly providedHash: string;\n\n constructor(existingHash: string, providedHash: string) {\n super(\n 'This Idempotency-Key was used with a different request payload.',\n 422,\n `${DOCS_BASE_URL}/mismatch`,\n 'Idempotency-Key reused'\n );\n this.name = 'MismatchError';\n this.existingHash = existingHash;\n this.providedHash = providedHash;\n }\n}\n\n/**\n * Thrown when the storage adapter encounters an error.\n * HTTP 500 Internal Server Error\n */\nexport class StorageError extends IdempotencyError {\n /** The underlying error from the storage adapter */\n override readonly cause: Error;\n\n constructor(message: string, cause: Error) {\n super(`Storage error: ${message}`, 500, `${DOCS_BASE_URL}/storage-error`, 'Storage error');\n this.name = 'StorageError';\n this.cause = cause;\n }\n}\n","import { randomUUID, createHash } from 'node:crypto';\n\n/**\n * Parse a TTL string into milliseconds.\n * Supports: '30s', '5m', '24h', '7d'\n */\nexport function parseTtl(ttl: number | string): number {\n if (typeof ttl === 'number') {\n return ttl;\n }\n\n if (ttl.length < 2) {\n throw new Error(\n `Invalid TTL format: \"${ttl}\". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`\n );\n }\n\n const unit = ttl[ttl.length - 1];\n if (unit !== 's' && unit !== 'm' && unit !== 'h' && unit !== 'd') {\n throw new Error(\n `Invalid TTL format: \"${ttl}\". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`\n );\n }\n\n const valueStr = ttl.slice(0, -1);\n if (!valueStr) {\n throw new Error(\n `Invalid TTL format: \"${ttl}\". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`\n );\n }\n\n for (let i = 0; i < valueStr.length; i++) {\n const code = valueStr.charCodeAt(i);\n if (code < 48 || code > 57) {\n throw new Error(\n `Invalid TTL format: \"${ttl}\". Use a number (milliseconds) or string like '30s', '5m', '24h', '7d'.`\n );\n }\n }\n\n const value = parseInt(valueStr, 10);\n\n switch (unit) {\n case 's':\n return value * 1000;\n case 'm':\n return value * 60 * 1000;\n case 'h':\n return value * 60 * 60 * 1000;\n case 'd':\n return value * 24 * 60 * 60 * 1000;\n default:\n throw new Error(`Invalid TTL unit: \"${unit}\"`);\n }\n}\n\n/**\n * Generate a simple hash from a string using DJB2 algorithm.\n *\n * **Note:** This is a fast, non-cryptographic hash suitable for request\n * fingerprinting. It has a 32-bit output space (~4 billion unique values).\n *\n * **Collision Risk:** At ~77,000 unique payloads, there's a 50% probability\n * of collision (birthday problem). For high-volume payment systems or\n * security-sensitive applications, use {@link hashObjectSecure} instead.\n *\n * For typical API use cases with moderate volume, DJB2 provides excellent\n * performance (~2.4M ops/sec vs ~600K ops/sec for SHA-256).\n */\nexport function simpleHash(str: string): string {\n let hash = 0;\n for (let i = 0; i < str.length; i++) {\n const char = str.charCodeAt(i);\n hash = (hash << 5) - hash + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n return Math.abs(hash).toString(16).padStart(8, '0');\n}\n\n/**\n * Generate a SHA-256 hash from a string.\n *\n * Use this for security-sensitive applications where collision resistance\n * is important. Slower than {@link simpleHash} but cryptographically secure.\n */\nexport function secureHash(str: string): string {\n return createHash('sha256').update(str).digest('hex');\n}\n\n/**\n * Hash an object by JSON stringifying it.\n *\n * Uses the fast DJB2 algorithm (~2.4M ops/sec). For high-volume payment\n * systems or security-sensitive applications where collision resistance\n * is critical, use {@link hashObjectSecure} instead.\n *\n * **Collision Risk:** At ~77,000 unique payloads, there's a 50% probability\n * of collision. A collision could cause:\n * - False mismatch errors (if hashes differ)\n * - Or incorrectly matching different payloads (if hashes collide)\n *\n * **Note:** Object key order affects the hash. If you need order-independent\n * hashing, sort the keys before passing to this function.\n */\nexport function hashObject(obj: unknown): string {\n const str = JSON.stringify(obj) ?? 'undefined';\n return simpleHash(str);\n}\n\n/**\n * Hash an object using SHA-256.\n *\n * Use this for security-sensitive applications where collision resistance\n * is important. Slower than {@link hashObject} but cryptographically secure.\n */\nexport function hashObjectSecure(obj: unknown): string {\n const str = JSON.stringify(obj) ?? 'undefined';\n return secureHash(str);\n}\n\n/**\n * Generate a cryptographically secure unique key (UUID v4).\n *\n * Uses Node.js crypto.randomUUID() for secure random generation.\n */\nexport function generateKey(): string {\n return randomUUID();\n}\n\n/**\n * Compose a deterministic key from multiple parts.\n */\nexport function composeKey(...parts: (string | number)[]): string {\n return parts.join(':');\n}\n","import type { StorageAdapter, OncelyOptions, OncelyConfig } from './types.js';\nimport { MemoryStorage } from './memory.js';\n\n// Re-export GlobalConfig as alias for backwards compatibility\nexport type GlobalConfig = OncelyConfig;\n\n// Global configuration store\nlet globalConfig: OncelyConfig = {};\n\n// Lazy-initialized default memory storage\nlet defaultMemoryStorage: MemoryStorage | null = null;\n\n/**\n * Configure global defaults for oncely.\n * Call this once at application startup to set defaults for all oncely operations.\n *\n * @example\n * ```typescript\n * import { oncely } from '@oncely/core';\n * import { redis } from '@oncely/redis';\n *\n * // Set up once at app startup\n * oncely.configure({\n * storage: redis(),\n * ttl: '1h',\n * onHit: (key) => console.log(`Cache hit: ${key}`),\n * });\n * ```\n */\nexport function configure(config: OncelyConfig): void {\n globalConfig = { ...config };\n}\n\n/**\n * Get the current global configuration.\n */\nexport function getConfig(): OncelyConfig {\n return globalConfig;\n}\n\n/**\n * Reset global configuration to defaults.\n * Useful for testing.\n */\nexport function resetConfig(): void {\n globalConfig = {};\n defaultMemoryStorage = null;\n}\n\n/**\n * Get the default storage adapter.\n * Returns the configured global storage, or a shared memory instance.\n */\nexport function getDefaultStorage(): StorageAdapter {\n if (globalConfig.storage) {\n return globalConfig.storage;\n }\n // Lazy-init shared memory storage\n if (!defaultMemoryStorage) {\n defaultMemoryStorage = new MemoryStorage();\n }\n return defaultMemoryStorage;\n}\n\n/**\n * Resolve oncely options by merging with global config.\n */\nexport function resolveOptions(options?: Partial<OncelyOptions>): OncelyOptions {\n const config = getConfig();\n return {\n storage: options?.storage ?? config.storage ?? getDefaultStorage(),\n ttl: options?.ttl ?? config.ttl ?? '24h',\n fingerprint: options?.fingerprint ?? config.fingerprint ?? true,\n debug: options?.debug ?? config.debug ?? false,\n onHit: options?.onHit ?? config.onHit,\n onMiss: options?.onMiss ?? config.onMiss,\n onConflict: options?.onConflict ?? config.onConflict,\n onError: options?.onError ?? config.onError,\n };\n}\n","import type {\n StorageAdapter,\n OncelyOptions,\n RunOptions,\n RunResult,\n StoredResponse,\n OnHitCallback,\n OnMissCallback,\n OnConflictCallback,\n OnErrorCallback,\n} from './types.js';\nimport { MissingKeyError, ConflictError, MismatchError, StorageError } from './errors.js';\nimport { parseTtl } from './utils.js';\nimport { resolveOptions } from './config.js';\n\n/**\n * Oncely idempotency service.\n * Ensures operations are executed exactly once per idempotency key.\n */\nexport class Oncely {\n private readonly storage: StorageAdapter;\n private readonly ttl: number;\n private readonly debug: boolean;\n private readonly onHit?: OnHitCallback;\n private readonly onMiss?: OnMissCallback;\n private readonly onConflict?: OnConflictCallback;\n private readonly onError?: OnErrorCallback;\n\n constructor(options: OncelyOptions) {\n this.storage = options.storage;\n this.ttl = parseTtl(options.ttl ?? '24h');\n this.debug = options.debug ?? false;\n this.onHit = options.onHit;\n this.onMiss = options.onMiss;\n this.onConflict = options.onConflict;\n this.onError = options.onError;\n }\n\n /**\n * Run an operation with idempotency protection.\n *\n * @example\n * ```typescript\n * const result = await idempotency.run({\n * key: 'order-123',\n * handler: async () => {\n * const order = await createOrder(data);\n * return order;\n * },\n * });\n *\n * if (result.cached) {\n * console.log('Returned cached response');\n * }\n * ```\n */\n async run<T>(options: RunOptions<T>): Promise<RunResult<T>> {\n const { key, hash = null, handler } = options;\n\n // Validate key\n if (!key) {\n throw new MissingKeyError();\n }\n\n this.log(`Acquiring lock for key: ${key}`);\n\n // Try to acquire lock\n let acquireResult;\n try {\n acquireResult = await this.storage.acquire(key, hash, this.ttl);\n } catch (err) {\n throw new StorageError('Failed to acquire lock', err as Error);\n }\n\n // Handle different acquire results\n switch (acquireResult.status) {\n case 'hit': {\n this.log(`Cache hit for key: ${key}`);\n this.onHit?.(key, acquireResult.response);\n return {\n data: acquireResult.response.data as T,\n cached: true,\n status: 'hit',\n createdAt: acquireResult.response.createdAt,\n };\n }\n\n case 'conflict': {\n this.log(`Conflict for key: ${key}`);\n const error = new ConflictError(acquireResult.startedAt);\n this.onConflict?.(key);\n this.onError?.(key, error);\n throw error;\n }\n\n case 'mismatch': {\n this.log(`Hash mismatch for key: ${key}`);\n const error = new MismatchError(acquireResult.existingHash, acquireResult.providedHash);\n this.onError?.(key, error);\n throw error;\n }\n\n case 'acquired': {\n this.log(`Lock acquired for key: ${key}`);\n this.onMiss?.(key);\n break;\n }\n }\n\n // Execute handler\n try {\n const data = await handler();\n const now = Date.now();\n\n // Save response\n const storedResponse: StoredResponse = {\n data,\n createdAt: now,\n hash,\n };\n\n try {\n await this.storage.save(key, storedResponse);\n this.log(`Saved response for key: ${key}`);\n } catch (err) {\n // Log but don't fail - the operation succeeded\n this.log(`Failed to save response for key: ${key} - ${err}`);\n }\n\n return {\n data,\n cached: false,\n status: 'created',\n createdAt: now,\n };\n } catch (err) {\n // Release lock on failure to allow retry\n this.log(`Handler failed for key: ${key}, releasing lock`);\n try {\n await this.storage.release(key);\n } catch (releaseErr) {\n this.log(`Failed to release lock for key: ${key} - ${releaseErr}`);\n }\n throw err;\n }\n }\n\n private log(message: string): void {\n if (this.debug) {\n console.log(`[oncely] ${message}`);\n }\n }\n}\n\n/**\n * Create an oncely idempotency instance.\n *\n * Options are optional - will use global config or sensible defaults.\n *\n * @example\n * ```typescript\n * import { createInstance } from '@oncely/core';\n *\n * // Zero-config (uses memory storage)\n * const idempotency = createInstance();\n *\n * // With explicit storage\n * const idempotency = createInstance({ storage: myRedis });\n *\n * const result = await idempotency.run({\n * key: 'order-123',\n * handler: async () => createOrder(data),\n * });\n * ```\n */\nexport function createInstance(options?: Partial<OncelyOptions>): Oncely {\n return new Oncely(resolveOptions(options));\n}\n","import { MemoryStorage } from './memory.js';\nimport { createInstance } from './oncely.js';\nimport { resetConfig, configure } from './config.js';\nimport type { OncelyOptions, OncelyConfig, StoredResponse, AcquireResult } from './types.js';\n\n/**\n * Mock storage for testing.\n * Same as MemoryStorage but with additional test utilities.\n */\nexport class MockStorage extends MemoryStorage {\n /** Track all operations for assertions */\n readonly operations: Array<{ type: string; key: string; timestamp: number }> = [];\n\n /** Simulated conflict keys */\n private simulatedConflicts = new Set<string>();\n\n /** Simulated mismatch keys */\n private simulatedMismatches = new Map<string, { existingHash: string; providedHash: string }>();\n\n override async acquire(key: string, hash: string | null, ttl: number): Promise<AcquireResult> {\n this.operations.push({ type: 'acquire', key, timestamp: Date.now() });\n\n // Check for simulated conflict\n if (this.simulatedConflicts.has(key)) {\n return { status: 'conflict', startedAt: Date.now() - 1000 };\n }\n\n // Check for simulated mismatch\n const mismatch = this.simulatedMismatches.get(key);\n if (mismatch) {\n return { status: 'mismatch', ...mismatch };\n }\n\n return super.acquire(key, hash, ttl);\n }\n\n override async save(key: string, response: StoredResponse): Promise<void> {\n this.operations.push({ type: 'save', key, timestamp: Date.now() });\n return super.save(key, response);\n }\n\n override async release(key: string): Promise<void> {\n this.operations.push({ type: 'release', key, timestamp: Date.now() });\n return super.release(key);\n }\n\n override async delete(key: string): Promise<void> {\n this.operations.push({ type: 'delete', key, timestamp: Date.now() });\n return super.delete(key);\n }\n\n override async clear(): Promise<void> {\n this.operations.length = 0;\n this.simulatedConflicts.clear();\n this.simulatedMismatches.clear();\n return super.clear();\n }\n\n /**\n * Get operations for a specific key.\n */\n getOperationsForKey(key: string) {\n return this.operations.filter((op) => op.key === key);\n }\n\n /**\n * Assert that a key was acquired.\n */\n assertAcquired(key: string): void {\n const acquired = this.operations.some((op) => op.type === 'acquire' && op.key === key);\n if (!acquired) {\n throw new Error(`Expected key \"${key}\" to be acquired, but it was not.`);\n }\n }\n\n /**\n * Assert that a key was saved (cached).\n */\n assertCached(key: string): void {\n const saved = this.operations.some((op) => op.type === 'save' && op.key === key);\n if (!saved) {\n throw new Error(`Expected key \"${key}\" to be cached, but it was not.`);\n }\n }\n\n /**\n * Assert that a key was NOT saved (not cached).\n */\n assertNotCached(key: string): void {\n const saved = this.operations.some((op) => op.type === 'save' && op.key === key);\n if (saved) {\n throw new Error(`Expected key \"${key}\" to NOT be cached, but it was.`);\n }\n }\n\n /**\n * Assert that a key was released.\n */\n assertReleased(key: string): void {\n const released = this.operations.some((op) => op.type === 'release' && op.key === key);\n if (!released) {\n throw new Error(`Expected key \"${key}\" to be released, but it was not.`);\n }\n }\n\n /**\n * Simulate a conflict error for a specific key.\n * The next acquire for this key will return conflict status.\n */\n simulateConflict(key: string): void {\n this.simulatedConflicts.add(key);\n }\n\n /**\n * Simulate a mismatch error for a specific key.\n * The next acquire for this key will return mismatch status.\n */\n simulateMismatch(\n key: string,\n existingHash = 'existing-hash',\n providedHash = 'provided-hash'\n ): void {\n this.simulatedMismatches.set(key, { existingHash, providedHash });\n }\n\n /**\n * Clear all simulations.\n */\n clearSimulations(): void {\n this.simulatedConflicts.clear();\n this.simulatedMismatches.clear();\n }\n}\n\n/**\n * Create an oncely instance configured for testing.\n *\n * @example\n * ```typescript\n * import { createTestInstance, MockStorage } from '@oncely/core/testing';\n *\n * describe('my api', () => {\n * const storage = new MockStorage();\n * const idempotency = createTestInstance({ storage });\n *\n * beforeEach(() => storage.clear());\n *\n * it('handles duplicate requests', async () => {\n * const handler = vi.fn().mockResolvedValue({ id: 1 });\n *\n * await idempotency.run({ key: 'test', handler });\n * await idempotency.run({ key: 'test', handler });\n *\n * expect(handler).toHaveBeenCalledTimes(1);\n * storage.assertCached('test');\n * });\n * });\n * ```\n */\nexport function createTestInstance(options?: Partial<OncelyOptions>) {\n return createInstance({\n storage: options?.storage ?? new MockStorage(),\n ttl: options?.ttl ?? '1h',\n debug: options?.debug ?? false,\n ...options,\n });\n}\n\n/**\n * @deprecated Use createTestInstance instead.\n */\nexport const createTestOncely = createTestInstance;\n\n/**\n * Set up oncely for testing with auto-reset between tests.\n * Returns utilities for testing assertions.\n *\n * @example\n * ```typescript\n * import { setupTest } from '@oncely/core/testing';\n *\n * describe('my api', () => {\n * const { storage, instance, reset } = setupTest();\n *\n * beforeEach(() => reset());\n *\n * it('caches responses', async () => {\n * await instance.run({ key: 'test', handler: () => ({ ok: true }) });\n * storage.assertCached('test');\n * });\n * });\n * ```\n */\nexport function setupTest(config?: OncelyConfig) {\n const storage = new MockStorage();\n const instance = createInstance({ storage, ...config });\n\n return {\n storage,\n instance,\n reset: () => {\n storage.clear();\n resetConfig();\n if (config) {\n configure(config);\n }\n },\n };\n}\n\n// Re-export useful types\nexport type {\n StorageAdapter,\n StoredResponse,\n OncelyOptions,\n OncelyConfig,\n RunOptions,\n RunResult,\n AcquireResult,\n} from './types.js';\n\n// Re-export errors for test assertions\nexport {\n IdempotencyError,\n MissingKeyError,\n ConflictError,\n MismatchError,\n StorageError,\n} from './errors.js';\n"]}
|