@farcaster/snap-upstash 0.0.0-canary-20260402031100

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.
@@ -0,0 +1,10 @@
1
+ import { type SnapFunction } from "@farcaster/snap";
2
+ export type WithUpstashOptions = {
3
+ lockAcquireTimeoutMs?: number;
4
+ lockLeaseDurationMs?: number;
5
+ };
6
+ /**
7
+ * Wraps a SnapFunction and injects an Upstash Redis-backed data store into
8
+ * the context.
9
+ */
10
+ export declare function withUpstash(snapFn: SnapFunction, options?: WithUpstashOptions): SnapFunction;
package/dist/index.js ADDED
@@ -0,0 +1,71 @@
1
+ import { Redis } from "@upstash/redis";
2
+ import { Lock } from "@upstash/lock";
3
+ const DEFAULT_LOCK_ACQUIRE_TIMEOUT_MS = 5000;
4
+ const DEFAULT_LOCK_LEASE_DURATION_MS = 1000;
5
+ const LOCK_KEY = "snap:lock";
6
+ const LOCK_RETRY_DELAY_MS = 50;
7
+ /**
8
+ * Wraps a SnapFunction and injects an Upstash Redis-backed data store into
9
+ * the context.
10
+ */
11
+ export function withUpstash(snapFn, options) {
12
+ const url = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;
13
+ const token = process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN;
14
+ if (!url || !token) {
15
+ console.warn("missing env vars -- skipping Upstash data store setup");
16
+ return snapFn;
17
+ }
18
+ const redis = new Redis({ url, token });
19
+ const acquireTimeoutMs = Math.max(1, options?.lockAcquireTimeoutMs ?? DEFAULT_LOCK_ACQUIRE_TIMEOUT_MS);
20
+ const leaseDurationMs = Math.max(1, options?.lockLeaseDurationMs ?? DEFAULT_LOCK_LEASE_DURATION_MS);
21
+ const ops = {
22
+ async get(key) {
23
+ return redis.get(key);
24
+ },
25
+ async set(key, value) {
26
+ await redis.set(key, value);
27
+ },
28
+ };
29
+ const store = {
30
+ ...ops,
31
+ async withLock(fn) {
32
+ const lock = new Lock({
33
+ id: LOCK_KEY,
34
+ redis,
35
+ lease: leaseDurationMs,
36
+ retry: {
37
+ attempts: Math.ceil(acquireTimeoutMs / LOCK_RETRY_DELAY_MS),
38
+ delay: LOCK_RETRY_DELAY_MS,
39
+ },
40
+ });
41
+ if (!(await lock.acquire())) {
42
+ throw new Error(`snap-upstash: failed to acquire lock within ${acquireTimeoutMs}ms`);
43
+ }
44
+ let fnError;
45
+ try {
46
+ return await fn(ops);
47
+ }
48
+ catch (err) {
49
+ fnError = err;
50
+ throw err;
51
+ }
52
+ finally {
53
+ try {
54
+ await lock.release();
55
+ }
56
+ catch (releaseError) {
57
+ if (!fnError) {
58
+ // If the critical section succeeded but releasing the lock failed,
59
+ // propagate the release error.
60
+ throw releaseError;
61
+ }
62
+ // If both the critical section and lock release failed, log the
63
+ // release error and preserve the original error from fn(ops).
64
+ // eslint-disable-next-line no-console
65
+ console.error("snap-upstash: failed to release lock", releaseError);
66
+ }
67
+ }
68
+ },
69
+ };
70
+ return (ctx) => snapFn({ ...ctx, data: store });
71
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@farcaster/snap-upstash",
3
+ "version": "0.0.0-canary-20260402031100",
4
+ "description": "Upstash Redis data store adapter for Farcaster Snaps",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/farcasterxyz/snap.git",
8
+ "directory": "pkgs/upstash"
9
+ },
10
+ "type": "module",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@upstash/lock": "^0.2.1",
30
+ "@upstash/redis": "^1.37.0",
31
+ "@farcaster/snap": "0.0.0-canary-20260402031100"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^25.5.0",
35
+ "tsc-alias": "^1.8.16",
36
+ "typescript": "^5.4.0",
37
+ "vitest": "^1.6.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsc && tsc-alias --resolve-full-paths --resolve-full-extension .js",
41
+ "clean": "rm -rf dist",
42
+ "test": "vitest run",
43
+ "typecheck": "tsc --noEmit"
44
+ }
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,103 @@
1
+ import { Redis } from "@upstash/redis";
2
+ import { Lock } from "@upstash/lock";
3
+ import {
4
+ type DataStoreValue,
5
+ type SnapDataStore,
6
+ type SnapDataStoreOperations,
7
+ type SnapFunction,
8
+ } from "@farcaster/snap";
9
+
10
+ export type WithUpstashOptions = {
11
+ lockAcquireTimeoutMs?: number;
12
+ lockLeaseDurationMs?: number;
13
+ };
14
+
15
+ const DEFAULT_LOCK_ACQUIRE_TIMEOUT_MS = 5_000;
16
+ const DEFAULT_LOCK_LEASE_DURATION_MS = 1_000;
17
+ const LOCK_KEY = "snap:lock";
18
+ const LOCK_RETRY_DELAY_MS = 50;
19
+
20
+ /**
21
+ * Wraps a SnapFunction and injects an Upstash Redis-backed data store into
22
+ * the context.
23
+ */
24
+ export function withUpstash(
25
+ snapFn: SnapFunction,
26
+ options?: WithUpstashOptions,
27
+ ): SnapFunction {
28
+ const url = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;
29
+ const token =
30
+ process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN;
31
+
32
+ if (!url || !token) {
33
+ console.warn("missing env vars -- skipping Upstash data store setup");
34
+ return snapFn;
35
+ }
36
+
37
+ const redis = new Redis({ url, token });
38
+
39
+ const acquireTimeoutMs = Math.max(
40
+ 1,
41
+ options?.lockAcquireTimeoutMs ?? DEFAULT_LOCK_ACQUIRE_TIMEOUT_MS,
42
+ );
43
+ const leaseDurationMs = Math.max(
44
+ 1,
45
+ options?.lockLeaseDurationMs ?? DEFAULT_LOCK_LEASE_DURATION_MS,
46
+ );
47
+
48
+ const ops: SnapDataStoreOperations = {
49
+ async get(key: string): Promise<DataStoreValue | null> {
50
+ return redis.get<DataStoreValue>(key);
51
+ },
52
+ async set(key: string, value: DataStoreValue): Promise<void> {
53
+ await redis.set(key, value);
54
+ },
55
+ };
56
+
57
+ const store: SnapDataStore = {
58
+ ...ops,
59
+ async withLock<T>(
60
+ fn: (store: SnapDataStoreOperations) => Promise<T>,
61
+ ): Promise<T> {
62
+ const lock = new Lock({
63
+ id: LOCK_KEY,
64
+ redis,
65
+ lease: leaseDurationMs,
66
+ retry: {
67
+ attempts: Math.ceil(acquireTimeoutMs / LOCK_RETRY_DELAY_MS),
68
+ delay: LOCK_RETRY_DELAY_MS,
69
+ },
70
+ });
71
+
72
+ if (!(await lock.acquire())) {
73
+ throw new Error(
74
+ `snap-upstash: failed to acquire lock within ${acquireTimeoutMs}ms`,
75
+ );
76
+ }
77
+
78
+ let fnError: unknown;
79
+ try {
80
+ return await fn(ops);
81
+ } catch (err) {
82
+ fnError = err;
83
+ throw err;
84
+ } finally {
85
+ try {
86
+ await lock.release();
87
+ } catch (releaseError) {
88
+ if (!fnError) {
89
+ // If the critical section succeeded but releasing the lock failed,
90
+ // propagate the release error.
91
+ throw releaseError;
92
+ }
93
+ // If both the critical section and lock release failed, log the
94
+ // release error and preserve the original error from fn(ops).
95
+ // eslint-disable-next-line no-console
96
+ console.error("snap-upstash: failed to release lock", releaseError);
97
+ }
98
+ }
99
+ },
100
+ };
101
+
102
+ return (ctx) => snapFn({ ...ctx, data: store });
103
+ }