@oncely/client 0.2.0 → 0.2.1

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 +94 -70
  3. package/package.json +13 -10
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,84 +1,124 @@
1
1
  # @oncely/client
2
2
 
3
- Client-side utilities for generating idempotency keys.
3
+ Client-side utilities for idempotency key generation and response handling.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@oncely/client.svg)](https://www.npmjs.com/package/@oncely/client)
4
6
 
5
7
  ## Installation
6
8
 
9
+ ```bash
7
10
  npm install @oncely/client
11
+ ```
8
12
 
9
- ## Usage
13
+ ## Quick Start
10
14
 
11
- import { store, generateKey, randomKey, compositeKey } from '@oncely/client';
15
+ ```typescript
16
+ import { generateKey } from '@oncely/client';
12
17
 
13
- // Generate a UUID-based key
14
- const key = generateKey(); // "550e8400-e29b-41d4-a716-446655440000"
18
+ const response = await fetch('/api/orders', {
19
+ method: 'POST',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ 'Idempotency-Key': generateKey(),
23
+ },
24
+ body: JSON.stringify(order),
25
+ });
26
+ ```
15
27
 
16
- // Generate a random alphanumeric key
17
- const key = randomKey(); // "x7k9m2n4p1"
18
- const key = randomKey(32); // longer key
28
+ ## Key Generation
19
29
 
20
- // Create composite keys
21
- const key = compositeKey('order', orderId, 'attempt', 1);
22
- // "order:123:attempt:1"
30
+ ```typescript
31
+ import { generateKey, generatePrefixedKey, createKeyGenerator } from '@oncely/client';
23
32
 
24
- // Store and retrieve keys (in-memory)
25
- store.set('current-order', key);
26
- const stored = store.get('current-order');
27
- store.delete('current-order');
28
- store.clear();
33
+ // UUID v4
34
+ const key = generateKey();
35
+ // => '550e8400-e29b-41d4-a716-446655440000'
29
36
 
30
- ## License
37
+ // Prefixed UUID
38
+ const key = generatePrefixedKey('ord');
39
+ // => 'ord_550e8400-e29b-41d4-a716-446655440000'
31
40
 
32
- MIT
33
- // Response headers include: Idempotency-Replay: true
34
- }
41
+ // Deterministic key from components
42
+ const key = generateKey('user', userId, 'create-order');
43
+ // => 'f8e9b4a2' (consistent hash for same inputs)
35
44
 
36
- // Check if response indicates a conflict (409)
37
- if (oncely.isConflict(response)) {
38
- // Request with this key is already in progress
39
- const retryAfter = oncely.getRetryAfter(response);
40
- await delay(retryAfter \* 1000);
41
- // Retry...
42
- }
45
+ // Key generator namespace
46
+ const key = createKeyGenerator();
47
+ key(); // UUID
48
+ key('a', 'b', 'c'); // Deterministic hash
49
+ key.prefixed('txn'); // Prefixed UUID
50
+ ```
51
+
52
+ ## Key Store
43
53
 
44
- // Check if response indicates a mismatch (422)
45
- if (oncely.isMismatch(response)) {
46
- // Key was reused with different request body
47
- // Generate a new key and retry
54
+ Track pending requests to prevent duplicate submissions:
55
+
56
+ ```typescript
57
+ import { createStore } from '@oncely/client';
58
+
59
+ const store = createStore({
60
+ prefix: 'oncely:',
61
+ ttl: '5m',
62
+ storage: localStorage,
63
+ });
64
+
65
+ // Mark request as pending
66
+ store.pending('key-123');
67
+
68
+ // Check if request is pending
69
+ if (store.isPending('key-123')) {
70
+ throw new Error('Request already in progress');
48
71
  }
49
72
 
50
- // Parse RFC 7807 problem details from error response
51
- const problem = await oncely.getProblem(response);
52
- // => { type: "https://oncely.dev/errors/conflict", title: "Request in progress", ... }
73
+ // Mark as completed
74
+ store.complete('key-123');
53
75
 
54
- ````
76
+ // Clear a specific key
77
+ store.clear('key-123');
78
+
79
+ // Clear all oncely keys
80
+ store.clearAll();
81
+ ```
55
82
 
56
- ### Constants
83
+ ## Response Helpers
57
84
 
58
85
  ```typescript
59
- import { oncely } from '@oncely/client';
86
+ import { isReplay, isConflict, isMismatch, getRetryAfter } from '@oncely/client';
60
87
 
61
- // Standard header name (IETF draft)
62
- oncely.HEADER; // => "Idempotency-Key"
88
+ const response = await fetch('/api/orders', { ... });
89
+
90
+ // Check if response was served from cache
91
+ if (isReplay(response)) {
92
+ console.log('Cached response');
93
+ }
94
+
95
+ // Handle 409 Conflict
96
+ if (isConflict(response)) {
97
+ const seconds = getRetryAfter(response);
98
+ await delay(seconds * 1000);
99
+ // Retry with same key...
100
+ }
63
101
 
64
- // Replay indicator header
65
- oncely.HEADER_REPLAY; // => "Idempotency-Replay"
66
- ````
102
+ // Handle 422 Mismatch
103
+ if (isMismatch(response)) {
104
+ // Key was reused with different body - generate new key
105
+ }
106
+ ```
67
107
 
68
- ## Usage with Fetch Wrappers
108
+ ## Usage with Fetch Libraries
69
109
 
70
- ### With ky
110
+ ### ky
71
111
 
72
112
  ```typescript
73
113
  import ky from 'ky';
74
- import { oncely } from '@oncely/client';
114
+ import { generateKey, IDEMPOTENCY_KEY_HEADER } from '@oncely/client';
75
115
 
76
116
  const api = ky.extend({
77
117
  hooks: {
78
118
  beforeRequest: [
79
119
  (request) => {
80
120
  if (['POST', 'PUT', 'PATCH'].includes(request.method)) {
81
- request.headers.set(oncely.HEADER, oncely.key());
121
+ request.headers.set(IDEMPOTENCY_KEY_HEADER, generateKey());
82
122
  }
83
123
  },
84
124
  ],
@@ -86,47 +126,31 @@ const api = ky.extend({
86
126
  });
87
127
  ```
88
128
 
89
- ### With axios
129
+ ### axios
90
130
 
91
131
  ```typescript
92
132
  import axios from 'axios';
93
- import { oncely } from '@oncely/client';
133
+ import { generateKey, IDEMPOTENCY_KEY_HEADER } from '@oncely/client';
94
134
 
95
135
  const api = axios.create();
96
136
 
97
137
  api.interceptors.request.use((config) => {
98
- if (['post', 'put', 'patch'].includes(config.method?.toLowerCase() ?? '')) {
99
- config.headers[oncely.HEADER] = oncely.key();
138
+ if (['post', 'put', 'patch'].includes(config.method ?? '')) {
139
+ config.headers[IDEMPOTENCY_KEY_HEADER] = generateKey();
100
140
  }
101
141
  return config;
102
142
  });
103
143
  ```
104
144
 
105
- ## TypeScript
106
-
107
- Full TypeScript support with exported types:
145
+ ## Constants
108
146
 
109
147
  ```typescript
110
- import type { ProblemDetails } from '@oncely/client';
148
+ import { IDEMPOTENCY_KEY_HEADER, IDEMPOTENCY_REPLAY_HEADER } from '@oncely/client';
111
149
 
112
- const problem: ProblemDetails = await oncely.getProblem(response);
150
+ IDEMPOTENCY_KEY_HEADER; // 'Idempotency-Key'
151
+ IDEMPOTENCY_REPLAY_HEADER; // 'Idempotency-Replay'
113
152
  ```
114
153
 
115
- ## Browser Support
116
-
117
- Works in all modern browsers. For older browsers, ensure you have:
118
-
119
- - `crypto.randomUUID()` polyfill (or use custom key generator)
120
- - `localStorage` / `sessionStorage` (for key store)
121
-
122
- ## Related Packages
123
-
124
- - [@oncely/core](../core) - Core idempotency engine for servers
125
- - [@oncely/redis](../redis) - Redis storage adapter
126
- - [@oncely/upstash](../upstash) - Upstash Redis adapter
127
- - [@oncely/express](../express) - Express.js middleware
128
- - [@oncely/next](../next) - Next.js API route handlers
129
-
130
154
  ## License
131
155
 
132
156
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oncely/client",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Client-side idempotency helpers for browser and frontend applications",
5
5
  "author": "stacks0x",
6
6
  "license": "MIT",
@@ -34,18 +34,21 @@
34
34
  "README.md",
35
35
  "LICENSE"
36
36
  ],
37
- "scripts": {
38
- "build": "tsup",
39
- "dev": "tsup --watch",
40
- "typecheck": "tsc --noEmit",
41
- "clean": "rm -rf dist"
42
- },
43
37
  "devDependencies": {
44
38
  "tsup": "^8.0.1",
45
39
  "typescript": "^5.3.3"
46
40
  },
41
+ "publishConfig": {
42
+ "access": "public"
43
+ },
47
44
  "engines": {
48
- "node": ">=18"
45
+ "node": ">=20.0.0"
49
46
  },
50
- "sideEffects": false
51
- }
47
+ "sideEffects": false,
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "typecheck": "tsc --noEmit",
52
+ "clean": "rm -rf dist"
53
+ }
54
+ }