@oncely/upstash 1.0.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/README.md +165 -0
- package/dist/index.cjs +154 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +128 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# @oncely/upstash
|
|
2
|
+
|
|
3
|
+
Upstash storage adapter for oncely (edge/serverless compatible).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
npm install @oncely/core @oncely/upstash
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
import { upstash, UpstashStorage } from '@oncely/upstash';
|
|
12
|
+
import { next } from '@oncely/next';
|
|
13
|
+
|
|
14
|
+
// From environment variables
|
|
15
|
+
const storage = upstash();
|
|
16
|
+
|
|
17
|
+
// With explicit config
|
|
18
|
+
const storage = upstash({
|
|
19
|
+
url: 'https://...',
|
|
20
|
+
token: '...',
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Use with Next.js
|
|
24
|
+
export const POST = next({ storage: upstash() })(handler);
|
|
25
|
+
|
|
26
|
+
## Environment Variables
|
|
27
|
+
|
|
28
|
+
ONCELY_UPSTASH_REST_URL - Upstash REST URL
|
|
29
|
+
ONCELY_UPSTASH_REST_TOKEN - Upstash REST token
|
|
30
|
+
|
|
31
|
+
## License
|
|
32
|
+
|
|
33
|
+
MIT
|
|
34
|
+
await storage.acquire(key, hash, ttl); // AcquireResult
|
|
35
|
+
await storage.save(key, response); // void
|
|
36
|
+
await storage.release(key); // void
|
|
37
|
+
await storage.delete(key); // void
|
|
38
|
+
await storage.clear(); // void
|
|
39
|
+
|
|
40
|
+
````
|
|
41
|
+
|
|
42
|
+
## Usage Examples
|
|
43
|
+
|
|
44
|
+
### Next.js App Router (Edge)
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// app/api/orders/route.ts
|
|
48
|
+
import { oncely } from '@oncely/core';
|
|
49
|
+
import { upstash } from '@oncely/upstash';
|
|
50
|
+
|
|
51
|
+
export const runtime = 'edge';
|
|
52
|
+
|
|
53
|
+
const handler = oncely.handler({
|
|
54
|
+
storage: upstash(),
|
|
55
|
+
ttl: 60000,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export async function POST(request: Request) {
|
|
59
|
+
return handler(request, async (req) => {
|
|
60
|
+
const body = await req.json();
|
|
61
|
+
const order = await createOrder(body);
|
|
62
|
+
|
|
63
|
+
return Response.json(order, { status: 201 });
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
````
|
|
67
|
+
|
|
68
|
+
### Vercel Serverless Function
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// api/checkout.ts
|
|
72
|
+
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
73
|
+
import { oncely } from '@oncely/core';
|
|
74
|
+
import { upstash } from '@oncely/upstash';
|
|
75
|
+
|
|
76
|
+
const storage = upstash();
|
|
77
|
+
|
|
78
|
+
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
79
|
+
const idempotencyKey = req.headers['idempotency-key'] as string;
|
|
80
|
+
|
|
81
|
+
if (!idempotencyKey) {
|
|
82
|
+
return res.status(400).json({ error: 'Idempotency-Key required' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const result = await storage.acquire(idempotencyKey, null, 60000);
|
|
86
|
+
|
|
87
|
+
if (result.status === 'hit') {
|
|
88
|
+
res.setHeader('Idempotency-Replay', 'true');
|
|
89
|
+
return res.json(result.response.data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (result.status === 'conflict') {
|
|
93
|
+
return res.status(409).json({ error: 'Request in progress' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const data = await processCheckout(req.body);
|
|
98
|
+
await storage.save(idempotencyKey, {
|
|
99
|
+
data,
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
hash: null,
|
|
102
|
+
});
|
|
103
|
+
return res.json(data);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
await storage.release(idempotencyKey);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Cloudflare Workers
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { oncely } from '@oncely/core';
|
|
115
|
+
import { upstash } from '@oncely/upstash';
|
|
116
|
+
|
|
117
|
+
export interface Env {
|
|
118
|
+
ONCELY_UPSTASH_REST_URL: string;
|
|
119
|
+
ONCELY_UPSTASH_REST_TOKEN: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default {
|
|
123
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
124
|
+
const storage = upstash({
|
|
125
|
+
url: env.ONCELY_UPSTASH_REST_URL,
|
|
126
|
+
token: env.ONCELY_UPSTASH_REST_TOKEN,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const handler = oncely.handler({
|
|
130
|
+
storage,
|
|
131
|
+
ttl: 60000,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return handler(request, async (req) => {
|
|
135
|
+
// Your handler logic
|
|
136
|
+
return new Response(JSON.stringify({ ok: true }));
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### With Regional Endpoints
|
|
143
|
+
|
|
144
|
+
For lower latency, use Upstash regional endpoints:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
const storage = upstash({
|
|
148
|
+
url: process.env.ONCELY_UPSTASH_REST_URL_US_EAST_1,
|
|
149
|
+
token: process.env.ONCELY_UPSTASH_REST_TOKEN,
|
|
150
|
+
});
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Why Upstash?
|
|
154
|
+
|
|
155
|
+
| Feature | Upstash | Self-hosted Redis |
|
|
156
|
+
| ------------------ | --------------- | ---------------------- |
|
|
157
|
+
| Edge/Serverless | ✅ HTTP-based | ❌ Requires TCP |
|
|
158
|
+
| Connection pooling | ✅ Not needed | ⚠️ Required |
|
|
159
|
+
| Cold start | ✅ Instant | ⚠️ Connection overhead |
|
|
160
|
+
| Scaling | ✅ Auto | Manual |
|
|
161
|
+
| Global | ✅ Multi-region | Manual setup |
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
UpstashStorage: () => UpstashStorage,
|
|
24
|
+
upstash: () => upstash
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
var import_redis = require("@upstash/redis");
|
|
28
|
+
var UpstashStorage = class {
|
|
29
|
+
client;
|
|
30
|
+
keyPrefix;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.client = options._client;
|
|
33
|
+
this.keyPrefix = options.keyPrefix;
|
|
34
|
+
}
|
|
35
|
+
key(id) {
|
|
36
|
+
return `${this.keyPrefix}${id}`;
|
|
37
|
+
}
|
|
38
|
+
async acquire(key, hash, ttlMs) {
|
|
39
|
+
const fullKey = this.key(key);
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
const record = {
|
|
42
|
+
status: "in_progress",
|
|
43
|
+
hash,
|
|
44
|
+
startedAt: now
|
|
45
|
+
};
|
|
46
|
+
const acquired = await this.client.set(fullKey, record, {
|
|
47
|
+
nx: true,
|
|
48
|
+
px: ttlMs
|
|
49
|
+
});
|
|
50
|
+
if (acquired === "OK") {
|
|
51
|
+
return { status: "acquired" };
|
|
52
|
+
}
|
|
53
|
+
const existing = await this.client.get(fullKey);
|
|
54
|
+
if (!existing) {
|
|
55
|
+
const retryAcquired = await this.client.set(fullKey, record, {
|
|
56
|
+
nx: true,
|
|
57
|
+
px: ttlMs
|
|
58
|
+
});
|
|
59
|
+
return retryAcquired === "OK" ? { status: "acquired" } : { status: "conflict", startedAt: now };
|
|
60
|
+
}
|
|
61
|
+
if (typeof existing !== "object" || !existing.status) {
|
|
62
|
+
await this.client.del(fullKey);
|
|
63
|
+
const retryAcquired = await this.client.set(fullKey, record, {
|
|
64
|
+
nx: true,
|
|
65
|
+
px: ttlMs
|
|
66
|
+
});
|
|
67
|
+
return retryAcquired === "OK" ? { status: "acquired" } : { status: "conflict", startedAt: now };
|
|
68
|
+
}
|
|
69
|
+
if (existing.status === "completed") {
|
|
70
|
+
if (hash !== null && existing.hash !== null && hash !== existing.hash) {
|
|
71
|
+
return {
|
|
72
|
+
status: "mismatch",
|
|
73
|
+
existingHash: existing.hash,
|
|
74
|
+
providedHash: hash
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
status: "hit",
|
|
79
|
+
response: {
|
|
80
|
+
data: existing.data,
|
|
81
|
+
createdAt: existing.createdAt ?? existing.startedAt,
|
|
82
|
+
hash: existing.hash
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
if (hash !== null && existing.hash !== null && hash !== existing.hash) {
|
|
87
|
+
return {
|
|
88
|
+
status: "mismatch",
|
|
89
|
+
existingHash: existing.hash,
|
|
90
|
+
providedHash: hash
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return { status: "conflict", startedAt: existing.startedAt };
|
|
94
|
+
}
|
|
95
|
+
async save(key, response) {
|
|
96
|
+
const fullKey = this.key(key);
|
|
97
|
+
const existing = await this.client.get(fullKey);
|
|
98
|
+
if (!existing) return;
|
|
99
|
+
const ttl = await this.client.pttl(fullKey);
|
|
100
|
+
if (ttl <= 0) return;
|
|
101
|
+
const record = {
|
|
102
|
+
status: "completed",
|
|
103
|
+
hash: response.hash,
|
|
104
|
+
data: response.data,
|
|
105
|
+
createdAt: response.createdAt,
|
|
106
|
+
startedAt: existing.startedAt ?? Date.now()
|
|
107
|
+
};
|
|
108
|
+
await this.client.set(fullKey, record, { px: ttl });
|
|
109
|
+
}
|
|
110
|
+
async release(key) {
|
|
111
|
+
await this.client.del(this.key(key));
|
|
112
|
+
}
|
|
113
|
+
async delete(key) {
|
|
114
|
+
await this.client.del(this.key(key));
|
|
115
|
+
}
|
|
116
|
+
async clear() {
|
|
117
|
+
const keys = await this.client.keys(`${this.keyPrefix}*`);
|
|
118
|
+
if (keys.length > 0) {
|
|
119
|
+
await this.client.del(...keys);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function upstash(options) {
|
|
124
|
+
const keyPrefix = options?.keyPrefix ?? "oncely:";
|
|
125
|
+
if (options?.client) {
|
|
126
|
+
return new UpstashStorage({
|
|
127
|
+
keyPrefix,
|
|
128
|
+
_client: options.client
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const url = options?.url ?? process.env.ONCELY_UPSTASH_REST_URL;
|
|
132
|
+
const token = options?.token ?? process.env.ONCELY_UPSTASH_REST_TOKEN;
|
|
133
|
+
if (!url) {
|
|
134
|
+
throw new Error(
|
|
135
|
+
"Upstash URL not provided. Set ONCELY_UPSTASH_REST_URL environment variable or pass url option."
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!token) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"Upstash token not provided. Set ONCELY_UPSTASH_REST_TOKEN environment variable or pass token option."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const client = new import_redis.Redis({ url, token });
|
|
144
|
+
return new UpstashStorage({
|
|
145
|
+
keyPrefix,
|
|
146
|
+
_client: client
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
150
|
+
0 && (module.exports = {
|
|
151
|
+
UpstashStorage,
|
|
152
|
+
upstash
|
|
153
|
+
});
|
|
154
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @oncely/upstash - Upstash Redis adapter for oncely idempotency\n *\n * Built for serverless and edge environments - HTTP-based, no connections required.\n *\n * @example\n * ```typescript\n * import { upstash } from '@oncely/upstash';\n *\n * // Auto-discover from ONCELY_UPSTASH_REST_URL and ONCELY_UPSTASH_REST_TOKEN\n * const storage = upstash();\n *\n * // Or configure explicitly\n * const storage = upstash({\n * url: 'https://your-redis.upstash.io',\n * token: 'your-token',\n * });\n * ```\n */\n\nimport type { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';\nimport { Redis } from '@upstash/redis';\n\n/**\n * Internal record structure stored in Upstash\n */\ninterface StoredRecord {\n status: 'in_progress' | 'completed';\n hash: string | null;\n data?: unknown;\n createdAt?: number;\n startedAt: number;\n}\n\n/**\n * Options for creating an Upstash storage adapter\n */\nexport interface UpstashOptions {\n /** Upstash REST URL (defaults to ONCELY_UPSTASH_REST_URL) */\n url?: string;\n /** Upstash REST token (defaults to ONCELY_UPSTASH_REST_TOKEN) */\n token?: string;\n /** Existing @upstash/redis client instance */\n client?: Redis;\n /** Key prefix for all stored data (default: 'oncely:') */\n keyPrefix?: string;\n}\n\n/**\n * Upstash Redis storage adapter for oncely idempotency.\n *\n * Implements the StorageAdapter interface using Upstash's HTTP-based Redis client,\n * making it ideal for serverless and edge environments.\n */\nexport class UpstashStorage implements StorageAdapter {\n private readonly client: Redis;\n private readonly keyPrefix: string;\n\n constructor(options: { keyPrefix: string; _client: Redis }) {\n this.client = options._client;\n this.keyPrefix = options.keyPrefix;\n }\n\n private key(id: string): string {\n return `${this.keyPrefix}${id}`;\n }\n\n async acquire(key: string, hash: string | null, ttlMs: number): Promise<AcquireResult> {\n const fullKey = this.key(key);\n const now = Date.now();\n\n // Atomic acquire: Try SET NX first (only set if not exists)\n // This prevents race conditions where two requests both think they acquired\n const record: StoredRecord = {\n status: 'in_progress',\n hash,\n startedAt: now,\n };\n\n const acquired = await this.client.set(fullKey, record, {\n nx: true,\n px: ttlMs,\n });\n\n // If we acquired the lock, we're done\n if (acquired === 'OK') {\n return { status: 'acquired' };\n }\n\n // Lock not acquired - key exists. Now fetch to determine why.\n const existing = await this.client.get<StoredRecord>(fullKey);\n\n // Key expired or was deleted between SET NX and GET - try again\n if (!existing) {\n const retryAcquired = await this.client.set(fullKey, record, {\n nx: true,\n px: ttlMs,\n });\n return retryAcquired === 'OK'\n ? { status: 'acquired' }\n : { status: 'conflict', startedAt: now };\n }\n\n // Validate it's a proper record\n if (typeof existing !== 'object' || !existing.status) {\n // Corrupted data, delete and retry\n await this.client.del(fullKey);\n const retryAcquired = await this.client.set(fullKey, record, {\n nx: true,\n px: ttlMs,\n });\n return retryAcquired === 'OK'\n ? { status: 'acquired' }\n : { status: 'conflict', startedAt: now };\n }\n\n // Check for completed response (cache hit)\n if (existing.status === 'completed') {\n // Check hash mismatch\n if (hash !== null && existing.hash !== null && hash !== existing.hash) {\n return {\n status: 'mismatch',\n existingHash: existing.hash,\n providedHash: hash,\n };\n }\n\n // Return cached response\n return {\n status: 'hit',\n response: {\n data: existing.data,\n createdAt: existing.createdAt ?? existing.startedAt,\n hash: existing.hash,\n },\n };\n }\n\n // Status is 'in_progress' - another request is processing\n // Check hash mismatch even for in-progress requests\n if (hash !== null && existing.hash !== null && hash !== existing.hash) {\n return {\n status: 'mismatch',\n existingHash: existing.hash,\n providedHash: hash,\n };\n }\n\n return { status: 'conflict', startedAt: existing.startedAt };\n }\n\n async save(key: string, response: StoredResponse): Promise<void> {\n const fullKey = this.key(key);\n\n // Get existing record to preserve TTL\n const existing = await this.client.get<StoredRecord>(fullKey);\n if (!existing) return;\n\n // Get remaining TTL\n const ttl = await this.client.pttl(fullKey);\n if (ttl <= 0) return;\n\n const record: StoredRecord = {\n status: 'completed',\n hash: response.hash,\n data: response.data,\n createdAt: response.createdAt,\n startedAt: existing.startedAt ?? Date.now(),\n };\n\n await this.client.set(fullKey, record, { px: ttl });\n }\n\n async release(key: string): Promise<void> {\n await this.client.del(this.key(key));\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del(this.key(key));\n }\n\n async clear(): Promise<void> {\n // Note: KEYS is not recommended for production with large datasets\n // Consider using SCAN in production environments\n const keys = await this.client.keys(`${this.keyPrefix}*`);\n if (keys.length > 0) {\n await this.client.del(...keys);\n }\n }\n}\n\n/**\n * Create an Upstash storage adapter.\n *\n * @param options - Configuration options or URL string\n * @returns UpstashStorage instance\n *\n * @example\n * ```typescript\n * // Auto-discover from environment\n * const storage = upstash();\n *\n * // With explicit config\n * const storage = upstash({\n * url: 'https://your-redis.upstash.io',\n * token: 'your-token',\n * });\n *\n * // With existing client\n * const storage = upstash({ client: existingClient });\n * ```\n */\nexport function upstash(options?: UpstashOptions): UpstashStorage {\n const keyPrefix = options?.keyPrefix ?? 'oncely:';\n\n if (options?.client) {\n return new UpstashStorage({\n keyPrefix,\n _client: options.client,\n });\n }\n\n // Get URL and token from options or environment\n const url = options?.url ?? process.env.ONCELY_UPSTASH_REST_URL;\n const token = options?.token ?? process.env.ONCELY_UPSTASH_REST_TOKEN;\n\n if (!url) {\n throw new Error(\n 'Upstash URL not provided. Set ONCELY_UPSTASH_REST_URL environment variable or pass url option.'\n );\n }\n\n if (!token) {\n throw new Error(\n 'Upstash token not provided. Set ONCELY_UPSTASH_REST_TOKEN environment variable or pass token option.'\n );\n }\n\n const client = new Redis({ url, token });\n\n return new UpstashStorage({\n keyPrefix,\n _client: client,\n });\n}\n\n// Re-export types for consumers\nexport type { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';
|
|
2
|
+
export { AcquireResult, StorageAdapter, StoredResponse } from '@oncely/core';
|
|
3
|
+
import { Redis } from '@upstash/redis';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @oncely/upstash - Upstash Redis adapter for oncely idempotency
|
|
7
|
+
*
|
|
8
|
+
* Built for serverless and edge environments - HTTP-based, no connections required.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { upstash } from '@oncely/upstash';
|
|
13
|
+
*
|
|
14
|
+
* // Auto-discover from ONCELY_UPSTASH_REST_URL and ONCELY_UPSTASH_REST_TOKEN
|
|
15
|
+
* const storage = upstash();
|
|
16
|
+
*
|
|
17
|
+
* // Or configure explicitly
|
|
18
|
+
* const storage = upstash({
|
|
19
|
+
* url: 'https://your-redis.upstash.io',
|
|
20
|
+
* token: 'your-token',
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for creating an Upstash storage adapter
|
|
27
|
+
*/
|
|
28
|
+
interface UpstashOptions {
|
|
29
|
+
/** Upstash REST URL (defaults to ONCELY_UPSTASH_REST_URL) */
|
|
30
|
+
url?: string;
|
|
31
|
+
/** Upstash REST token (defaults to ONCELY_UPSTASH_REST_TOKEN) */
|
|
32
|
+
token?: string;
|
|
33
|
+
/** Existing @upstash/redis client instance */
|
|
34
|
+
client?: Redis;
|
|
35
|
+
/** Key prefix for all stored data (default: 'oncely:') */
|
|
36
|
+
keyPrefix?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Upstash Redis storage adapter for oncely idempotency.
|
|
40
|
+
*
|
|
41
|
+
* Implements the StorageAdapter interface using Upstash's HTTP-based Redis client,
|
|
42
|
+
* making it ideal for serverless and edge environments.
|
|
43
|
+
*/
|
|
44
|
+
declare class UpstashStorage implements StorageAdapter {
|
|
45
|
+
private readonly client;
|
|
46
|
+
private readonly keyPrefix;
|
|
47
|
+
constructor(options: {
|
|
48
|
+
keyPrefix: string;
|
|
49
|
+
_client: Redis;
|
|
50
|
+
});
|
|
51
|
+
private key;
|
|
52
|
+
acquire(key: string, hash: string | null, ttlMs: number): Promise<AcquireResult>;
|
|
53
|
+
save(key: string, response: StoredResponse): Promise<void>;
|
|
54
|
+
release(key: string): Promise<void>;
|
|
55
|
+
delete(key: string): Promise<void>;
|
|
56
|
+
clear(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an Upstash storage adapter.
|
|
60
|
+
*
|
|
61
|
+
* @param options - Configuration options or URL string
|
|
62
|
+
* @returns UpstashStorage instance
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // Auto-discover from environment
|
|
67
|
+
* const storage = upstash();
|
|
68
|
+
*
|
|
69
|
+
* // With explicit config
|
|
70
|
+
* const storage = upstash({
|
|
71
|
+
* url: 'https://your-redis.upstash.io',
|
|
72
|
+
* token: 'your-token',
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* // With existing client
|
|
76
|
+
* const storage = upstash({ client: existingClient });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare function upstash(options?: UpstashOptions): UpstashStorage;
|
|
80
|
+
|
|
81
|
+
export { type UpstashOptions, UpstashStorage, upstash };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';
|
|
2
|
+
export { AcquireResult, StorageAdapter, StoredResponse } from '@oncely/core';
|
|
3
|
+
import { Redis } from '@upstash/redis';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @oncely/upstash - Upstash Redis adapter for oncely idempotency
|
|
7
|
+
*
|
|
8
|
+
* Built for serverless and edge environments - HTTP-based, no connections required.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { upstash } from '@oncely/upstash';
|
|
13
|
+
*
|
|
14
|
+
* // Auto-discover from ONCELY_UPSTASH_REST_URL and ONCELY_UPSTASH_REST_TOKEN
|
|
15
|
+
* const storage = upstash();
|
|
16
|
+
*
|
|
17
|
+
* // Or configure explicitly
|
|
18
|
+
* const storage = upstash({
|
|
19
|
+
* url: 'https://your-redis.upstash.io',
|
|
20
|
+
* token: 'your-token',
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for creating an Upstash storage adapter
|
|
27
|
+
*/
|
|
28
|
+
interface UpstashOptions {
|
|
29
|
+
/** Upstash REST URL (defaults to ONCELY_UPSTASH_REST_URL) */
|
|
30
|
+
url?: string;
|
|
31
|
+
/** Upstash REST token (defaults to ONCELY_UPSTASH_REST_TOKEN) */
|
|
32
|
+
token?: string;
|
|
33
|
+
/** Existing @upstash/redis client instance */
|
|
34
|
+
client?: Redis;
|
|
35
|
+
/** Key prefix for all stored data (default: 'oncely:') */
|
|
36
|
+
keyPrefix?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Upstash Redis storage adapter for oncely idempotency.
|
|
40
|
+
*
|
|
41
|
+
* Implements the StorageAdapter interface using Upstash's HTTP-based Redis client,
|
|
42
|
+
* making it ideal for serverless and edge environments.
|
|
43
|
+
*/
|
|
44
|
+
declare class UpstashStorage implements StorageAdapter {
|
|
45
|
+
private readonly client;
|
|
46
|
+
private readonly keyPrefix;
|
|
47
|
+
constructor(options: {
|
|
48
|
+
keyPrefix: string;
|
|
49
|
+
_client: Redis;
|
|
50
|
+
});
|
|
51
|
+
private key;
|
|
52
|
+
acquire(key: string, hash: string | null, ttlMs: number): Promise<AcquireResult>;
|
|
53
|
+
save(key: string, response: StoredResponse): Promise<void>;
|
|
54
|
+
release(key: string): Promise<void>;
|
|
55
|
+
delete(key: string): Promise<void>;
|
|
56
|
+
clear(): Promise<void>;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Create an Upstash storage adapter.
|
|
60
|
+
*
|
|
61
|
+
* @param options - Configuration options or URL string
|
|
62
|
+
* @returns UpstashStorage instance
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```typescript
|
|
66
|
+
* // Auto-discover from environment
|
|
67
|
+
* const storage = upstash();
|
|
68
|
+
*
|
|
69
|
+
* // With explicit config
|
|
70
|
+
* const storage = upstash({
|
|
71
|
+
* url: 'https://your-redis.upstash.io',
|
|
72
|
+
* token: 'your-token',
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* // With existing client
|
|
76
|
+
* const storage = upstash({ client: existingClient });
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
declare function upstash(options?: UpstashOptions): UpstashStorage;
|
|
80
|
+
|
|
81
|
+
export { type UpstashOptions, UpstashStorage, upstash };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Redis } from "@upstash/redis";
|
|
3
|
+
var UpstashStorage = class {
|
|
4
|
+
client;
|
|
5
|
+
keyPrefix;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.client = options._client;
|
|
8
|
+
this.keyPrefix = options.keyPrefix;
|
|
9
|
+
}
|
|
10
|
+
key(id) {
|
|
11
|
+
return `${this.keyPrefix}${id}`;
|
|
12
|
+
}
|
|
13
|
+
async acquire(key, hash, ttlMs) {
|
|
14
|
+
const fullKey = this.key(key);
|
|
15
|
+
const now = Date.now();
|
|
16
|
+
const record = {
|
|
17
|
+
status: "in_progress",
|
|
18
|
+
hash,
|
|
19
|
+
startedAt: now
|
|
20
|
+
};
|
|
21
|
+
const acquired = await this.client.set(fullKey, record, {
|
|
22
|
+
nx: true,
|
|
23
|
+
px: ttlMs
|
|
24
|
+
});
|
|
25
|
+
if (acquired === "OK") {
|
|
26
|
+
return { status: "acquired" };
|
|
27
|
+
}
|
|
28
|
+
const existing = await this.client.get(fullKey);
|
|
29
|
+
if (!existing) {
|
|
30
|
+
const retryAcquired = await this.client.set(fullKey, record, {
|
|
31
|
+
nx: true,
|
|
32
|
+
px: ttlMs
|
|
33
|
+
});
|
|
34
|
+
return retryAcquired === "OK" ? { status: "acquired" } : { status: "conflict", startedAt: now };
|
|
35
|
+
}
|
|
36
|
+
if (typeof existing !== "object" || !existing.status) {
|
|
37
|
+
await this.client.del(fullKey);
|
|
38
|
+
const retryAcquired = await this.client.set(fullKey, record, {
|
|
39
|
+
nx: true,
|
|
40
|
+
px: ttlMs
|
|
41
|
+
});
|
|
42
|
+
return retryAcquired === "OK" ? { status: "acquired" } : { status: "conflict", startedAt: now };
|
|
43
|
+
}
|
|
44
|
+
if (existing.status === "completed") {
|
|
45
|
+
if (hash !== null && existing.hash !== null && hash !== existing.hash) {
|
|
46
|
+
return {
|
|
47
|
+
status: "mismatch",
|
|
48
|
+
existingHash: existing.hash,
|
|
49
|
+
providedHash: hash
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
status: "hit",
|
|
54
|
+
response: {
|
|
55
|
+
data: existing.data,
|
|
56
|
+
createdAt: existing.createdAt ?? existing.startedAt,
|
|
57
|
+
hash: existing.hash
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (hash !== null && existing.hash !== null && hash !== existing.hash) {
|
|
62
|
+
return {
|
|
63
|
+
status: "mismatch",
|
|
64
|
+
existingHash: existing.hash,
|
|
65
|
+
providedHash: hash
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return { status: "conflict", startedAt: existing.startedAt };
|
|
69
|
+
}
|
|
70
|
+
async save(key, response) {
|
|
71
|
+
const fullKey = this.key(key);
|
|
72
|
+
const existing = await this.client.get(fullKey);
|
|
73
|
+
if (!existing) return;
|
|
74
|
+
const ttl = await this.client.pttl(fullKey);
|
|
75
|
+
if (ttl <= 0) return;
|
|
76
|
+
const record = {
|
|
77
|
+
status: "completed",
|
|
78
|
+
hash: response.hash,
|
|
79
|
+
data: response.data,
|
|
80
|
+
createdAt: response.createdAt,
|
|
81
|
+
startedAt: existing.startedAt ?? Date.now()
|
|
82
|
+
};
|
|
83
|
+
await this.client.set(fullKey, record, { px: ttl });
|
|
84
|
+
}
|
|
85
|
+
async release(key) {
|
|
86
|
+
await this.client.del(this.key(key));
|
|
87
|
+
}
|
|
88
|
+
async delete(key) {
|
|
89
|
+
await this.client.del(this.key(key));
|
|
90
|
+
}
|
|
91
|
+
async clear() {
|
|
92
|
+
const keys = await this.client.keys(`${this.keyPrefix}*`);
|
|
93
|
+
if (keys.length > 0) {
|
|
94
|
+
await this.client.del(...keys);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
function upstash(options) {
|
|
99
|
+
const keyPrefix = options?.keyPrefix ?? "oncely:";
|
|
100
|
+
if (options?.client) {
|
|
101
|
+
return new UpstashStorage({
|
|
102
|
+
keyPrefix,
|
|
103
|
+
_client: options.client
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const url = options?.url ?? process.env.ONCELY_UPSTASH_REST_URL;
|
|
107
|
+
const token = options?.token ?? process.env.ONCELY_UPSTASH_REST_TOKEN;
|
|
108
|
+
if (!url) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
"Upstash URL not provided. Set ONCELY_UPSTASH_REST_URL environment variable or pass url option."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (!token) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
"Upstash token not provided. Set ONCELY_UPSTASH_REST_TOKEN environment variable or pass token option."
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
const client = new Redis({ url, token });
|
|
119
|
+
return new UpstashStorage({
|
|
120
|
+
keyPrefix,
|
|
121
|
+
_client: client
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
UpstashStorage,
|
|
126
|
+
upstash
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * @oncely/upstash - Upstash Redis adapter for oncely idempotency\n *\n * Built for serverless and edge environments - HTTP-based, no connections required.\n *\n * @example\n * ```typescript\n * import { upstash } from '@oncely/upstash';\n *\n * // Auto-discover from ONCELY_UPSTASH_REST_URL and ONCELY_UPSTASH_REST_TOKEN\n * const storage = upstash();\n *\n * // Or configure explicitly\n * const storage = upstash({\n * url: 'https://your-redis.upstash.io',\n * token: 'your-token',\n * });\n * ```\n */\n\nimport type { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';\nimport { Redis } from '@upstash/redis';\n\n/**\n * Internal record structure stored in Upstash\n */\ninterface StoredRecord {\n status: 'in_progress' | 'completed';\n hash: string | null;\n data?: unknown;\n createdAt?: number;\n startedAt: number;\n}\n\n/**\n * Options for creating an Upstash storage adapter\n */\nexport interface UpstashOptions {\n /** Upstash REST URL (defaults to ONCELY_UPSTASH_REST_URL) */\n url?: string;\n /** Upstash REST token (defaults to ONCELY_UPSTASH_REST_TOKEN) */\n token?: string;\n /** Existing @upstash/redis client instance */\n client?: Redis;\n /** Key prefix for all stored data (default: 'oncely:') */\n keyPrefix?: string;\n}\n\n/**\n * Upstash Redis storage adapter for oncely idempotency.\n *\n * Implements the StorageAdapter interface using Upstash's HTTP-based Redis client,\n * making it ideal for serverless and edge environments.\n */\nexport class UpstashStorage implements StorageAdapter {\n private readonly client: Redis;\n private readonly keyPrefix: string;\n\n constructor(options: { keyPrefix: string; _client: Redis }) {\n this.client = options._client;\n this.keyPrefix = options.keyPrefix;\n }\n\n private key(id: string): string {\n return `${this.keyPrefix}${id}`;\n }\n\n async acquire(key: string, hash: string | null, ttlMs: number): Promise<AcquireResult> {\n const fullKey = this.key(key);\n const now = Date.now();\n\n // Atomic acquire: Try SET NX first (only set if not exists)\n // This prevents race conditions where two requests both think they acquired\n const record: StoredRecord = {\n status: 'in_progress',\n hash,\n startedAt: now,\n };\n\n const acquired = await this.client.set(fullKey, record, {\n nx: true,\n px: ttlMs,\n });\n\n // If we acquired the lock, we're done\n if (acquired === 'OK') {\n return { status: 'acquired' };\n }\n\n // Lock not acquired - key exists. Now fetch to determine why.\n const existing = await this.client.get<StoredRecord>(fullKey);\n\n // Key expired or was deleted between SET NX and GET - try again\n if (!existing) {\n const retryAcquired = await this.client.set(fullKey, record, {\n nx: true,\n px: ttlMs,\n });\n return retryAcquired === 'OK'\n ? { status: 'acquired' }\n : { status: 'conflict', startedAt: now };\n }\n\n // Validate it's a proper record\n if (typeof existing !== 'object' || !existing.status) {\n // Corrupted data, delete and retry\n await this.client.del(fullKey);\n const retryAcquired = await this.client.set(fullKey, record, {\n nx: true,\n px: ttlMs,\n });\n return retryAcquired === 'OK'\n ? { status: 'acquired' }\n : { status: 'conflict', startedAt: now };\n }\n\n // Check for completed response (cache hit)\n if (existing.status === 'completed') {\n // Check hash mismatch\n if (hash !== null && existing.hash !== null && hash !== existing.hash) {\n return {\n status: 'mismatch',\n existingHash: existing.hash,\n providedHash: hash,\n };\n }\n\n // Return cached response\n return {\n status: 'hit',\n response: {\n data: existing.data,\n createdAt: existing.createdAt ?? existing.startedAt,\n hash: existing.hash,\n },\n };\n }\n\n // Status is 'in_progress' - another request is processing\n // Check hash mismatch even for in-progress requests\n if (hash !== null && existing.hash !== null && hash !== existing.hash) {\n return {\n status: 'mismatch',\n existingHash: existing.hash,\n providedHash: hash,\n };\n }\n\n return { status: 'conflict', startedAt: existing.startedAt };\n }\n\n async save(key: string, response: StoredResponse): Promise<void> {\n const fullKey = this.key(key);\n\n // Get existing record to preserve TTL\n const existing = await this.client.get<StoredRecord>(fullKey);\n if (!existing) return;\n\n // Get remaining TTL\n const ttl = await this.client.pttl(fullKey);\n if (ttl <= 0) return;\n\n const record: StoredRecord = {\n status: 'completed',\n hash: response.hash,\n data: response.data,\n createdAt: response.createdAt,\n startedAt: existing.startedAt ?? Date.now(),\n };\n\n await this.client.set(fullKey, record, { px: ttl });\n }\n\n async release(key: string): Promise<void> {\n await this.client.del(this.key(key));\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del(this.key(key));\n }\n\n async clear(): Promise<void> {\n // Note: KEYS is not recommended for production with large datasets\n // Consider using SCAN in production environments\n const keys = await this.client.keys(`${this.keyPrefix}*`);\n if (keys.length > 0) {\n await this.client.del(...keys);\n }\n }\n}\n\n/**\n * Create an Upstash storage adapter.\n *\n * @param options - Configuration options or URL string\n * @returns UpstashStorage instance\n *\n * @example\n * ```typescript\n * // Auto-discover from environment\n * const storage = upstash();\n *\n * // With explicit config\n * const storage = upstash({\n * url: 'https://your-redis.upstash.io',\n * token: 'your-token',\n * });\n *\n * // With existing client\n * const storage = upstash({ client: existingClient });\n * ```\n */\nexport function upstash(options?: UpstashOptions): UpstashStorage {\n const keyPrefix = options?.keyPrefix ?? 'oncely:';\n\n if (options?.client) {\n return new UpstashStorage({\n keyPrefix,\n _client: options.client,\n });\n }\n\n // Get URL and token from options or environment\n const url = options?.url ?? process.env.ONCELY_UPSTASH_REST_URL;\n const token = options?.token ?? process.env.ONCELY_UPSTASH_REST_TOKEN;\n\n if (!url) {\n throw new Error(\n 'Upstash URL not provided. Set ONCELY_UPSTASH_REST_URL environment variable or pass url option.'\n );\n }\n\n if (!token) {\n throw new Error(\n 'Upstash token not provided. Set ONCELY_UPSTASH_REST_TOKEN environment variable or pass token option.'\n );\n }\n\n const client = new Redis({ url, token });\n\n return new UpstashStorage({\n keyPrefix,\n _client: client,\n });\n}\n\n// Re-export types for consumers\nexport type { StorageAdapter, AcquireResult, StoredResponse } from '@oncely/core';\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oncely/upstash",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Upstash Redis adapter for oncely idempotency",
|
|
5
|
+
"author": "stacks0x",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/stacks0x/oncely.git",
|
|
10
|
+
"directory": "packages/upstash"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"oncely",
|
|
14
|
+
"idempotency",
|
|
15
|
+
"idempotent",
|
|
16
|
+
"upstash",
|
|
17
|
+
"redis",
|
|
18
|
+
"serverless",
|
|
19
|
+
"edge",
|
|
20
|
+
"vercel"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"require": "./dist/index.cjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup",
|
|
40
|
+
"dev": "tsup --watch",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"clean": "rm -rf dist"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@oncely/core": "workspace:*",
|
|
46
|
+
"@upstash/redis": "^1.28.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@oncely/core": "workspace:*",
|
|
50
|
+
"@upstash/redis": "^1.28.0",
|
|
51
|
+
"tsup": "^8.0.1",
|
|
52
|
+
"typescript": "^5.3.3"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=18.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|