@oncely/next 1.0.0 → 1.0.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.
- package/LICENSE +21 -0
- package/README.md +68 -95
- package/package.json +13 -13
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,145 +1,118 @@
|
|
|
1
1
|
# @oncely/next
|
|
2
2
|
|
|
3
|
-
Next.js integration for
|
|
3
|
+
Next.js integration for HTTP idempotency. Supports App Router and Pages Router.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@oncely/next)
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
9
|
+
```bash
|
|
7
10
|
npm install @oncely/core @oncely/next
|
|
11
|
+
```
|
|
8
12
|
|
|
9
13
|
## App Router
|
|
10
14
|
|
|
15
|
+
```typescript
|
|
11
16
|
// app/api/orders/route.ts
|
|
12
17
|
import { next } from '@oncely/next';
|
|
13
18
|
|
|
14
19
|
export const POST = next()(async (req) => {
|
|
15
|
-
const
|
|
16
|
-
return Response.json(
|
|
20
|
+
const order = await createOrder(await req.json());
|
|
21
|
+
return Response.json(order, { status: 201 });
|
|
17
22
|
});
|
|
18
|
-
|
|
19
|
-
// With options
|
|
20
|
-
export const POST = next({
|
|
21
|
-
required: true,
|
|
22
|
-
ttl: '1h',
|
|
23
|
-
})(handler);
|
|
23
|
+
```
|
|
24
24
|
|
|
25
25
|
## Pages Router
|
|
26
26
|
|
|
27
|
+
```typescript
|
|
27
28
|
// pages/api/orders.ts
|
|
28
29
|
import { pages } from '@oncely/next/pages';
|
|
29
30
|
|
|
30
31
|
export default pages()(async (req, res) => {
|
|
31
|
-
|
|
32
|
+
const order = await createOrder(req.body);
|
|
33
|
+
res.status(201).json(order);
|
|
32
34
|
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
Both `next()` and `pages()` accept the same options:
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
export const POST = next({
|
|
43
|
+
storage: upstash(), // Storage adapter
|
|
44
|
+
ttl: '1h', // Cache duration
|
|
45
|
+
required: true, // Require Idempotency-Key header
|
|
46
|
+
failOpen: true, // Continue if storage fails
|
|
47
|
+
getKey: (req) => req.headers.get('x-request-id'), // Custom key extraction
|
|
48
|
+
getHash: async (req) => hashObject(await req.json()), // Custom hash
|
|
49
|
+
onHit: (key, response) => {}, // Cache hit callback
|
|
50
|
+
onMiss: (key) => {}, // Cache miss callback
|
|
51
|
+
onError: (key, error) => {}, // Error callback
|
|
52
|
+
})(handler);
|
|
53
|
+
```
|
|
33
54
|
|
|
34
55
|
## Pre-configured Factory
|
|
35
56
|
|
|
57
|
+
Share configuration across routes:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// lib/idempotency.ts
|
|
36
61
|
import { configure } from '@oncely/next';
|
|
37
62
|
import { upstash } from '@oncely/upstash';
|
|
38
63
|
|
|
39
64
|
export const idempotent = configure({
|
|
40
|
-
storage: upstash(),
|
|
41
|
-
ttl: '1h',
|
|
65
|
+
storage: upstash(),
|
|
66
|
+
ttl: '1h',
|
|
42
67
|
});
|
|
43
68
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## Headers
|
|
48
|
-
|
|
49
|
-
Request: Idempotency-Key
|
|
50
|
-
Response: Idempotency-Key, Idempotency-Replay (on cache hit)
|
|
51
|
-
|
|
52
|
-
## License
|
|
53
|
-
|
|
54
|
-
MIT
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const result = await idempotency.run({
|
|
58
|
-
key,
|
|
59
|
-
hash: hashObject(body),
|
|
60
|
-
handler: () => handler(req, ctx, body),
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return result.data;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return handler(req, ctx, body);
|
|
67
|
-
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
````
|
|
72
|
-
|
|
73
|
-
## Pages Router
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// pages/api/orders.ts
|
|
77
|
-
import { oncely } from "oncely-next/pages";
|
|
78
|
-
import { memory } from "oncely";
|
|
69
|
+
// app/api/orders/route.ts
|
|
70
|
+
import { idempotent } from '@/lib/idempotency';
|
|
79
71
|
|
|
80
|
-
export
|
|
81
|
-
|
|
82
|
-
const order = await createOrder(req.body);
|
|
83
|
-
res.status(201).json(order);
|
|
84
|
-
},
|
|
85
|
-
{ storage: memory },
|
|
86
|
-
);
|
|
87
|
-
````
|
|
72
|
+
export const POST = idempotent()(handler);
|
|
73
|
+
```
|
|
88
74
|
|
|
89
|
-
|
|
75
|
+
Pages Router has its own configure:
|
|
90
76
|
|
|
91
77
|
```typescript
|
|
92
78
|
// lib/idempotency.ts
|
|
93
|
-
import { configure } from 'oncely
|
|
94
|
-
import { ioredis } from 'oncely-redis';
|
|
79
|
+
import { configure } from '@oncely/next/pages';
|
|
95
80
|
|
|
96
|
-
export const
|
|
97
|
-
storage:
|
|
98
|
-
ttl: '
|
|
81
|
+
export const idempotent = configure({
|
|
82
|
+
storage: upstash(),
|
|
83
|
+
ttl: '1h',
|
|
99
84
|
});
|
|
100
85
|
|
|
101
86
|
// pages/api/orders.ts
|
|
102
|
-
import {
|
|
87
|
+
import { idempotent } from '@/lib/idempotency';
|
|
103
88
|
|
|
104
|
-
export default
|
|
105
|
-
const order = await createOrder(req.body);
|
|
106
|
-
res.status(201).json(order);
|
|
107
|
-
});
|
|
89
|
+
export default idempotent()(handler);
|
|
108
90
|
```
|
|
109
91
|
|
|
110
|
-
##
|
|
92
|
+
## Headers
|
|
111
93
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
// Optional
|
|
118
|
-
keyHeader: 'Idempotency-Key', // Header name (default: 'Idempotency-Key')
|
|
119
|
-
required: false, // Require key? (default: false)
|
|
120
|
-
responseHeaders: true, // Add idempotency headers to response
|
|
121
|
-
ttl: '24h', // Key expiration
|
|
122
|
-
debug: false, // Enable debug logging
|
|
123
|
-
|
|
124
|
-
// Custom hash function
|
|
125
|
-
hashRequest: async (req) => hashObject(await req.json()),
|
|
126
|
-
|
|
127
|
-
// Callbacks
|
|
128
|
-
onHit: (key) => {},
|
|
129
|
-
onMiss: (key) => {},
|
|
130
|
-
onError: (key, err) => {},
|
|
131
|
-
});
|
|
132
|
-
```
|
|
94
|
+
| Header | Direction | Description |
|
|
95
|
+
| -------------------- | --------- | ------------------------------------- |
|
|
96
|
+
| `Idempotency-Key` | Request | Client-provided unique key |
|
|
97
|
+
| `Idempotency-Key` | Response | Echo of the key used |
|
|
98
|
+
| `Idempotency-Replay` | Response | `true` when returning cached response |
|
|
133
99
|
|
|
134
|
-
##
|
|
100
|
+
## Error Responses
|
|
135
101
|
|
|
136
|
-
|
|
102
|
+
All errors follow [RFC 7807 Problem Details](https://tools.ietf.org/html/rfc7807):
|
|
137
103
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
104
|
+
| Status | Type | Cause |
|
|
105
|
+
| ------ | -------------- | ----------------------------------- |
|
|
106
|
+
| 400 | `key-required` | Missing key when `required: true` |
|
|
107
|
+
| 409 | `conflict` | Request with same key in progress |
|
|
108
|
+
| 422 | `mismatch` | Same key reused with different body |
|
|
109
|
+
|
|
110
|
+
## With Upstash (Serverless)
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { upstash } from '@oncely/upstash';
|
|
114
|
+
|
|
115
|
+
export const POST = next({ storage: upstash() })(handler);
|
|
143
116
|
```
|
|
144
117
|
|
|
145
118
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oncely/next",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Next.js integration for oncely idempotency",
|
|
5
5
|
"author": "stacks0x",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,26 +38,26 @@
|
|
|
38
38
|
"README.md",
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "tsup",
|
|
43
|
-
"dev": "tsup --watch",
|
|
44
|
-
"typecheck": "tsc --noEmit",
|
|
45
|
-
"clean": "rm -rf dist"
|
|
46
|
-
},
|
|
47
41
|
"peerDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
42
|
+
"next": ">=13.0.0",
|
|
43
|
+
"@oncely/core": "0.2.2"
|
|
50
44
|
},
|
|
51
45
|
"devDependencies": {
|
|
52
|
-
"@oncely/core": "workspace:*",
|
|
53
46
|
"next": "^16.1.4",
|
|
54
47
|
"tsup": "^8.0.1",
|
|
55
|
-
"typescript": "^5.3.3"
|
|
48
|
+
"typescript": "^5.3.3",
|
|
49
|
+
"@oncely/core": "0.2.2"
|
|
56
50
|
},
|
|
57
51
|
"publishConfig": {
|
|
58
52
|
"access": "public"
|
|
59
53
|
},
|
|
60
54
|
"engines": {
|
|
61
|
-
"node": ">=
|
|
55
|
+
"node": ">=20.0.0"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"dev": "tsup --watch",
|
|
60
|
+
"typecheck": "tsc --noEmit",
|
|
61
|
+
"clean": "rm -rf dist"
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
}
|