A social knowledge tool for researchers built on ATProto
at main 1.8 kB view raw
1import { RuntimeLock } from '@atproto/oauth-client-node'; 2import { ILockService } from './ILockService'; 3 4interface LockInfo { 5 expiresAt: number; 6 promise: Promise<any>; 7} 8 9export class InMemoryLockService implements ILockService { 10 private locks = new Map<string, LockInfo>(); 11 12 constructor() { 13 // Clean up expired locks every 30 seconds 14 setInterval(() => { 15 const now = Date.now(); 16 for (const [key, lock] of this.locks.entries()) { 17 if (now > lock.expiresAt) { 18 this.locks.delete(key); 19 } 20 } 21 }, 30000); 22 } 23 24 createRequestLock(): RuntimeLock { 25 return async (key: string, fn: () => any) => { 26 const lockKey = `oauth:lock:${key}`; 27 const now = Date.now(); 28 const expiresAt = now + 45000; // 45 seconds 29 30 // Check if lock exists and is still valid 31 const existingLock = this.locks.get(lockKey); 32 if (existingLock && now < existingLock.expiresAt) { 33 // Wait for existing lock to complete, then retry 34 try { 35 await existingLock.promise; 36 } catch { 37 // Ignore errors from other processes 38 } 39 await new Promise((resolve) => setTimeout(resolve, 100)); 40 return this.createRequestLock()(key, fn); 41 } 42 43 // Create new lock 44 const lockPromise = this.executeLocked(fn); 45 this.locks.set(lockKey, { 46 expiresAt, 47 promise: lockPromise, 48 }); 49 50 try { 51 return await lockPromise; 52 } finally { 53 // Clean up lock if it's still ours 54 const currentLock = this.locks.get(lockKey); 55 if (currentLock?.promise === lockPromise) { 56 this.locks.delete(lockKey); 57 } 58 } 59 }; 60 } 61 62 private async executeLocked<T>(fn: () => Promise<T>): Promise<T> { 63 return await fn(); 64 } 65}