A social knowledge tool for researchers built on ATProto
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}