forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import type { RuntimeLock } from '@atproto/oauth-client-node'
2import { requestLocalLock } from '@atproto/oauth-client-node'
3import { Redis } from '@upstash/redis'
4
5type Awaitable<T> = T | PromiseLike<T>
6
7/**
8 * Creates a distributed lock using Upstash Redis.
9 * Falls back gracefully if the lock cannot be acquired.
10 */
11function createUpstashLock(redis: Redis): RuntimeLock {
12 return async <T>(key: string, fn: () => Awaitable<T>): Promise<T> => {
13 const lockKey = `oauth:lock:${key}`
14 const lockValue = crypto.randomUUID()
15 const lockTTL = 30 // seconds
16
17 // Try to acquire lock with NX (only set if not exists) and EX (expire)
18 const acquired = await redis.set(lockKey, lockValue, {
19 nx: true,
20 ex: lockTTL,
21 })
22
23 if (!acquired) {
24 // Another instance holds the lock, wait briefly and retry once
25 await new Promise(resolve => setTimeout(resolve, 100))
26 const retryAcquired = await redis.set(lockKey, lockValue, {
27 nx: true,
28 ex: lockTTL,
29 })
30 if (!retryAcquired) {
31 // Still can't acquire, proceed without lock (better than failing)
32 // The worst case is a token refresh race, which will just require re-auth
33 return await fn()
34 }
35 }
36
37 try {
38 return await fn()
39 } finally {
40 // Release lock only if we still own it (compare-and-delete)
41 const currentValue = await redis.get(lockKey)
42 if (currentValue === lockValue) {
43 await redis.del(lockKey)
44 }
45 }
46 }
47}
48
49/**
50 * Returns the appropriate lock mechanism based on environment:
51 * - Production with Upstash config: distributed Redis lock
52 * - Otherwise: in-memory lock (sufficient for single instance)
53 */
54export function getOAuthLock(): RuntimeLock {
55 const config = useRuntimeConfig()
56
57 // Use distributed lock in production if Upstash is configured
58 if (!import.meta.dev && config.upstash?.redisRestUrl && config.upstash?.redisRestToken) {
59 const redis = new Redis({
60 url: config.upstash.redisRestUrl,
61 token: config.upstash.redisRestToken,
62 })
63 return createUpstashLock(redis)
64 }
65
66 // Fall back to in-memory lock for dev/preview or when Redis isn't configured
67 return requestLocalLock
68}