@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 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":[]}
@@ -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 };
@@ -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
+ }