@oncely/core 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -27,6 +27,12 @@ interface StorageAdapter {
27
27
  * Useful for testing.
28
28
  */
29
29
  clear(): Promise<void>;
30
+ /**
31
+ * Maximum payload size in bytes that this adapter can store.
32
+ * If undefined, no limit is enforced.
33
+ * Responses exceeding this limit will skip caching (warn + continue).
34
+ */
35
+ readonly maxPayloadSize?: number;
30
36
  }
31
37
  /**
32
38
  * Result of attempting to acquire a lock.
@@ -71,6 +77,23 @@ type OnConflictCallback = (key: string) => void;
71
77
  * Callback fired when an error occurs.
72
78
  */
73
79
  type OnErrorCallback = (key: string, error: Error) => void;
80
+ /**
81
+ * Reason why caching was skipped.
82
+ */
83
+ type SkipReason = 'payload_too_large';
84
+ /**
85
+ * Details about why caching was skipped.
86
+ */
87
+ interface SkipDetails {
88
+ /** Size of the payload in bytes */
89
+ payloadSize: number;
90
+ /** Maximum allowed size in bytes */
91
+ maxSize: number;
92
+ }
93
+ /**
94
+ * Callback fired when caching is skipped (operation succeeds but result not cached).
95
+ */
96
+ type OnSkipCallback = (key: string, reason: SkipReason, details: SkipDetails) => void;
74
97
  /**
75
98
  * Global configuration options for oncely.
76
99
  */
@@ -110,6 +133,11 @@ interface OncelyConfig {
110
133
  * Callback fired when an error occurs.
111
134
  */
112
135
  onError?: OnErrorCallback;
136
+ /**
137
+ * Callback fired when caching is skipped (e.g., payload too large).
138
+ * The operation still succeeds, but the result is not cached.
139
+ */
140
+ onSkip?: OnSkipCallback;
113
141
  }
114
142
  /**
115
143
  * Options for creating an oncely instance.
@@ -159,6 +187,7 @@ declare class Oncely {
159
187
  private readonly onMiss?;
160
188
  private readonly onConflict?;
161
189
  private readonly onError?;
190
+ private readonly onSkip?;
162
191
  constructor(options: OncelyOptions);
163
192
  /**
164
193
  * Run an operation with idempotency protection.
@@ -212,6 +241,11 @@ declare function createInstance(options?: Partial<OncelyOptions>): Oncely;
212
241
  * Use Redis for production multi-instance deployments.
213
242
  */
