A social knowledge tool for researchers built on ATProto

formatting and linting

+55 -40
+1
eslint.config.mjs
··· 76 76 NodeJS: 'readonly', 77 77 clearTimeout: 'readonly', 78 78 setImmediate: 'readonly', 79 + setInterval: 'readonly', 79 80 }, 80 81 }, 81 82 rules: {
+1 -1
src/shared/infrastructure/locking/RedisLockService.ts
··· 27 27 // Include Fly.io instance info in lock key 28 28 const instanceId = process.env.FLY_ALLOC_ID || 'local'; 29 29 const lockKey = `oauth:lock:${instanceId}:${key}`; 30 - 30 + 31 31 // 30 seconds for Fly.io (containers restart more frequently) 32 32 const lock = await this.redlock.acquire([lockKey], 30000); 33 33
+53 -39
src/shared/infrastructure/locking/tests/RedisLockService.integration.test.ts
··· 69 69 const createTestFunction = (id: number) => async () => { 70 70 const executionId = ++currentExecution; 71 71 executionOrder.push(id); 72 - 72 + 73 73 // Simulate some work 74 - await new Promise(resolve => setTimeout(resolve, 100)); 75 - 74 + await new Promise((resolve) => setTimeout(resolve, 100)); 75 + 76 76 return `result-${id}-${executionId}`; 77 77 }; 78 78 ··· 87 87 expect(result1).toMatch(/^result-1-\d+$/); 88 88 expect(result2).toMatch(/^result-2-\d+$/); 89 89 expect(executionOrder).toHaveLength(2); 90 - 90 + 91 91 // Verify they executed sequentially (not concurrently) 92 92 expect(currentExecution).toBe(2); 93 93 }); ··· 97 97 const lockKey1 = 'lock-key-1'; 98 98 const lockKey2 = 'lock-key-2'; 99 99 let startTimes: number[] = []; 100 - 100 + 101 101 const createTestFunction = (id: number) => async () => { 102 102 startTimes.push(Date.now()); 103 - await new Promise(resolve => setTimeout(resolve, 200)); 103 + await new Promise((resolve) => setTimeout(resolve, 200)); 104 104 return `result-${id}`; 105 105 }; 106 106 ··· 117 117 expect(result1).toBe('result-1'); 118 118 expect(result2).toBe('result-2'); 119 119 expect(startTimes).toHaveLength(2); 120 - 120 + 121 121 // Should complete in roughly 200ms (concurrent) rather than 400ms (sequential) 122 122 expect(totalTime).toBeLessThan(350); 123 - 123 + 124 124 // Start times should be close together (concurrent execution) 125 125 const timeDiff = Math.abs(startTimes[1]! - startTimes[0]!); 126 126 expect(timeDiff).toBeLessThan(50); ··· 136 136 137 137 // Act & Assert 138 138 const requestLock = lockService.createRequestLock(); 139 - await expect(requestLock(lockKey, errorFunction)).rejects.toThrow(errorMessage); 139 + await expect(requestLock(lockKey, errorFunction)).rejects.toThrow( 140 + errorMessage, 141 + ); 140 142 141 143 // Verify lock was released even after error 142 144 const lockPattern = `oauth:lock:*:${lockKey}`; ··· 148 150 // Arrange 149 151 const lockKey = 'async-test-lock'; 150 152 const asyncFunction = async () => { 151 - await new Promise(resolve => setTimeout(resolve, 50)); 153 + await new Promise((resolve) => setTimeout(resolve, 50)); 152 154 return { data: 'async-result', timestamp: Date.now() }; 153 155 }; 154 156 ··· 168 170 // Arrange 169 171 const originalAllocId = process.env.FLY_ALLOC_ID; 170 172 process.env.FLY_ALLOC_ID = 'test-instance-123'; 171 - 173 + 172 174 const lockKey = 'instance-test-lock'; 173 175 let lockKeyUsed = ''; 174 - 176 + 175 177 // Mock redlock to capture the actual lock key used 176 178 const originalAcquire = lockService['redlock'].acquire; 177 - lockService['redlock'].acquire = jest.fn().mockImplementation(async (keys: string[]) => { 178 - lockKeyUsed = keys[0]!; 179 - return originalAcquire.call(lockService['redlock'], keys, 30000); 180 - }); 179 + lockService['redlock'].acquire = jest 180 + .fn() 181 + .mockImplementation(async (keys: string[]) => { 182 + lockKeyUsed = keys[0]!; 183 + return originalAcquire.call(lockService['redlock'], keys, 30000); 184 + }); 181 185 182 186 try { 183 187 // Act ··· 197 201 // Arrange 198 202 const originalAllocId = process.env.FLY_ALLOC_ID; 199 203 delete process.env.FLY_ALLOC_ID; 200 - 204 + 201 205 const lockKey = 'local-test-lock'; 202 206 let lockKeyUsed = ''; 203 - 207 + 204 208 // Mock redlock to capture the actual lock key used 205 209 const originalAcquire = lockService['redlock'].acquire; 206 - lockService['redlock'].acquire = jest.fn().mockImplementation(async (keys: string[]) => { 207 - lockKeyUsed = keys[0]!; 208 - return originalAcquire.call(lockService['redlock'], keys, 30000); 209 - }); 210 + lockService['redlock'].acquire = jest 211 + .fn() 212 + .mockImplementation(async (keys: string[]) => { 213 + lockKeyUsed = keys[0]!; 214 + return originalAcquire.call(lockService['redlock'], keys, 30000); 215 + }); 210 216 211 217 try { 212 218 // Act ··· 227 233 it('should automatically release lock after TTL expires', async () => { 228 234 // Arrange 229 235 const lockKey = 'ttl-test-lock'; 230 - 236 + 231 237 // Manually acquire a lock with short TTL to simulate timeout 232 238 const instanceId = process.env.FLY_ALLOC_ID || 'local'; 233 239 const fullLockKey = `oauth:lock:${instanceId}:${lockKey}`; 234 - 240 + 235 241 // Use redlock directly to set a very short TTL (100ms) 236 - const shortLock = await lockService['redlock'].acquire([fullLockKey], 100); 242 + const shortLock = await lockService['redlock'].acquire( 243 + [fullLockKey], 244 + 100, 245 + ); 237 246 238 247 // Act - Wait for lock to expire 239 - await new Promise(resolve => setTimeout(resolve, 200)); 248 + await new Promise((resolve) => setTimeout(resolve, 200)); 240 249 241 250 // Try to acquire the same lock - should succeed if previous lock expired 242 251 const requestLock = lockService.createRequestLock(); 243 - const result = await requestLock(lockKey, async () => 'success-after-timeout'); 252 + const result = await requestLock( 253 + lockKey, 254 + async () => 'success-after-timeout', 255 + ); 244 256 245 257 // Assert 246 258 expect(result).toBe('success-after-timeout'); ··· 258 270 const lockKey = 'high-concurrency-lock'; 259 271 const concurrentOperations = 5; 260 272 let completedOperations = 0; 261 - 273 + 262 274 const testFunction = async () => { 263 - await new Promise(resolve => setTimeout(resolve, 50)); 275 + await new Promise((resolve) => setTimeout(resolve, 50)); 264 276 return ++completedOperations; 265 277 }; 266 278 267 279 // Act - Start multiple concurrent operations 268 280 const requestLock = lockService.createRequestLock(); 269 281 const promises = Array.from({ length: concurrentOperations }, () => 270 - requestLock(lockKey, testFunction) 282 + requestLock(lockKey, testFunction), 271 283 ); 272 - 284 + 273 285 const results = await Promise.all(promises); 274 286 275 287 // Assert - All operations should complete successfully 276 288 expect(results).toHaveLength(concurrentOperations); 277 289 expect(completedOperations).toBe(concurrentOperations); 278 - 290 + 279 291 // Results should be sequential numbers (1, 2, 3, 4, 5) 280 292 const sortedResults = results.sort((a, b) => a - b); 281 293 expect(sortedResults).toEqual([1, 2, 3, 4, 5]); ··· 285 297 describe('Error Handling', () => { 286 298 it('should handle Redis connection issues gracefully', async () => { 287 299 // Arrange - Create a new Redis connection that we can close 288 - const testRedis = new Redis(redisContainer.getConnectionUrl(), { 289 - maxRetriesPerRequest: null 300 + const testRedis = new Redis(redisContainer.getConnectionUrl(), { 301 + maxRetriesPerRequest: null, 290 302 }); 291 303 const testLockService = new RedisLockService(testRedis); 292 - 304 + 293 305 // Close the connection to simulate network issues 294 306 await testRedis.quit(); 295 307 296 308 // Act & Assert - Should throw an error when trying to acquire lock 297 309 const requestLock = testLockService.createRequestLock(); 298 310 await expect( 299 - requestLock('test-key', async () => 'should-not-execute') 311 + requestLock('test-key', async () => 'should-not-execute'), 300 312 ).rejects.toThrow(); 301 313 }); 302 314 ··· 304 316 // Arrange 305 317 const lockKey = 'interrupt-test-lock'; 306 318 let lockAcquired = false; 307 - 319 + 308 320 const interruptedFunction = async () => { 309 321 lockAcquired = true; 310 322 // Simulate an interruption/error after lock is acquired ··· 313 325 314 326 // Act & Assert 315 327 const requestLock = lockService.createRequestLock(); 316 - await expect(requestLock(lockKey, interruptedFunction)).rejects.toThrow('Simulated interruption'); 317 - 328 + await expect(requestLock(lockKey, interruptedFunction)).rejects.toThrow( 329 + 'Simulated interruption', 330 + ); 331 + 318 332 // Verify lock was acquired initially 319 333 expect(lockAcquired).toBe(true); 320 334