@oncely/next 1.0.1 → 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/README.md +68 -95
- package/package.json +4 -4
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",
|
|
@@ -40,19 +40,19 @@
|
|
|
40
40
|
],
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"next": ">=13.0.0",
|
|
43
|
-
"@oncely/core": "0.2.
|
|
43
|
+
"@oncely/core": "0.2.2"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"next": "^16.1.4",
|
|
47
47
|
"tsup": "^8.0.1",
|
|
48
48
|
"typescript": "^5.3.3",
|
|
49
|
-
"@oncely/core": "0.2.
|
|
49
|
+
"@oncely/core": "0.2.2"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|
|
55
|
-
"node": ">=
|
|
55
|
+
"node": ">=20.0.0"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|