···1010 await appProcess.start();
11111212 // Only start event worker in same process when using in-memory events
1313- const useInMemoryEvents = process.env.USE_IN_MEMORY_EVENTS === 'true';
1313+ const useInMemoryEvents = configService.shouldUseInMemoryEvents();
1414 if (useInMemoryEvents) {
1515 console.log('Starting in-memory event worker in the same process...');
1616 const inMemoryWorkerProcess = new InMemoryEventWorkerProcess(configService);
···11+import { RuntimeLock } from '@atproto/oauth-client-node';
22+import Redis from 'ioredis';
33+import Redlock from 'redlock';
44+import { ILockService } from './ILockService';
55+66+export class RedisLockService implements ILockService {
77+ private redlock: Redlock;
88+99+ constructor(private redis: Redis) {
1010+ this.redlock = new Redlock([redis], {
1111+ // Retry settings
1212+ retryCount: 3,
1313+ retryDelay: 200, // ms
1414+ retryJitter: 200, // ms
1515+ });
1616+1717+ // Handle Fly.io container shutdown gracefully
1818+ process.on('SIGTERM', () => {
1919+ console.log('Received SIGTERM, shutting down gracefully...');
2020+ // Redlock will automatically release locks when the process exits
2121+ // No manual cleanup needed due to TTL
2222+ });
2323+ }
2424+2525+ createRequestLock(): RuntimeLock {
2626+ return async (key: string, fn: () => any) => {
2727+ // Include Fly.io instance info in lock key
2828+ const instanceId = process.env.FLY_ALLOC_ID || 'local';
2929+ const lockKey = `oauth:lock:${instanceId}:${key}`;
3030+3131+ // 30 seconds for Fly.io (containers restart more frequently)
3232+ const lock = await this.redlock.acquire([lockKey], 30000);
3333+3434+ try {
3535+ return await fn();
3636+ } finally {
3737+ await this.redlock.release(lock);
3838+ }
3939+ };
4040+ }
4141+}
+4
src/shared/infrastructure/locking/index.ts
···11+export type { ILockService } from './ILockService';
22+export { RedisLockService } from './RedisLockService';
33+export { InMemoryLockService } from './InMemoryLockService';
44+export { LockServiceFactory } from './LockServiceFactory';
···11-import { ApiClient } from '@/api-client/ApiClient';
21import { useMutation, useQueryClient } from '@tanstack/react-query';
22+import { createCollection } from '../dal';
33+import { collectionKeys } from '../collectionKeys';
3445export default function useCreateCollection() {
55- const apiClient = new ApiClient(
66- process.env.NEXT_PUBLIC_API_BASE_URL || 'http://127.0.0.1:3000',
77- );
88-96 const queryClient = useQueryClient();
107118 const mutation = useMutation({
129 mutationFn: (newCollection: { name: string; description: string }) => {
1313- return apiClient.createCollection(newCollection);
1010+ return createCollection(newCollection);
1411 },
15121613 // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks
1714 // Do UI related things like redirects or showing toast notifications in mutate callbacks. If the user navigated away from the current screen before the mutation finished, those will purposefully not fire
1815 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire
1916 onSuccess: () => {
2020- queryClient.invalidateQueries({ queryKey: ['collections'] });
1717+ queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() });
1818+ queryClient.refetchQueries({ queryKey: collectionKeys.mine() });
2119 },
2220 });
2321