214
243
  declare class MemoryStorage implements StorageAdapter {
244
+ /**
245
+ * Maximum payload size in bytes (default: 5MB).
246
+ * Prevents unbounded memory growth from large responses.
247
+ */
248
+ readonly maxPayloadSize: number;
215
249
  private store;
216
250
  private cleanupInterval;
217
251
  constructor();
@@ -307,4 +341,4 @@ declare class StorageError extends IdempotencyError {
307
341
  constructor(message: string, cause: Error);
308
342
  }
309
343
 
310
- export { type AcquireResult as A, ConflictError as C, IdempotencyError as I, MemoryStorage as M, type OncelyConfig as O, type ProblemDetails as P, type RunOptions as R, type StorageAdapter as S, Oncely as a, MissingKeyError as b, createInstance as c, MismatchError as d, StorageError as e, type StoredResponse as f, type OncelyOptions as g, type RunResult as h, type OnHitCallback as i, type OnMissCallback as j, type OnConflictCallback as k, type OnErrorCallback as l };
344
+ export { type AcquireResult as A, ConflictError as C, IdempotencyError as I, MemoryStorage as M, type OncelyConfig as O, type ProblemDetails as P, type RunOptions as R, type StorageAdapter as S, MismatchError as a, MissingKeyError as b, createInstance as c, type OnConflictCallback as d, type OnErrorCallback as e, type OnHitCallback as f, type OnMissCallback as g, type OnSkipCallback as h, Oncely as i, type OncelyOptions as j, type RunResult as k, type SkipDetails as l, type SkipReason as m, StorageError as n, type StoredResponse as o };
@@ -27,6 +27,12 @@ interface StorageAdapter {
27
27
  * Useful for testing.
28
28
  */
29
29
  clear(): Promise<void>;
30
+ /**
31
+ * Maximum payload size in bytes that this adapter can store.
32
+ * If undefined, no limit is enforced.
33
+ * Responses exceeding this limit will skip caching (warn + continue).
34
+ */
35
+ readonly maxPayloadSize?: number;
30
36
  }
31
37
  /**
32
38
  * Result of attempting to acquire a lock.
@@ -71,6 +77,23 @@ type OnConflictCallback = (key: string) => void;
71
77
  * Callback fired when an error occurs.
72
78
  */
73
79
  type OnErrorCallback = (key: string, error: Error) => void;
80
+ /**
81
+ * Reason why caching was skipped.
82
+ */
83
+ type SkipReason = 'payload_too_large';
84
+ /**
85
+ * Details about why caching was skipped.
86
+ */
87
+ interface SkipDetails {
88
+ /** Size of the payload in bytes */
89
+ payloadSize: number;
90
+ /** Maximum allowed size in bytes */
91
+ maxSize: number;
92
+ }
93
+ /**
94
+ * Callback fired when caching is skipped (operation succeeds but result not cached).
95
+ */
96
+ type OnSkipCallback = (key: string, reason: SkipReason, details: SkipDetails) => void;
74
97
  /**
75
98
  * Global configuration options for oncely.
76
99
  */
@@ -110,6 +133,11 @@ interface OncelyConfig {
110
133
  * Callback fired when an error occurs.
111
134
  */
112
135
  onError?: OnErrorCallback;
136
+ /**
137
+ * Callback fired when caching is skipped (e.g., payload too large).
138
+ * The operation still succeeds, but the result is not cached.
139
+ */
140
+ onSkip?: OnSkipCallback;
113
141
  }
114
142
  /**
115
143
  * Options for creating an oncely instance.
@@ -159,6 +187,7 @@ declare class Oncely {
159
187
  private readonly onMiss?;
160
188
  private readonly onConflict?;
161
189
  private readonly onError?;
190
+ private readonly onSkip?;
162
191
  constructor(options: OncelyOptions);
163
192
  /**
164
193
  * Run an operation with idempotency protection.
@@ -212,6 +241,11 @@ declare function createInstance(options?: Partial<OncelyOptions>): Oncely;
212
241
  * Use Redis for production multi-instance deployments.
213
242
  */
214
243
  declare class MemoryStorage implements StorageAdapter {
244
+ /**
245
+ * Maximum payload size in bytes (default: 5MB).
246
+ * Prevents unbounded memory growth from large responses.
247
+ */
248
+ readonly maxPayloadSize: number;
215
249
  private store;
216
250
  private cleanupInterval;
217
251
  constructor();
@@ -307,4 +341,4 @@ declare class StorageError extends IdempotencyError {
307
341
  constructor(message: string, cause: Error);
308
342
  }
309
343
 
310
- export { type AcquireResult as A, ConflictError as C, IdempotencyError as I, MemoryStorage as M, type OncelyConfig as O, type ProblemDetails as P, type RunOptions as R, type StorageAdapter as S, Oncely as a, MissingKeyError as b, createInstance as c, MismatchError as d, StorageError as e, type StoredResponse as f, type OncelyOptions as g, type RunResult as h, type OnHitCallback as i, type OnMissCallback as j, type OnConflictCallback as k, type OnErrorCallback as l };
344
+ export { type AcquireResult as A, ConflictError as C, IdempotencyError as I, MemoryStorage as M, type OncelyConfig as O, type ProblemDetails as P, type RunOptions as R, type StorageAdapter as S, MismatchError as a, MissingKeyError as b, createInstance as c, type OnConflictCallback as d, type OnErrorCallback as e, type OnHitCallback as f, type OnMissCallback as g, type OnSkipCallback as h, Oncely as i, type OncelyOptions as j, type RunResult as k, type SkipDetails as l, type SkipReason as m, StorageError as n, type StoredResponse as o };
package/dist/index.cjs CHANGED
@@ -174,6 +174,11 @@ function composeKey(...parts) {
174
174
 
175
175
  // src/memory.ts
176
176
  var MemoryStorage = class {
177
+ /**
178
+ * Maximum payload size in bytes (default: 5MB).
179
+ * Prevents unbounded memory growth from large responses.
180
+ */
181
+ maxPayloadSize = 5 * 1024 * 1024;
177
182
  store = /* @__PURE__ */ new Map();
178
183
  cleanupInterval = null;
179
184
  constructor() {
@@ -285,11 +290,19 @@ function resolveOptions(options) {
285
290
  onHit: options?.onHit ?? config.onHit,
286
291
  onMiss: options?.onMiss ?? config.onMiss,
287
292
  onConflict: options?.onConflict ?? config.onConflict,
288
- onError: options?.onError ?? config.onError
293
+ onError: options?.onError ?? config.onError,
294
+ onSkip: options?.onSkip ?? config.onSkip
289
295
  };
290
296
  }
291
297
 
292
298
  // src/oncely.ts
299
+ function estimatePayloadSize(value) {
300
+ try {
301
+ return new TextEncoder().encode(JSON.stringify(value)).length;
302
+ } catch {
303
+ return 0;
304
+ }
305
+ }
293
306
  var Oncely = class {
294
307
  storage;
295
308
  ttl;
@@ -298,6 +311,7 @@ var Oncely = class {
298
311
  onMiss;
299
312
  onConflict;
300
313
  onError;
314
+ onSkip;
301
315
  constructor(options) {
302
316
  this.storage = options.storage;
303
317
  this.ttl = parseTtl(options.ttl ?? "24h");
@@ -306,6 +320,7 @@ var Oncely = class {
306
320
  this.onMiss = options.onMiss;
307
321
  this.onConflict = options.onConflict;
308
322
  this.onError = options.onError;
323
+ this.onSkip = options.onSkip;
309
324
  }
310
325
  /**
311
326
  * Run an operation with idempotency protection.
@@ -375,6 +390,27 @@ var Oncely = class {
375
390
  createdAt: now,
376
391
  hash
377
392
  };
393
+ const maxSize = this.storage.maxPayloadSize;
394
+ if (maxSize !== void 0 && maxSize > 0) {
395
+ const payloadSize = estimatePayloadSize(storedResponse);
396
+ if (payloadSize > maxSize) {
397
+ this.log(
398
+ `Payload too large for key: ${key} (${payloadSize} bytes > ${maxSize} bytes limit). Skipping cache.`
399
+ );
400
+ this.onSkip?.(key, "payload_too_large", { payloadSize, maxSize });
401
+ try {
402
+ await this.storage.release(key);
403
+ } catch (releaseErr) {
404
+ this.log(`Failed to release lock for key: ${key} - ${releaseErr}`);
405
+ }
406
+ return {
407
+ data,
408
+ cached: false,
409
+ status: "created",
410
+ createdAt: now
411
+ };
412
+ }
413
+ }
378
414
  try {
379
415
  await this.storage.save(key, storedResponse);
380
416
  this.log(`Saved response for key: ${key}`);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/constants.ts","../src/errors.ts","../src/utils.ts","../src/memory.ts","../src/config.ts","../src/oncely.ts","../src/index.ts"],"names":["createHash","randomUUID","memory"],"mappings":";;;;;AAMO,IAAM,MAAA,GAAS;AAGf,IAAM,aAAA,GAAgB;AAGtB,IAAM,aAAA,GAAgB,2BAAA;;;ACatB,IAAM,gBAAA,GAAN,cAA+B,KAAA,CAAM;AAAA;AAAA,EAEjC,UAAA;AAAA;AAAA,EAEA,IAAA;AAAA;AAAA,EAEA,KAAA;AAAA,EAET,WAAA,CAAY,OAAA,EAAiB,UAAA,EAAoB,IAAA,EAAc,KAAA,EAAe;AAC5E,IAAA,KAAA,CAAM,OAAO,CAAA;AACb,IAAA,IAAA,CAAK,IAAA,GAAO,kBAAA;AACZ,IAAA,IAAA,CAAK,UAAA,GAAa,UAAA;AAClB,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AACZ,IAAA,IAAA,CAAK,KAAA,GAAQ,KAAA;AACb,IAAA,MAAA,CAAO,cAAA,CAAe,IAAA,EAAM,GAAA,CAAA,MAAA,CAAW,SAAS,CAAA;AAAA,EAClD;AAAA;AAAA;AAAA;AAAA,EAKA,gBAAA,GAAmC;AACjC,IAAA,OAAO;AAAA,MACL,MAAM,IAAA,CAAK,IAAA;AAAA,MACX,OAAO,IAAA,CAAK,KAAA;AAAA,MACZ,QAAQ,IAAA,CAAK,UAAA;AAAA,MACb,QAAQ,IAAA,CAAK;AAAA,KACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAA,GAAyB;AACvB,IAAA,OAAO,KAAK,gBAAA,EAAiB;AAAA,EAC/B;AACF;AAMO,IAAM,eAAA,GAAN,cAA8B,gBAAA,CAAiB;AAAA,EACpD,WAAA,GAAc;AACZ,IAAA,KAAA;AAAA,MACE,oDAAA;AAAA,MACA,GAAA;AAAA,MACA,GAAG,aAAa,CAAA,YAAA,CAAA;AAAA,MAChB;AAAA,KACF;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,iBAAA;AAAA,EACd;AACF;AAMO,IAAM,aAAA,GAAN,cAA4B,gBAAA,CAAiB;AAAA;AAAA,EAEzC,SAAA;AAAA;AAAA,EAEA,UAAA;AAAA,EAET,YAAY,SAAA,EAAmB;AAC7B,IAAA,MAAM,aAAa,IAAA,CAAK,KAAA,CAAA,CAAO,KAAK,GAAA,EAAI,GAAI,aAAa,GAAI,CAAA;AAC7D,IAAA,KAAA;AAAA,MACE,uEAAA;AAAA,MACA,GAAA;AAAA,MACA,GAAG,aAAa,CAAA,SAAA,CAAA;AAAA,MAChB;AAAA,KACF;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,eAAA;AACZ,IAAA,IAAA,CAAK,SAAA,GAAY,SAAA;AACjB,IAAA,IAAA,CAAK,UAAA,GAAa,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,GAAA,CAAI,CAAA,EAAG,UAAU,CAAC,CAAA;AAAA,EACvD;AAAA,EAES,gBAAA,GAAmC;AAC1C,IAAA,OAAO;AAAA,MACL,GAAG,MAAM,gBAAA,EAAiB;AAAA,MAC1B,YAAY,IAAA,CAAK;AAAA,KACnB;AAAA,EACF;AACF;AAMO,IAAM,aAAA,GAAN,cAA4B,gBAAA,CAAiB;AAAA;AAAA,EAEzC,YAAA;AAAA;AAAA,EAEA,YAAA;AAAA,EAET,WAAA,CAAY,cAAsB,YAAA,EAAsB;AACtD,IAAA,KAAA;AAAA,MACE,iEAAA;AAAA,MACA,GAAA;AAAA,MACA,GAAG,aAAa,CAAA,SAAA,CAAA;AAAA,MAChB;AAAA,KACF;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,eAAA;AACZ,IAAA,IAAA,CAAK,YAAA,GAAe,YAAA;AACpB,IAAA,IAAA,CAAK,YAAA,GAAe,YAAA;AAAA,EACtB;AACF;AAMO,IAAM,YAAA,GAAN,cAA2B,gBAAA,CAAiB;AAAA;AAAA,EAE/B,KAAA;AAAA,EAElB,WAAA,CAAY,SAAiB,KAAA,EAAc;AACzC,IAAA,KAAA,CAAM,kBAAkB,OAAO,CAAA,CAAA,EAAI,KAAK,CAAA,EAAG,aAAa,kBAAkB,eAAe,CAAA;AACzF,IAAA,IAAA,CAAK,IAAA,GAAO,cAAA;AACZ,IAAA,IAAA,CAAK,KAAA,GAAQ,KAAA;AAAA,EACf;AACF;AC3IO,SAAS,SAAS,GAAA,EAA8B;AACrD,EAAA,IAAI,OAAO,QAAQ,QAAA,EAAU;AAC3B,IAAA,OAAO,GAAA;AAAA,EACT;AAEA,EAAA,IAAI,GAAA,CAAI,SAAS,CAAA,EAAG;AAClB,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,wBAAwB,GAAG,CAAA,uEAAA;AAAA,KAC7B;AAAA,EACF;AAEA,EAAA,MAAM,IAAA,GAAO,GAAA,CAAI,GAAA,CAAI,MAAA,GAAS,CAAC,CAAA;AAC/B,EAAA,IAAI,SAAS,GAAA,IAAO,IAAA,KAAS,OAAO,IAAA,KAAS,GAAA,IAAO,SAAS,GAAA,EAAK;AAChE,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,wBAAwB,GAAG,CAAA,uEAAA;AAAA,KAC7B;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAW,GAAA,CAAI,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AAChC,EAAA,IAAI,CAAC,QAAA,EAAU;AACb,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,wBAAwB,GAAG,CAAA,uEAAA;AAAA,KAC7B;AAAA,EACF;AAEA,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,QAAA,CAAS,QAAQ,CAAA,EAAA,EAAK;AACxC,IAAA,MAAM,IAAA,GAAO,QAAA,CAAS,UAAA,CAAW,CAAC,CAAA;AAClC,IAAA,IAAI,IAAA,GAAO,EAAA,IAAM,IAAA,GAAO,EAAA,EAAI;AAC1B,MAAA,MAAM,IAAI,KAAA;AAAA,QACR,wBAAwB,GAAG,CAAA,uEAAA;AAAA,OAC7B;AAAA,IACF;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,QAAA,EAAU,EAAE,CAAA;AAEnC,EAAA,QAAQ,IAAA;AAAM,IACZ,KAAK,GAAA;AACH,MAAA,OAAO,KAAA,GAAQ,GAAA;AAAA,IACjB,KAAK,GAAA;AACH,MAAA,OAAO,QAAQ,EAAA,GAAK,GAAA;AAAA,IACtB,KAAK,GAAA;AACH,MAAA,OAAO,KAAA,GAAQ,KAAK,EAAA,GAAK,GAAA;AAAA,IAC3B,KAAK,GAAA;AACH,MAAA,OAAO,KAAA,GAAQ,EAAA,GAAK,EAAA,GAAK,EAAA,GAAK,GAAA;AAAA,IAChC;AACE,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,IAAI,CAAA,CAAA,CAAG,CAAA;AAAA;AAEnD;AAeO,SAAS,WAAW,GAAA,EAAqB;AAC9C,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,KAAA,IAAS,CAAA,GAAI,CAAA,EAAG,CAAA,GAAI,GAAA,CAAI,QAAQ,CAAA,EAAA,EAAK;AACnC,IAAA,MAAM,IAAA,GAAO,GAAA,CAAI,UAAA,CAAW,CAAC,CAAA;AAC7B,IAAA,IAAA,GAAA,CAAQ,IAAA,IAAQ,KAAK,IAAA,GAAO,IAAA;AAC5B,IAAA,IAAA,GAAO,IAAA,GAAO,IAAA;AAAA,EAChB;AACA,EAAA,OAAO,IAAA,CAAK,IAAI,IAAI,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,QAAA,CAAS,CAAA,EAAG,GAAG,CAAA;AACpD;AAQO,SAAS,WAAW,GAAA,EAAqB;AAC9C,EAAA,OAAOA,kBAAW,QAAQ,CAAA,CAAE,OAAO,GAAG,CAAA,CAAE,OAAO,KAAK,CAAA;AACtD;AAiBO,SAAS,WAAW,GAAA,EAAsB;AAC/C,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,SAAA,CAAU,GAAG,CAAA,IAAK,WAAA;AACnC,EAAA,OAAO,WAAW,GAAG,CAAA;AACvB;AAQO,SAAS,iBAAiB,GAAA,EAAsB;AACrD,EAAA,MAAM,GAAA,GAAM,IAAA,CAAK,SAAA,CAAU,GAAG,CAAA,IAAK,WAAA;AACnC,EAAA,OAAO,WAAW,GAAG,CAAA;AACvB;AAOO,SAAS,WAAA,GAAsB;AACpC,EAAA,OAAOC,iBAAA,EAAW;AACpB;AAKO,SAAS,cAAc,KAAA,EAAoC;AAChE,EAAA,OAAO,KAAA,CAAM,KAAK,GAAG,CAAA;AACvB;;;ACrHO,IAAM,gBAAN,MAA8C;AAAA,EAC3C,KAAA,uBAAY,GAAA,EAA0B;AAAA,EACtC,eAAA,GAAyD,IAAA;AAAA,EAEjE,WAAA,GAAc;AAEZ,IAAA,IAAA,CAAK,kBAAkB,WAAA,CAAY,MAAM,IAAA,CAAK,OAAA,IAAW,GAAM,CAAA;AAE/D,IAAA,IAAI,IAAA,CAAK,gBAAgB,KAAA,EAAO;AAC9B,MAAA,IAAA,CAAK,gBAAgB,KAAA,EAAM;AAAA,IAC7B;AAAA,EACF;AAAA,EAEA,MAAM,OAAA,CAAQ,GAAA,EAAa,IAAA,EAAqB,GAAA,EAAqC;AACnF,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,IAAI,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAEjC,IAAA,IAAI,QAAA,IAAY,QAAA,CAAS,SAAA,IAAa,GAAA,EAAK;AACzC,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AACrB,MAAA,QAAA,GAAW,MAAA;AAAA,IACb;AAGA,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,eAAe,QAAA,CAAS,IAAA;AAE9B,MAAA,IAAI,IAAA,IAAQ,YAAA,IAAgB,YAAA,KAAiB,IAAA,EAAM;AACjD,QAAA,OAAO;AAAA,UACL,MAAA,EAAQ,UAAA;AAAA,UACR,YAAA;AAAA,UACA,YAAA,EAAc;AAAA,SAChB;AAAA,MACF;AAGA,MAAA,IAAI,QAAA,CAAS,WAAW,WAAA,EAAa;AACnC,QAAA,MAAM,WAAW,QAAA,CAAS,QAAA;AAC1B,QAAA,IAAI,QAAA,EAAU;AACZ,UAAA,OAAO,EAAE,MAAA,EAAQ,KAAA,EAAO,QAAA,EAAS;AAAA,QACnC;AAAA,MACF;AAGA,MAAA,IAAI,QAAA,CAAS,WAAW,aAAA,EAAe;AACrC,QAAA,OAAO,EAAE,MAAA,EAAQ,UAAA,EAAY,SAAA,EAAW,SAAS,SAAA,EAAU;AAAA,MAC7D;AAAA,IACF;AAGA,IAAA,IAAA,CAAK,KAAA,CAAM,IAAI,GAAA,EAAK;AAAA,MAClB,MAAA,EAAQ,aAAA;AAAA,MACR,IAAA;AAAA,MACA,SAAA,EAAW,GAAA;AAAA,MACX,WAAW,GAAA,GAAM;AAAA,KAClB,CAAA;AAED,IAAA,OAAO,EAAE,QAAQ,UAAA,EAAW;AAAA,EAC9B;AAAA,EAEA,MAAM,IAAA,CAAK,GAAA,EAAa,QAAA,EAAyC;AAC/D,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AACnC,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,QAAA,CAAS,MAAA,GAAS,WAAA;AAClB,MAAA,QAAA,CAAS,QAAA,GAAW,QAAA;AAAA,IACtB;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,GAAA,EAA4B;AACxC,IAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,EACvB;AAAA,EAEA,MAAM,OAAO,GAAA,EAA4B;AACvC,IAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,EACvB;AAAA,EAEA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAA,GAAgB;AACd,IAAA,IAAI,KAAK,eAAA,EAAiB;AACxB,MAAA,aAAA,CAAc,KAAK,eAAe,CAAA;AAClC,MAAA,IAAA,CAAK,eAAA,GAAkB,IAAA;AAAA,IACzB;AAAA,EACF;AAAA,EAEQ,OAAA,GAAgB;AACtB,IAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AACrB,IAAA,KAAA,MAAW,CAAC,GAAA,EAAK,MAAM,CAAA,IAAK,KAAK,KAAA,EAAO;AACtC,MAAA,IAAI,MAAA,CAAO,aAAa,GAAA,EAAK;AAC3B,QAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;AAasC,IAAI,aAAA;;;ACzH1C,IAAI,eAA6B,EAAC;AAGlC,IAAI,oBAAA,GAA6C,IAAA;AAmB1C,SAAS,UAAU,MAAA,EAA4B;AACpD,EAAA,YAAA,GAAe,EAAE,GAAG,MAAA,EAAO;AAC7B;AAKO,SAAS,SAAA,GAA0B;AACxC,EAAA,OAAO,YAAA;AACT;AAMO,SAAS,WAAA,GAAoB;AAClC,EAAA,YAAA,GAAe,EAAC;AAChB,EAAA,oBAAA,GAAuB,IAAA;AACzB;AAMO,SAAS,iBAAA,GAAoC;AAClD,EAAA,IAAI,aAAa,OAAA,EAAS;AACxB,IAAA,OAAO,YAAA,CAAa,OAAA;AAAA,EACtB;AAEA,EAAA,IAAI,CAAC,oBAAA,EAAsB;AACzB,IAAA,oBAAA,GAAuB,IAAI,aAAA,EAAc;AAAA,EAC3C;AACA,EAAA,OAAO,oBAAA;AACT;AAKO,SAAS,eAAe,OAAA,EAAiD;AAC9E,EAAA,MAAM,SAAS,SAAA,EAAU;AACzB,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,OAAA,EAAS,OAAA,IAAW,MAAA,CAAO,WAAW,iBAAA,EAAkB;AAAA,IACjE,GAAA,EAAK,OAAA,EAAS,GAAA,IAAO,MAAA,CAAO,GAAA,IAAO,KAAA;AAAA,IACnC,WAAA,EAAa,OAAA,EAAS,WAAA,IAAe,MAAA,CAAO,WAAA,IAAe,IAAA;AAAA,IAC3D,KAAA,EAAO,OAAA,EAAS,KAAA,IAAS,MAAA,CAAO,KAAA,IAAS,KAAA;AAAA,IACzC,KAAA,EAAO,OAAA,EAAS,KAAA,IAAS,MAAA,CAAO,KAAA;AAAA,IAChC,MAAA,EAAQ,OAAA,EAAS,MAAA,IAAU,MAAA,CAAO,MAAA;AAAA,IAClC,UAAA,EAAY,OAAA,EAAS,UAAA,IAAc,MAAA,CAAO,UAAA;AAAA,IAC1C,OAAA,EAAS,OAAA,EAAS,OAAA,IAAW,MAAA,CAAO;AAAA,GACtC;AACF;;;AC5DO,IAAM,SAAN,MAAa;AAAA,EACD,OAAA;AAAA,EACA,GAAA;AAAA,EACA,KAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EAEjB,YAAY,OAAA,EAAwB;AAClC,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AACvB,IAAA,IAAA,CAAK,GAAA,GAAM,QAAA,CAAS,OAAA,CAAQ,GAAA,IAAO,KAAK,CAAA;AACxC,IAAA,IAAA,CAAK,KAAA,GAAQ,QAAQ,KAAA,IAAS,KAAA;AAC9B,IAAA,IAAA,CAAK,QAAQ,OAAA,CAAQ,KAAA;AACrB,IAAA,IAAA,CAAK,SAAS,OAAA,CAAQ,MAAA;AACtB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,UAAA;AAC1B,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,OAAA;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAoBA,MAAM,IAAO,OAAA,EAA+C;AAC1D,IAAA,MAAM,EAAE,GAAA,EAAK,IAAA,GAAO,IAAA,EAAM,SAAQ,GAAI,OAAA;AAGtC,IAAA,IAAI,CAAC,GAAA,EAAK;AACR,MAAA,MAAM,IAAI,eAAA,EAAgB;AAAA,IAC5B;AAEA,IAAA,IAAA,CAAK,GAAA,CAAI,CAAA,wBAAA,EAA2B,GAAG,CAAA,CAAE,CAAA;AAGzC,IAAA,IAAI,aAAA;AACJ,IAAA,IAAI;AACF,MAAA,aAAA,GAAgB,MAAM,IAAA,CAAK,OAAA,CAAQ,QAAQ,GAAA,EAAK,IAAA,EAAM,KAAK,GAAG,CAAA;AAAA,IAChE,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,YAAA,CAAa,wBAAA,EAA0B,GAAY,CAAA;AAAA,IAC/D;AAGA,IAAA,QAAQ,cAAc,MAAA;AAAQ,MAC5B,KAAK,KAAA,EAAO;AACV,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,mBAAA,EAAsB,GAAG,CAAA,CAAE,CAAA;AACpC,QAAA,IAAA,CAAK,KAAA,GAAQ,GAAA,EAAK,aAAA,CAAc,QAAQ,CAAA;AACxC,QAAA,OAAO;AAAA,UACL,IAAA,EAAM,cAAc,QAAA,CAAS,IAAA;AAAA,UAC7B,MAAA,EAAQ,IAAA;AAAA,UACR,MAAA,EAAQ,KAAA;AAAA,UACR,SAAA,EAAW,cAAc,QAAA,CAAS;AAAA,SACpC;AAAA,MACF;AAAA,MAEA,KAAK,UAAA,EAAY;AACf,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,kBAAA,EAAqB,GAAG,CAAA,CAAE,CAAA;AACnC,QAAA,MAAM,KAAA,GAAQ,IAAI,aAAA,CAAc,aAAA,CAAc,SAAS,CAAA;AACvD,QAAA,IAAA,CAAK,aAAa,GAAG,CAAA;AACrB,QAAA,IAAA,CAAK,OAAA,GAAU,KAAK,KAAK,CAAA;AACzB,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,MAEA,KAAK,UAAA,EAAY;AACf,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,uBAAA,EAA0B,GAAG,CAAA,CAAE,CAAA;AACxC,QAAA,MAAM,QAAQ,IAAI,aAAA,CAAc,aAAA,CAAc,YAAA,EAAc,cAAc,YAAY,CAAA;AACtF,QAAA,IAAA,CAAK,OAAA,GAAU,KAAK,KAAK,CAAA;AACzB,QAAA,MAAM,KAAA;AAAA,MACR;AAAA,MAEA,KAAK,UAAA,EAAY;AACf,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,uBAAA,EAA0B,GAAG,CAAA,CAAE,CAAA;AACxC,QAAA,IAAA,CAAK,SAAS,GAAG,CAAA;AACjB,QAAA;AAAA,MACF;AAAA;AAIF,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,GAAO,MAAM,OAAA,EAAQ;AAC3B,MAAA,MAAM,GAAA,GAAM,KAAK,GAAA,EAAI;AAGrB,MAAA,MAAM,cAAA,GAAiC;AAAA,QACrC,IAAA;AAAA,QACA,SAAA,EAAW,GAAA;AAAA,QACX;AAAA,OACF;AAEA,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,GAAA,EAAK,cAAc,CAAA;AAC3C,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,wBAAA,EAA2B,GAAG,CAAA,CAAE,CAAA;AAAA,MAC3C,SAAS,GAAA,EAAK;AAEZ,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,iCAAA,EAAoC,GAAG,CAAA,GAAA,EAAM,GAAG,CAAA,CAAE,CAAA;AAAA,MAC7D;AAEA,MAAA,OAAO;AAAA,QACL,IAAA;AAAA,QACA,MAAA,EAAQ,KAAA;AAAA,QACR,MAAA,EAAQ,SAAA;AAAA,QACR,SAAA,EAAW;AAAA,OACb;AAAA,IACF,SAAS,GAAA,EAAK;AAEZ,MAAA,IAAA,CAAK,GAAA,CAAI,CAAA,wBAAA,EAA2B,GAAG,CAAA,gBAAA,CAAkB,CAAA;AACzD,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,OAAA,CAAQ,OAAA,CAAQ,GAAG,CAAA;AAAA,MAChC,SAAS,UAAA,EAAY;AACnB,QAAA,IAAA,CAAK,GAAA,CAAI,CAAA,gCAAA,EAAmC,GAAG,CAAA,GAAA,EAAM,UAAU,CAAA,CAAE,CAAA;AAAA,MACnE;AACA,MAAA,MAAM,GAAA;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,IAAI,OAAA,EAAuB;AACjC,IAAA,IAAI,KAAK,KAAA,EAAO;AACd,MAAA,OAAA,CAAQ,GAAA,CAAI,CAAA,SAAA,EAAY,OAAO,CAAA,CAAE,CAAA;AAAA,IACnC;AAAA,EACF;AACF;AAuBO,SAAS,eAAe,OAAA,EAA0C;AACvE,EAAA,OAAO,IAAI,MAAA,CAAO,cAAA,CAAe,OAAO,CAAC,CAAA;AAC3C;;;AChJO,IAAM,MAAA,GAAS;AAAA;AAAA;AAAA;AAAA;AAAA,EAKpB,SAAA;AAAA;AAAA;AAAA;AAAA,EAKA,SAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,WAAA;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AACF;AA2BO,IAAMC,OAAAA,GAAyB,IAAI,aAAA","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/** 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, 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","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 { Oncely, createInstance } from './oncely.js';\nimport { MemoryStorage } from './memory.js';\nimport {\n configure as configureGlobal,\n getConfig,\n resetConfig,\n getDefaultStorage,\n} from './config.js';\nimport { HEADER, HEADER_REPLAY } from './constants.js';\nimport type { StorageAdapter } from './types.js';\n\n/**\n * Oncely namespace object.\n * All oncely functionality is accessed through this namespace.\n *\n * @example\n * ```typescript\n * import { oncely } from '@oncely/core';\n *\n * // Configure globally\n * oncely.configure({\n * storage: redis(),\n * ttl: '1h',\n * });\n *\n * // Create an instance\n * const instance = oncely.createInstance();\n * const result = await instance.run({\n * key: 'order-123',\n * handler: () => createOrder(data),\n * });\n * ```\n */\nexport const oncely = {\n /**\n * Configure global defaults for oncely.\n * Call this once at application startup.\n */\n configure: configureGlobal,\n\n /**\n * Get the current global configuration.\n */\n getConfig,\n\n /**\n * Reset global configuration to defaults.\n * Useful for testing.\n */\n resetConfig,\n\n /**\n * Get the default storage adapter.\n */\n getDefaultStorage,\n\n /**\n * Create an oncely instance with optional configuration.\n * Uses global config merged with provided options.\n */\n createInstance,\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/**\n * Interface for oncely namespace - can be extended via module augmentation.\n */\nexport interface OncelyNamespace {\n readonly configure: typeof configureGlobal;\n readonly getConfig: typeof getConfig;\n readonly resetConfig: typeof resetConfig;\n readonly getDefaultStorage: typeof getDefaultStorage;\n readonly createInstance: typeof createInstance;\n readonly HEADER: typeof HEADER;\n readonly HEADER_REPLAY: typeof HEADER_REPLAY;\n // Allow extension via module augmentation\n [key: string]: unknown;\n}\n\n// Re-export the Oncely class for advanced usage\nexport { Oncely };\n\n// Re-export memory storage\nexport { MemoryStorage };\n\n/**\n * Pre-configured memory storage instance.\n * Use this for quick setup in development.\n */\nexport const memory: StorageAdapter = new MemoryStorage();\n\n// Re-export configuration functions for direct import (backwards compatibility)\nexport { configure, getConfig, resetConfig, getDefaultStorage } from './config.js';\nexport type { GlobalConfig } from './config.js';\n\n// Re-export constants\nexport { HEADER, HEADER_REPLAY } from './constants.js';\n\n// Re-export errors\nexport {\n IdempotencyError,\n MissingKeyError,\n ConflictError,\n MismatchError,\n StorageError,\n type ProblemDetails,\n} from './errors.js';\n\n// Re-export utilities\nexport {\n parseTtl,\n simpleHash,\n secureHash,\n hashObject,\n hashObjectSecure,\n generateKey,\n composeKey,\n} from './utils.js';\n\n// Re-export types\nexport type {\n StorageAdapter,\n AcquireResult,\n StoredResponse,\n OncelyOptions,\n OncelyConfig,\n RunOptions,\n RunResult,\n OnHitCallback,\n OnMissCallback,\n OnConflictCallback,\n OnErrorCallback,\n} from './types.js';\n"]}
1
+ {"version":3,"sources":["../src/constants.ts","../src/errors.ts","../src/utils.ts","../src/memory.ts","../src/config.ts","../src/oncely.ts","../src/index.ts"],"names":["createHash","randomUUID","memory"],"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/** 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, 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 /**\n * Maximum payload size in bytes (default: 5MB).\n * Prevents unbounded memory growth from large responses.\n */\n readonly maxPayloadSize: number = 5 * 1024 * 1024;\n\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","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 onSkip: options?.onSkip ?? config.onSkip,\n };\n}\n","import type {\n StorageAdapter,\n OncelyOptions,\n RunOptions,\n RunResult,\n StoredResponse,\n OnHitCallback,\n OnMissCallback,\n OnConflictCallback,\n OnErrorCallback,\n OnSkipCallback,\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 * Estimate the size of a value in bytes when serialized to JSON.\n */\nfunction estimatePayloadSize(value: unknown): number {\n try {\n return new TextEncoder().encode(JSON.stringify(value)).length;\n } catch {\n // If serialization fails, return 0 to allow normal flow\n // (save() will likely fail anyway)\n return 0;\n }\n}\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 private readonly onSkip?: OnSkipCallback;\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 this.onSkip = options.onSkip;\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 // Check payload size limit\n const maxSize = this.storage.maxPayloadSize;\n if (maxSize !== undefined && maxSize > 0) {\n const payloadSize = estimatePayloadSize(storedResponse);\n if (payloadSize > maxSize) {\n this.log(\n `Payload too large for key: ${key} (${payloadSize} bytes > ${maxSize} bytes limit). Skipping cache.`\n );\n this.onSkip?.(key, 'payload_too_large', { payloadSize, maxSize });\n\n // Release lock and return without caching\n try {\n await this.storage.release(key);\n } catch (releaseErr) {\n this.log(`Failed to release lock for key: ${key} - ${releaseErr}`);\n }\n\n return {\n data,\n cached: false,\n status: 'created',\n createdAt: now,\n };\n }\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 { Oncely, createInstance } from './oncely.js';\nimport { MemoryStorage } from './memory.js';\nimport {\n configure as configureGlobal,\n getConfig,\n resetConfig,\n getDefaultStorage,\n} from './config.js';\nimport { HEADER, HEADER_REPLAY } from './constants.js';\nimport type { StorageAdapter } from './types.js';\n\n/**\n * Oncely namespace object.\n * All oncely functionality is accessed through this namespace.\n *\n * @example\n * ```typescript\n * import { oncely } from '@oncely/core';\n *\n * // Configure globally\n * oncely.configure({\n * storage: redis(),\n * ttl: '1h',\n * });\n *\n * // Create an instance\n * const instance = oncely.createInstance();\n * const result = await instance.run({\n * key: 'order-123',\n * handler: () => createOrder(data),\n * });\n * ```\n */\nexport const oncely = {\n /**\n * Configure global defaults for oncely.\n * Call this once at application startup.\n */\n configure: configureGlobal,\n\n /**\n * Get the current global configuration.\n */\n getConfig,\n\n /**\n * Reset global configuration to defaults.\n * Useful for testing.\n */\n resetConfig,\n\n /**\n * Get the default storage adapter.\n */\n getDefaultStorage,\n\n /**\n * Create an oncely instance with optional configuration.\n * Uses global config merged with provided options.\n */\n createInstance,\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/**\n * Interface for oncely namespace - can be extended via module augmentation.\n */\nexport interface OncelyNamespace {\n readonly configure: typeof configureGlobal;\n readonly getConfig: typeof getConfig;\n readonly resetConfig: typeof resetConfig;\n readonly getDefaultStorage: typeof getDefaultStorage;\n readonly createInstance: typeof createInstance;\n readonly HEADER: typeof HEADER;\n readonly HEADER_REPLAY: typeof HEADER_REPLAY;\n // Allow extension via module augmentation\n [key: string]: unknown;\n}\n\n// Re-export the Oncely class for advanced usage\nexport { Oncely };\n\n// Re-export memory storage\nexport { MemoryStorage };\n\n/**\n * Pre-configured memory storage instance.\n * Use this for quick setup in development.\n */\nexport const memory: StorageAdapter = new MemoryStorage();\n\n// Re-export configuration functions for direct import (backwards compatibility)\nexport { configure, getConfig, resetConfig, getDefaultStorage } from './config.js';\nexport type { GlobalConfig } from './config.js';\n\n// Re-export constants\nexport { HEADER, HEADER_REPLAY } from './constants.js';\n\n// Re-export errors\nexport {\n IdempotencyError,\n MissingKeyError,\n ConflictError,\n MismatchError,\n StorageError,\n type ProblemDetails,\n} from './errors.js';\n\n// Re-export utilities\nexport {\n parseTtl,\n simpleHash,\n secureHash,\n hashObject,\n hashObjectSecure,\n generateKey,\n composeKey,\n} from './utils.js';\n\n// Re-export types\nexport type {\n StorageAdapter,\n AcquireResult,\n StoredResponse,\n OncelyOptions,\n OncelyConfig,\n RunOptions,\n RunResult,\n OnHitCallback,\n OnMissCallback,\n OnConflictCallback,\n OnErrorCallback,\n OnSkipCallback,\n SkipReason,\n SkipDetails,\n} from './types.js';\n"]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OncelyConfig, S as StorageAdapter, c as createInstance } from './errors-BUehgS6t.cjs';
2
- export { A as AcquireResult, C as ConflictError, I as IdempotencyError, M as MemoryStorage, d as MismatchError, b as MissingKeyError, k as OnConflictCallback, l as OnErrorCallback, i as OnHitCallback, j as OnMissCallback, a as Oncely, g as OncelyOptions, P as ProblemDetails, R as RunOptions, h as RunResult, e as StorageError, f as StoredResponse } from './errors-BUehgS6t.cjs';
1
+ import { O as OncelyConfig, S as StorageAdapter, c as createInstance } from './errors-DVBjbOG7.cjs';
2
+ export { A as AcquireResult, C as ConflictError, I as IdempotencyError, M as MemoryStorage, a as MismatchError, b as MissingKeyError, d as OnConflictCallback, e as OnErrorCallback, f as OnHitCallback, g as OnMissCallback, h as OnSkipCallback, i as Oncely, j as OncelyOptions, P as ProblemDetails, R as RunOptions, k as RunResult, l as SkipDetails, m as SkipReason, n as StorageError, o as StoredResponse } from './errors-DVBjbOG7.cjs';
3
3
 
4
4
  type GlobalConfig = OncelyConfig;
5
5
  /**
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { O as OncelyConfig, S as StorageAdapter, c as createInstance } from './errors-BUehgS6t.js';
2
- export { A as AcquireResult, C as ConflictError, I as IdempotencyError, M as MemoryStorage, d as MismatchError, b as MissingKeyError, k as OnConflictCallback, l as OnErrorCallback, i as OnHitCallback, j as OnMissCallback, a as Oncely, g as OncelyOptions, P as ProblemDetails, R as RunOptions, h as RunResult, e as StorageError, f as StoredResponse } from './errors-BUehgS6t.js';
1
+ import { O as OncelyConfig, S as StorageAdapter, c as createInstance } from './errors-DVBjbOG7.js';
2
+ export { A as AcquireResult, C as ConflictError, I as IdempotencyError, M as MemoryStorage, a as MismatchError, b as MissingKeyError, d as OnConflictCallback, e as OnErrorCallback, f as OnHitCallback, g as OnMissCallback, h as OnSkipCallback, i as Oncely, j as OncelyOptions, P as ProblemDetails, R as RunOptions, k as RunResult, l as SkipDetails, m as SkipReason, n as StorageError, o as StoredResponse } from './errors-DVBjbOG7.js';
3
3
 
4
4
  type GlobalConfig = OncelyConfig;
5
5
  /**
package/dist/index.js CHANGED
@@ -172,6 +172,11 @@ function composeKey(...parts) {
172
172
 
173
173
  // src/memory.ts
174
174
  var MemoryStorage = class {
175
+ /**
176
+ * Maximum payload size in bytes (default: 5MB).
177
+ * Prevents unbounded memory growth from large responses.
178
+ */
179
+ maxPayloadSize = 5 * 1024 * 1024;
175
180
  store = /* @__PURE__ */ new Map();
176
181
  cleanupInterval = null;
177
182
  constructor() {
@@ -283,11 +288,19 @@ function resolveOptions(options) {
283
288
  onHit: options?.onHit ?? config.onHit,
284
289
  onMiss: options?.onMiss ?? config.onMiss,
285
290
  onConflict: options?.onConflict ?? config.onConflict,
286
- onError: options?.onError ?? config.onError
291
+ onError: options?.onError ?? config.onError,
292
+ onSkip: options?.onSkip ?? config.onSkip
287
293
  };
288
294
  }
289
295
 
290
296
  // src/oncely.ts
297
+ function estimatePayloadSize(value) {
298
+ try {
299
+ return new TextEncoder().encode(JSON.stringify(value)).length;
300
+ } catch {
301
+ return 0;
302
+ }
303
+ }
291
304
  var Oncely = class {
292
305
  storage;
293
306
  ttl;
@@ -296,6 +309,7 @@ var Oncely = class {
296
309
  onMiss;
297
310
  onConflict;
298
311
  onError;
312
+ onSkip;
299
313
  constructor(options) {
300
314
  this.storage = options.storage;
301
315
  this.ttl = parseTtl(options.ttl ?? "24h");
@@ -304,6 +318,7 @@ var Oncely = class {
304
318
  this.onMiss = options.onMiss;
305
319
  this.onConflict = options.onConflict;
306
320
  this.onError = options.onError;
321
+ this.onSkip = options.onSkip;
307
322
  }
308
323
  /**
309
324
  * Run an operation with idempotency protection.
@@ -373,6 +388,27 @@ var Oncely = class {
373
388
  createdAt: now,
374
389
  hash
375
390
  };
391
+ const maxSize = this.storage.maxPayloadSize;
392
+ if (maxSize !== void 0 && maxSize > 0) {
393
+ const payloadSize = estimatePayloadSize(storedResponse);
394
+ if (payloadSize > maxSize) {
395
+ this.log(
396
+ `Payload too large for key: ${key} (${payloadSize} bytes > ${maxSize} bytes limit). Skipping cache.`
397
+ );
398
+ this.onSkip?.(key, "payload_too_large", { payloadSize, maxSize });
399
+ try {
400
+ await this.storage.release(key);
401
+ } catch (releaseErr) {
402
+ this.log(`Failed to release lock for key: ${key} - ${releaseErr}`);
403
+ }
404
+ return {
405
+ data,
406
+ cached: false,
407
+ status: "created",
408
+ createdAt: now
409
+ };
410
+ }
411
+ }
376
412
  try {
377
413
  await this.storage.save(key, storedResponse);
378
414
  this.log(`Saved response for key: ${key}`);