@oncely/core 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -589
  3. package/package.json +9 -9
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stacks0x
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @oncely/core
2
2
 
3
- Core idempotency library with memory storage adapter. Provides the lean kernel with full TypeScript support.
3
+ Core idempotency library with memory storage. The foundation for all oncely packages.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@oncely/core.svg)](https://www.npmjs.com/package/@oncely/core)
4
6
 
5
7
  ## Installation
6
8
 
@@ -11,650 +13,133 @@ npm install @oncely/core
11
13
  ## Quick Start
12
14
 
13
15
  ```typescript
14
- import { oncely, MemoryStorage } from '@oncely/core';
15
-
16
- // Configure globally
17
- oncely.configure({ ttl: '1h' });
18
-
19
- // Create an instance
20
- const idempotency = oncely.createInstance({
21
- storage: new MemoryStorage(),
22
- });
23
-
24
- // Run with idempotency
25
- const result = await idempotency.run({
26
- key: 'unique-request-id',
27
- handler: async () => {
28
- return await createOrder({ amount: 100 });
29
- },
30
- });
16
+ import { MemoryStorage, hashObject, parseTtl } from '@oncely/core';
31
17
 
32
- console.log(result.data); // { id: 'ord_123' }
33
- console.log(result.cached); // false (first request)
34
- console.log(result.createdAt); // 1704888000000
35
- ```
36
-
37
- ## API Reference
38
-
39
- ### `oncely` Namespace
40
-
41
- Global namespace for configuration and instance creation.
42
-
43
- ```typescript
44
- // Configure global defaults
45
- oncely.configure({
46
- storage: new MemoryStorage(),
47
- ttl: '24h',
48
- debug: false,
49
- });
50
-
51
- // Get current configuration
52
- const config = oncely.getConfig();
53
-
54
- // Reset configuration
55
- oncely.resetConfig();
56
-
57
- // Get default storage
58
- const storage = oncely.getDefaultStorage();
59
-
60
- // Create instance with merged config
61
- const instance = oncely.createInstance({
62
- ttl: '1h', // Override global ttl
63
- });
64
-
65
- // Constants
66
- oncely.HEADER; // 'Idempotency-Key'
67
- oncely.HEADER_REPLAY; // 'Idempotency-Replay'
68
- ```
18
+ const storage = new MemoryStorage();
69
19
 
70
- ### `idempotency.run(options)`
20
+ // Acquire a lock for a request
21
+ const result = await storage.acquire('request-key', hashObject(body), parseTtl('1h'));
71
22
 
72
- Execute a handler with idempotency protection.
23
+ if (result.status === 'acquired') {
24
+ // Execute the operation
25
+ const response = await processRequest(body);
73
26
 
74
- ```typescript
75
- interface RunOptions<T = any> {
76
- key: string; // Idempotency key (required)
77
- hash?: string; // Request hash for mismatch detection
78
- handler: () => Promise<T>; // Async function to execute
79
- ttl?: number | string; // Override global TTL
27
+ // Save the response
28
+ await storage.save('request-key', {
29
+ data: response,
30
+ createdAt: Date.now(),
31
+ hash: hashObject(body),
32
+ });
80
33
  }
81
34
 
82
- interface RunResult<T = any> {
83
- data: T; // Handler return value
84
- cached: boolean; // Was this a cache hit?
85
- status: 'created' | 'hit'; // Result status
86
- createdAt: number; // Timestamp of original request
35
+ if (result.status === 'hit') {
36
+ // Return cached response
37
+ return result.response.data;
87
38
  }
88
-
89
- const result = await idempotency.run({
90
- key: 'order-123',
91
- hash: hashObject(req.body),
92
- handler: async () => {
93
- const order = await createOrder(req.body);
94
- return order;
95
- },
96
- ttl: '2h',
97
- });
98
39
  ```
99
40
 
100
- ## Configuration
101
-
102
- ### Global Configuration
41
+ ## Storage Adapter
103
42
 
104
- ```typescript
105
- oncely.configure({
106
- // Storage adapter (required)
107
- storage: new MemoryStorage(),
108
-
109
- // Time-to-live for cached responses
110
- // Accepts: milliseconds (number) or string ('1h', '30m', '5s')
111
- ttl: '24h',
112
-
113
- // Enable debug logging
114
- debug: false,
115
-
116
- // Callbacks
117
- onHit: (key, response) => {
118
- console.log(`Cache hit for key: ${key}`);
119
- },
120
- onMiss: (key) => {
121
- console.log(`Cache miss for key: ${key}`);
122
- },
123
- onConflict: (key, error) => {
124
- console.log(`Conflict for key: ${key}`);
125
- },
126
- });
127
- ```
43
+ `MemoryStorage` is the default adapter, ideal for:
128
44
 
129
- ### Per-Instance Configuration
45
+ - Development and testing
46
+ - Single-instance deployments
47
+ - Prototyping
130
48
 
131
- ```typescript
132
- const instance = oncely.createInstance({
133
- storage: redis(),
134
- ttl: '1h',
135
- onHit: (key) => analytics.track('cache_hit', { key }),
136
- });
137
- ```
49
+ For production multi-instance deployments, use:
138
50
 
139
- ## Error Handling
51
+ - [@oncely/redis](https://www.npmjs.com/package/@oncely/redis) - Standard Redis
52
+ - [@oncely/upstash](https://www.npmjs.com/package/@oncely/upstash) - Serverless Redis
140
53
 
141
- All errors are instances of `IdempotencyError` and include a `statusCode`.
54
+ ### StorageAdapter Interface
142
55
 
143
56
  ```typescript
144
- import {
145
- IdempotencyError,
146
- MissingKeyError,
147
- ConflictError,
148
- MismatchError,
149
- StorageError,
150
- } from '@oncely/core';
151
-
152
- try {
153
- const result = await idempotency.run({ key, handler });
154
- } catch (error) {
155
- if (error instanceof MissingKeyError) {
156
- // status: 400
157
- // Idempotency key is required but not provided
158
- return new Response('Idempotency-Key header required', { status: 400 });
159
- }
160
-
161
- if (error instanceof ConflictError) {
162
- // status: 409
163
- // Request with this key is already in progress
164
- return new Response('Request in progress', {
165
- status: 409,
166
- headers: { 'Retry-After': String(error.retryAfter ?? 30) },
167
- });
168
- }
169
-
170
- if (error instanceof MismatchError) {
171
- // status: 422
172
- // Same key used with different request body
173
- return new Response('Idempotency key mismatch', {
174
- status: 422,
175
- headers: {
176
- 'Content-Type': 'application/problem+json',
177
- },
178
- body: JSON.stringify({
179
- type: 'https://oncely.dev/errors/mismatch',
180
- title: 'Idempotency Key Mismatch',
181
- detail: `Key was previously used with different request body`,
182
- existingHash: error.existingHash,
183
- providedHash: error.providedHash,
184
- }),
185
- });
186
- }
187
-
188
- if (error instanceof StorageError) {
189
- // status: 500
190
- // Storage adapter failed
191
- console.error('Storage error:', error);
192
- return new Response('Storage error', { status: 500 });
193
- }
194
-
195
- // Other errors
196
- throw error;
57
+ interface StorageAdapter {
58
+ acquire(key: string, hash: string | null, ttl: number): Promise<AcquireResult>;
59
+ save(key: string, response: StoredResponse): Promise<void>;
60
+ release(key: string): Promise<void>;
61
+ delete(key: string): Promise<void>;
62
+ clear(): Promise<void>;
197
63
  }
64
+
65
+ type AcquireResult =
66
+ | { status: 'acquired' }
67
+ | { status: 'hit'; response: StoredResponse }
68
+ | { status: 'conflict'; startedAt: number }
69
+ | { status: 'mismatch'; existingHash: string; providedHash: string };
198
70
  ```
199
71
 
200
72
  ## Utilities
201
73
 
202
- ### Key Generation
203
-
204
- ```typescript
205
- import { generateKey, composeKey } from '@oncely/core';
206
-
207
- // Generate UUID-based key
208
- const key = generateKey();
209
- // => '550e8400-e29b-41d4-a716-446655440000'
210
-
211
- // Compose keys from parts
212
- const key = composeKey('order', orderId, 'attempt', attemptNumber);
213
- // => 'order:123:attempt:1'
214
- ```
215
-
216
- ### Hashing
74
+ ### Hash Objects
217
75
 
218
76
  ```typescript
219
77
  import { hashObject } from '@oncely/core';
220
78
 
221
- // Hash request body for mismatch detection
222
- const hash = hashObject(req.body);
223
- // => 'f8e9b4a2c1d5e3f6...'
224
-
225
- // Works with any serializable object
226
- const hash = hashObject({
227
- amount: 100,
228
- currency: 'USD',
229
- items: [{ id: 1, qty: 2 }],
230
- });
79
+ const hash = hashObject({ amount: 100, currency: 'USD' });
80
+ // => 'a1b2c3d4...'
231
81
  ```
232
82
 
233
- ### TTL Parsing
83
+ ### Parse TTL
234
84
 
235
85
  ```typescript
236
86
  import { parseTtl } from '@oncely/core';
237
87
 
238
- parseTtl(3600000); // 3600000 (milliseconds)
239
88
  parseTtl('1h'); // 3600000
240
89
  parseTtl('30m'); // 1800000
241
90
  parseTtl('5s'); // 5000
242
91
  parseTtl('1d'); // 86400000
243
-
244
- // Invalid values throw Error
245
- parseTtl('invalid'); // throws
92
+ parseTtl(60000); // 60000
246
93
  ```
247
94
 
248
- ## Storage Adapters
249
-
250
- ### Memory (Built-in, Default)
251
-
252
- In-memory storage for development and testing.
95
+ ## Error Classes
253
96
 
254
97
  ```typescript
255
- import { oncely, MemoryStorage } from '@oncely/core';
256
-
257
- // Implicit (default)
258
- const idempotency = oncely.createInstance();
259
-
260
- // Explicit
261
- const storage = new MemoryStorage();
262
- const idempotency = oncely.createInstance({ storage });
263
-
264
- // Shared singleton
265
- import { memory } from '@oncely/core';
266
- const idempotency = oncely.createInstance({ storage: memory });
267
- ```
268
-
269
- **Pros:**
270
-
271
- - Zero configuration
272
- - Fast (no I/O)
273
- - Easy to test
274
-
275
- **Cons:**
276
-
277
- - Data lost on restart
278
- - Not shared between processes
279
- - Only suitable for single-instance apps
280
-
281
- ### Redis (Production)
282
-
283
- For production deployments with ioredis.
284
-
285
- ```bash
286
- npm install @oncely/redis ioredis
287
- ```
288
-
289
- ```typescript
290
- import { redis } from '@oncely/redis';
291
-
292
- // From environment variable (ONCELY_REDIS_URL)
293
- const storage = redis();
294
-
295
- // From explicit URL
296
- const storage = redis('redis://localhost:6379');
297
-
298
- // With options
299
- const storage = redis('redis://localhost:6379', {
300
- keyPrefix: 'oncely:',
301
- });
302
-
303
- // From existing client
304
- import Redis from 'ioredis';
305
- const client = new Redis(process.env.REDIS_URL);
306
- const storage = new RedisStorage(client, { keyPrefix: 'oncely:' });
307
- ```
308
-
309
- See [@oncely/redis](../redis) for full documentation.
310
-
311
- ### Upstash (Serverless)
312
-
313
- For serverless and edge deployments.
314
-
315
- ```bash
316
- npm install @oncely/upstash
317
- ```
318
-
319
- ```typescript
320
- import { upstash } from '@oncely/upstash';
321
-
322
- // From environment variables
323
- const storage = upstash();
324
-
325
- // With explicit config
326
- const storage = upstash({
327
- url: process.env.UPSTASH_REDIS_REST_URL,
328
- token: process.env.UPSTASH_REDIS_REST_TOKEN,
329
- });
330
- ```
331
-
332
- See [@oncely/upstash](../upstash) for full documentation.
333
-
334
- ### Custom Adapters
335
-
336
- Implement the `StorageAdapter` interface for custom backends.
337
-
338
- ```typescript
339
- import type { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';
340
-
341
- export class DatabaseStorage implements StorageAdapter {
342
- async acquire(key: string, hash: string | null, ttl: number): Promise<AcquireResult> {
343
- // Acquire lock and check for existing response
344
- // Return { status: 'acquired', startedAt: Date.now() }
345
- // Or { status: 'conflict', startedAt, retryAfter }
346
- // Or { status: 'mismatch', existingHash, providedHash }
347
- }
348
-
349
- async save(key: string, response: StoredResponse): Promise<void> {
350
- // Save response and release lock
351
- }
352
-
353
- async release(key: string): Promise<void> {
354
- // Release lock without saving
355
- }
356
-
357
- async delete(key: string): Promise<void> {
358
- // Optional: delete stored response
359
- }
360
-
361
- async clear(): Promise<void> {
362
- // Optional: clear all stored responses
363
- }
364
- }
365
-
366
- // Usage
367
- const storage = new DatabaseStorage();
368
- const idempotency = oncely.createInstance({ storage });
369
- ```
370
-
371
- ## Testing
372
-
373
- The library includes testing utilities via `@oncely/core/testing`.
374
-
375
- ```typescript
376
- import { MockStorage, createTestInstance } from '@oncely/core/testing';
377
-
378
- // Use MockStorage to track operations
379
- const storage = new MockStorage();
380
- const idempotency = createTestInstance({ storage });
381
-
382
- // Run handler
383
- await idempotency.run({
384
- key: 'test-key',
385
- handler: async () => ({ id: 1 }),
386
- });
387
-
388
- // Verify operations
389
- const operations = storage.operations;
390
- // [
391
- // { type: 'acquire', key: 'test-key', timestamp: ... },
392
- // { type: 'save', key: 'test-key', timestamp: ... },
393
- // ]
394
-
395
- const saveOps = storage.getOperationsForKey('test-key', 'save');
396
- expect(saveOps).toHaveLength(1);
397
- ```
398
-
399
- ## Callbacks
400
-
401
- Hooks for monitoring and logging.
402
-
403
- ```typescript
404
- oncely.configure({
405
- onHit: (key, response) => {
406
- console.log(`Cache hit: ${key}`);
407
- analytics.track('idempotency_hit', { key });
408
- },
409
-
410
- onMiss: (key) => {
411
- console.log(`Cache miss: ${key}`);
412
- analytics.track('idempotency_miss', { key });
413
- },
414
-
415
- onConflict: (key, error) => {
416
- console.log(`Conflict: ${key}`);
417
- metrics.increment('idempotency_conflicts');
418
- },
419
- });
420
- ```
421
-
422
- ## Type Definitions
423
-
424
- Full TypeScript support with strict typing.
425
-
426
- ```typescript
427
- interface Oncely {
428
- configure(options: OncelyConfig): void;
429
- getConfig(): OncelyConfig;
430
- resetConfig(): void;
431
- getDefaultStorage(): StorageAdapter;
432
- createInstance(options?: Partial<OncelyOptions>): OncelyInstance;
433
- HEADER: string;
434
- HEADER_REPLAY: string;
435
- }
436
-
437
- interface OncelyOptions {
438
- storage: StorageAdapter;
439
- ttl: number | string;
440
- debug: boolean;
441
- onHit?: (key: string, response: StoredResponse) => void;
442
- onMiss?: (key: string) => void;
443
- onConflict?: (key: string, error: ConflictError) => void;
444
- }
445
-
446
- interface OncelyInstance {
447
- run<T>(options: RunOptions<T>): Promise<RunResult<T>>;
448
- }
449
- ```
450
-
451
- ## Performance
452
-
453
- - Small bundle: ~11 KB (ESM)
454
- - No external dependencies
455
- - Tree-shakeable exports
456
- - Efficient hashing (SHA256)
457
- - Minimal memory overhead
458
-
459
- ## FAQ
460
-
461
- **Q: Should I use memory storage in production?**
462
- A: Only for single-instance deployments. Use Redis or Upstash for distributed systems.
463
-
464
- **Q: How do I generate idempotency keys on the client?**
465
- A: See [@oncely/client](../client) package.
466
-
467
- **Q: Can I customize error responses?**
468
- A: Yes, catch errors and format responses as needed. Framework packages (Express, Next.js) handle this automatically.
469
-
470
- **Q: How do I migrate from another idempotency library?**
471
- A: See [migration guide](../../README.md#migration-guide) in root README.
472
-
473
- ## License
474
-
475
- MIT © stacks0x
476
-
477
- ```typescript
478
- import { fromEnv } from 'oncely';
479
-
480
- // Read REDIS_URL or UPSTASH_REDIS_REST_URL + TOKEN
481
- const storage = fromEnv();
482
-
483
- // With options
484
- const storage = fromEnv({
485
- preferUpstash: true, // Prefer Upstash when both configured
486
- env: {
487
- redisUrl: 'MY_REDIS_URL', // Custom env var names
488
- upstashUrl: 'MY_UPSTASH_URL',
489
- upstashToken: 'MY_UPSTASH_TOKEN',
490
- },
491
- });
492
- ```
493
-
494
- ### `redis(url, options?)`
495
-
496
- Quick helper to create Redis storage from a URL.
497
-
498
- ```typescript
499
- import { redis } from 'oncely';
500
-
501
- const storage = redis('redis://localhost:6379');
502
- const storage = redis(process.env.REDIS_URL!, { keyPrefix: 'myapp:' });
503
- ```
504
-
505
- ### `idempotency.run(options)`
506
-
507
- Run an operation with idempotency protection.
508
-
509
- ```typescript
510
- const result = await idempotency.run({
511
- key: 'unique-key', // Required: idempotency key
512
- hash: 'request-hash', // Optional: for mismatch detection
513
- handler: async () => {
514
- // Required: your operation
515
- return { id: 1 };
516
- },
517
- });
518
-
519
- // result.data - your handler's return value
520
- // result.cached - boolean, was this a cache hit?
521
- // result.status - 'created' | 'hit'
522
- // result.createdAt - when the response was created
523
- ```
524
-
525
- ## Errors
526
-
527
- All errors extend `IdempotencyError` and include a `statusCode` property.
528
-
529
- | Error | Status | When |
530
- | ----------------- | ------ | ---------------------------- |
531
- | `MissingKeyError` | 400 | Key is undefined/null |
532
- | `ConflictError` | 409 | Same key already in progress |
533
- | `MismatchError` | 422 | Same key, different hash |
534
- | `StorageError` | 500 | Storage adapter failed |
535
-
536
- ```typescript
537
- import { IdempotencyError, ConflictError } from 'oncely';
538
-
539
- try {
540
- await idempotency.run({ key, handler });
541
- } catch (err) {
542
- if (err instanceof ConflictError) {
543
- // Return 409 with Retry-After header
544
- return new Response('Request in progress', {
545
- status: 409,
546
- headers: { 'Retry-After': String(err.retryAfter) },
547
- });
548
- }
549
- }
550
- ```
551
-
552
- ## Utilities
553
-
554
- ```typescript
555
- import { generateKey, composeKey, hashObject } from 'oncely';
556
-
557
- // Generate a UUID-like key
558
- const key = generateKey(); // "f47ac10b-58cc-4372-a567-0e02b2c3d479"
559
-
560
- // Compose a deterministic key
561
- const key = composeKey('user', userId, 'order', orderId); // "user:123:order:456"
562
-
563
- // Hash an object for fingerprinting
564
- const hash = hashObject({ amount: 100, currency: 'usd' });
565
- ```
566
-
567
- ## Storage Adapters
568
-
569
- ### Memory (Built-in, Default)
570
-
571
- For development and testing. Used automatically when no storage is configured:
572
-
573
- ```typescript
574
- import { oncely, memory, MemoryStorage } from 'oncely';
575
-
576
- // Zero-config — uses shared memory
577
- const idempotency = oncely();
578
-
579
- // Explicit memory storage
580
- const idempotency = oncely({ storage: memory });
581
-
582
- // Custom instance
583
- const myStorage = new MemoryStorage();
584
- const idempotency = oncely({ storage: myStorage });
98
+ import {
99
+ IdempotencyError,
100
+ MissingKeyError, // 400 - Key required but missing
101
+ ConflictError, // 409 - Request in progress
102
+ MismatchError, // 422 - Key reused with different body
103
+ } from '@oncely/core';
585
104
  ```
586
105
 
587
- ### Redis with ioredis
106
+ All errors extend `IdempotencyError` and include:
588
107
 
589
- For production with self-hosted Redis or managed services:
108
+ - `statusCode` - HTTP status code
109
+ - `toProblemDetails()` - RFC 7807 Problem Details
590
110
 
591
- ```bash
592
- npm install ioredis
593
- ```
111
+ ## Constants
594
112
 
595
113
  ```typescript
596
- import { oncely, redis, ioredis } from 'oncely';
597
- import Redis from 'ioredis';
114
+ import { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER } from '@oncely/core';
598
115
 
599
- // Quick setup from URL
600
- const idempotency = oncely({
601
- storage: redis('redis://localhost:6379'),
602
- });
603
-
604
- // With existing client
605
- const client = new Redis(process.env.REDIS_URL);
606
- const idempotency = oncely({
607
- storage: ioredis(client, { keyPrefix: 'idempotent:' }),
608
- });
116
+ IDEMPOTENCY_KEY_HEADER; // 'Idempotency-Key'
117
+ IDEMPOTENCY_REPLAY_HEADER; // 'Idempotency-Replay'
609
118
  ```
610
119
 
611
- ### Upstash / Vercel KV
612
-
613
- For serverless environments:
614
-
615
- ```bash
616
- npm install @upstash/redis
617
- ```
120
+ ## Testing Utilities
618
121
 
619
122
  ```typescript
620
- import { oncely, upstash } from 'oncely';
621
- import { Redis } from '@upstash/redis';
123
+ import { createTestStorage, MockStorage } from '@oncely/core/testing';
622
124
 
623
- const redis = new Redis({
624
- url: process.env.UPSTASH_REDIS_REST_URL!,
625
- token: process.env.UPSTASH_REDIS_REST_TOKEN!,
125
+ // Create a test storage with optional pre-seeded data
126
+ const storage = createTestStorage({
127
+ 'existing-key': { data: { id: 1 }, createdAt: Date.now(), hash: null },
626
128
  });
627
129
 
628
- const idempotency = oncely({
629
- storage: upstash(redis, { keyPrefix: 'idempotent:' }),
630
- });
130
+ // Or use MockStorage for fine-grained control
131
+ const mock = new MockStorage();
132
+ mock.simulateConflict('key');
133
+ mock.simulateError(new Error('Connection failed'));
631
134
  ```
632
135
 
633
- ### Custom Adapters
634
-
635
- Implement the `StorageAdapter` interface:
136
+ ## Related Packages
636
137
 
637
- ```typescript
638
- import type { StorageAdapter } from 'oncely';
639
-
640
- const myAdapter: StorageAdapter = {
641
- async acquire(key, hash, ttl) {
642
- // Try to acquire lock, return { acquired: true/false, response?: cached }
643
- },
644
- async save(key, response) {
645
- // Save response, release lock
646
- },
647
- async release(key) {
648
- // Release lock without saving
649
- },
650
- async delete(key) {
651
- // Delete stored response (optional)
652
- },
653
- async clear() {
654
- // Clear all stored responses (optional)
655
- },
656
- };
657
- ```
138
+ - [@oncely/express](https://www.npmjs.com/package/@oncely/express) - Express middleware
139
+ - [@oncely/next](https://www.npmjs.com/package/@oncely/next) - Next.js integration
140
+ - [@oncely/client](https://www.npmjs.com/package/@oncely/client) - Client utilities
141
+ - [@oncely/redis](https://www.npmjs.com/package/@oncely/redis) - Redis adapter
142
+ - [@oncely/upstash](https://www.npmjs.com/package/@oncely/upstash) - Upstash adapter
658
143
 
659
144
  ## License
660
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oncely/core",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Idempotency engine for HTTP APIs - ensures operations execute exactly once",
5
5
  "author": "stacks0x",
6
6
  "license": "MIT",
@@ -40,12 +40,6 @@
40
40
  "README.md",
41
41
  "LICENSE"
42
42
  ],
43
- "scripts": {
44
- "build": "tsup",
45
- "dev": "tsup --watch",
46
- "typecheck": "tsc --noEmit",
47
- "clean": "rm -rf dist"
48
- },
49
43
  "devDependencies": {
50
44
  "tsup": "^8.0.1",
51
45
  "typescript": "^5.3.3"
@@ -54,6 +48,12 @@
54
48
  "access": "public"
55
49
  },
56
50
  "engines": {
57
- "node": ">=18.0.0"
51
+ "node": ">=20.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup",
55
+ "dev": "tsup --watch",
56
+ "typecheck": "tsc --noEmit",
57
+ "clean": "rm -rf dist"
58
58
  }
59
- }
59
+ }