@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.
- package/dist/{errors-BUehgS6t.d.cts → errors-DVBjbOG7.d.cts} +35 -1
- package/dist/{errors-BUehgS6t.d.ts → errors-DVBjbOG7.d.ts} +35 -1
- package/dist/index.cjs +37 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +37 -1
- package/dist/index.js.map +1 -1
- package/dist/testing.cjs +37 -1
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +37 -1
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
|
@@ -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,
|
|
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,
|
|
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}`);
|
package/dist/index.cjs.map
CHANGED
|
@@ -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":"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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 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-
|
|
2
|
-
export { A as AcquireResult, C as ConflictError, I as IdempotencyError, M as MemoryStorage,
|
|
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-
|
|
2
|
-
export { A as AcquireResult, C as ConflictError, I as IdempotencyError, M as MemoryStorage,
|
|
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}`);
|