A social knowledge tool for researchers built on ATProto

formatting and linting

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