This repository has no description
1import { EnvironmentConfigService } from '../../config/EnvironmentConfigService';
2import { jwtConfig, oauthConfig } from '../../config';
3import { JwtTokenService } from '../../../../modules/user/infrastructure/services/JwtTokenService';
4import {
5 AtProtoOAuthProcessor,
6 OAuthClientFactory,
7} from '../../../../modules/user/infrastructure';
8import { UserAuthenticationService } from '../../../../modules/user/infrastructure/services/UserAuthenticationService';
9import { ATProtoAgentService } from '../../../../modules/atproto/infrastructure/services/ATProtoAgentService';
10import { IFramelyMetadataService } from '../../../../modules/cards/infrastructure/IFramelyMetadataService';
11import { CitoidMetadataService } from '../../../../modules/cards/infrastructure/CitoidMetadataService';
12import { CompositeMetadataService } from '../../../../modules/cards/infrastructure/CompositeMetadataService';
13import { CachedMetadataService } from '../../../../modules/cards/infrastructure/CachedMetadataService';
14import { BlueskyProfileService } from '../../../../modules/atproto/infrastructure/services/BlueskyProfileService';
15import { CachedBlueskyProfileService } from '../../../../modules/atproto/infrastructure/services/CachedBlueskyProfileService';
16import { ATProtoCollectionPublisher } from '../../../../modules/atproto/infrastructure/publishers/ATProtoCollectionPublisher';
17import { ATProtoCardPublisher } from '../../../../modules/atproto/infrastructure/publishers/ATProtoCardPublisher';
18import { ATProtoFollowPublisher } from '../../../../modules/atproto/infrastructure/publishers/ATProtoFollowPublisher';
19import { FakeCollectionPublisher } from '../../../../modules/cards/tests/utils/FakeCollectionPublisher';
20import { FakeCardPublisher } from '../../../../modules/cards/tests/utils/FakeCardPublisher';
21import { FakeFollowPublisher } from '../../../../modules/atproto/infrastructure/publishers/FakeFollowPublisher';
22import { CardLibraryService } from '../../../../modules/cards/domain/services/CardLibraryService';
23import { CardCollectionService } from '../../../../modules/cards/domain/services/CardCollectionService';
24import { AuthMiddleware } from '../middleware/AuthMiddleware';
25import { Repositories } from './RepositoryFactory';
26import { NodeOAuthClient } from '@atproto/oauth-client-node';
27import { AppPasswordSessionService } from 'src/modules/atproto/infrastructure/services/AppPasswordSessionService';
28import { AtpAppPasswordProcessor } from 'src/modules/atproto/infrastructure/services/AtpAppPasswordProcessor';
29import { ICollectionPublisher } from 'src/modules/cards/application/ports/ICollectionPublisher';
30import { ICardPublisher } from 'src/modules/cards/application/ports/ICardPublisher';
31import { IFollowPublisher } from 'src/modules/user/application/ports/IFollowPublisher';
32import { IMetadataService } from 'src/modules/cards/domain/services/IMetadataService';
33import { BullMQEventSubscriber } from '../../events/BullMQEventSubscriber';
34import { BullMQEventPublisher } from '../../events/BullMQEventPublisher';
35import { InMemoryEventPublisher } from '../../events/InMemoryEventPublisher';
36import { InMemoryEventSubscriber } from '../../events/InMemoryEventSubscriber';
37import Redis from 'ioredis';
38// Mock/Fake service imports
39import { FakeJwtTokenService } from '../../../../modules/user/infrastructure/services/FakeJwtTokenService';
40import { FakeAtProtoOAuthProcessor } from '../../../../modules/atproto/infrastructure/services/FakeAtProtoOAuthProcessor';
41import { FakeUserAuthenticationService } from '../../../../modules/user/infrastructure/services/FakeUserAuthenticationService';
42import { FakeAgentService } from '../../../../modules/atproto/infrastructure/services/FakeAgentService';
43import { FakeAppPasswordSessionService } from '../../../../modules/atproto/infrastructure/services/FakeAppPasswordSessionService';
44import { FakeAtpAppPasswordProcessor } from '../../../../modules/atproto/infrastructure/services/FakeAtpAppPasswordProcessor';
45import { ITokenService } from 'src/modules/user/application/services/ITokenService';
46import { IOAuthProcessor } from 'src/modules/user/application/services/IOAuthProcessor';
47import { IAppPasswordProcessor } from 'src/modules/atproto/application/IAppPasswordProcessor';
48import { IUserAuthenticationService } from 'src/modules/user/domain/services/IUserAuthenticationService';
49import { IAgentService } from 'src/modules/atproto/application/IAgentService';
50import { IProfileService } from 'src/modules/cards/domain/services/IProfileService';
51import { IEventPublisher } from '../../../application/events/IEventPublisher';
52import { QueueName } from '../../events/QueueConfig';
53import { RedisFactory } from '../../redis/RedisFactory';
54import { IEventSubscriber } from 'src/shared/application/events/IEventSubscriber';
55import { FeedService } from '../../../../modules/feeds/domain/services/FeedService';
56import { ATProtoIdentityResolutionService } from '../../../../modules/atproto/infrastructure/services/ATProtoIdentityResolutionService';
57import { IIdentityResolutionService } from '../../../../modules/atproto/domain/services/IIdentityResolutionService';
58import { CookieService } from '../services/CookieService';
59import { InMemorySagaStateStore } from '../../../../modules/feeds/infrastructure/InMemorySagaStateStore';
60import { RedisSagaStateStore } from '../../../../modules/feeds/infrastructure/RedisSagaStateStore';
61import { ISagaStateStore } from 'src/modules/feeds/application/sagas/ISagaStateStore';
62import { SearchService } from '../../../../modules/search/domain/services/SearchService';
63import { IVectorDatabase } from '../../../../modules/search/domain/IVectorDatabase';
64import { InMemoryVectorDatabase } from '../../../../modules/search/infrastructure/InMemoryVectorDatabase';
65import { UpstashVectorDatabase } from '../../../../modules/search/infrastructure/UpstashVectorDatabase';
66import { NotificationService } from '../../../../modules/notifications/domain/services/NotificationService';
67import { FakeLeafletSearchService } from 'src/modules/search/infrastructure/FakeLeafletSearchService';
68import { ILeafletSearchService } from 'src/modules/search/domain/services/ILeafletSearchService';
69import { ConstellationLeafletSearchService } from 'src/modules/search/domain/services/ConstellationLeafletSearchService';
70import { CachedLeafletSearchService } from 'src/modules/search/infrastructure/CachedLeafletSearchService';
71import { IAtProtoRepoService } from '../../../../modules/atproto/application/IAtProtoRepoService';
72import { ATProtoRepoService } from '../../../../modules/atproto/infrastructure/services/ATProtoRepoService';
73import { FakeAtProtoRepoService } from '../../../../modules/atproto/infrastructure/services/FakeAtProtoRepoService';
74import { DistributedLockServiceFactory } from '../../locking/DistributedLockServiceFactory';
75
76// Shared services needed by both web app and workers
77export interface SharedServices {
78 tokenService: ITokenService;
79 userAuthService: IUserAuthenticationService;
80 atProtoAgentService: IAgentService;
81 atProtoRepoService: IAtProtoRepoService;
82 metadataService: IMetadataService;
83 profileService: IProfileService;
84 feedService: FeedService;
85 notificationService: NotificationService;
86 nodeOauthClient: NodeOAuthClient;
87 identityResolutionService: IIdentityResolutionService;
88 configService: EnvironmentConfigService;
89 cookieService: CookieService;
90 searchService: SearchService;
91 leafletSearchService: ILeafletSearchService;
92 cardLibraryService: CardLibraryService;
93 cardCollectionService: CardCollectionService;
94 eventPublisher: IEventPublisher;
95 collectionPublisher: ICollectionPublisher;
96 followPublisher: IFollowPublisher;
97}
98
99// Web app specific services (includes publishers, auth middleware)
100export interface WebAppServices extends SharedServices {
101 oauthProcessor: IOAuthProcessor;
102 appPasswordProcessor: IAppPasswordProcessor;
103 cardPublisher: ICardPublisher;
104 authMiddleware: AuthMiddleware;
105}
106
107// Worker specific services (includes subscribers)
108export interface WorkerServices extends SharedServices {
109 redisConnection: Redis | null;
110 createEventSubscriber: (queueName: QueueName) => IEventSubscriber;
111 sagaStateStore: ISagaStateStore;
112}
113
114// Legacy interface for backward compatibility
115export interface Services extends WebAppServices {}
116
117export class ServiceFactory {
118 static create(
119 configService: EnvironmentConfigService,
120 repositories: Repositories,
121 ): Services {
122 return this.createForWebApp(configService, repositories);
123 }
124
125 static createForWebApp(
126 configService: EnvironmentConfigService,
127 repositories: Repositories,
128 ): WebAppServices {
129 const sharedServices = this.createSharedServices(
130 configService,
131 repositories,
132 );
133
134 const useMockAuth = configService.shouldUseMockAuth();
135
136 // App Password Session Service
137 const appPasswordSessionService = useMockAuth
138 ? new FakeAppPasswordSessionService()
139 : new AppPasswordSessionService(
140 repositories.appPasswordSessionRepository,
141 );
142
143 // App Password Processor
144 const appPasswordProcessor = useMockAuth
145 ? new FakeAtpAppPasswordProcessor()
146 : new AtpAppPasswordProcessor(appPasswordSessionService);
147
148 // OAuth Processor
149 const oauthProcessor = useMockAuth
150 ? new FakeAtProtoOAuthProcessor(sharedServices.tokenService)
151 : new AtProtoOAuthProcessor(sharedServices.nodeOauthClient);
152
153 const useFakePublishers = configService.shouldUseFakePublishers();
154 const collections = configService.getAtProtoCollections();
155
156 const cardPublisher = useFakePublishers
157 ? new FakeCardPublisher()
158 : new ATProtoCardPublisher(
159 sharedServices.atProtoAgentService,
160 collections.card,
161 );
162
163 const authMiddleware = new AuthMiddleware(
164 sharedServices.tokenService,
165 sharedServices.cookieService,
166 );
167
168 return {
169 ...sharedServices,
170 oauthProcessor,
171 appPasswordProcessor,
172 cardPublisher,
173 authMiddleware,
174 };
175 }
176
177 static createForWorker(
178 configService: EnvironmentConfigService,
179 repositories: Repositories,
180 ): WorkerServices {
181 const sharedServices = this.createSharedServices(
182 configService,
183 repositories,
184 );
185
186 const useInMemoryEvents = configService.shouldUseInMemoryEvents();
187
188 let eventPublisher: IEventPublisher;
189 let redisConnection: Redis | null = null;
190 let createEventSubscriber: (queueName: QueueName) => IEventSubscriber;
191
192 if (useInMemoryEvents) {
193 eventPublisher = new InMemoryEventPublisher();
194 createEventSubscriber = (queueName: QueueName) => {
195 return new InMemoryEventSubscriber();
196 };
197 } else {
198 // Redis connection is required for BullMQ workers
199 if (!process.env.REDIS_URL) {
200 throw new Error(
201 'REDIS_URL environment variable is required for BullMQ worker services',
202 );
203 }
204
205 const redisConfig = configService.getWorkersConfig().redisConfig;
206 redisConnection = RedisFactory.createConnection(redisConfig);
207 eventPublisher = new BullMQEventPublisher(redisConnection);
208
209 createEventSubscriber = (queueName: QueueName) => {
210 return new BullMQEventSubscriber(redisConnection!, { queueName });
211 };
212 }
213
214 // Create appropriate saga state store
215 const sagaStateStore = useInMemoryEvents
216 ? new InMemorySagaStateStore()
217 : new RedisSagaStateStore(redisConnection!);
218
219 return {
220 ...sharedServices,
221 redisConnection: redisConnection,
222 createEventSubscriber,
223 sagaStateStore,
224 };
225 }
226
227 private static createSharedServices(
228 configService: EnvironmentConfigService,
229 repositories: Repositories,
230 ): SharedServices {
231 const useMockAuth = configService.shouldUseMockAuth();
232
233 const nodeOauthClient = OAuthClientFactory.createClient(
234 repositories.oauthStateStore,
235 repositories.oauthSessionStore,
236 oauthConfig.baseUrl,
237 );
238
239 // Token Service
240 const tokenService = useMockAuth
241 ? new FakeJwtTokenService(repositories.tokenRepository)
242 : new JwtTokenService(
243 repositories.tokenRepository,
244 jwtConfig.jwtSecret,
245 jwtConfig.accessTokenExpiresIn,
246 jwtConfig.refreshTokenExpiresIn,
247 );
248
249 // User Authentication Service
250 const userAuthService = useMockAuth
251 ? new FakeUserAuthenticationService(repositories.userRepository)
252 : new UserAuthenticationService(repositories.userRepository);
253
254 // App Password Session Service (needed for ATProto Agent Service)
255 const appPasswordSessionService = useMockAuth
256 ? new FakeAppPasswordSessionService()
257 : new AppPasswordSessionService(
258 repositories.appPasswordSessionRepository,
259 );
260
261 // ATProto Agent Service
262 const atProtoAgentService = useMockAuth
263 ? new FakeAgentService()
264 : new ATProtoAgentService(
265 nodeOauthClient,
266 appPasswordSessionService,
267 configService,
268 );
269
270 // ATProto Repo Service
271 const atProtoRepoService = useMockAuth
272 ? new FakeAtProtoRepoService()
273 : new ATProtoRepoService(atProtoAgentService);
274
275 // Create individual metadata services
276 const baseIframelyService = new IFramelyMetadataService(
277 configService.getIFramelyApiKey(),
278 );
279 const baseCitoidService = new CitoidMetadataService();
280
281 // Apply caching conditionally (similar to profile service)
282 const useMockPersistence = configService.shouldUseMockPersistence();
283
284 let iframelyService: IMetadataService;
285 let citoidService: IMetadataService;
286
287 if (useMockPersistence) {
288 // No caching for mock persistence
289 iframelyService = baseIframelyService;
290 citoidService = baseCitoidService;
291 } else {
292 // Create Redis connection for caching
293 const redisConfig = configService.getRedisConfig();
294 const redis = RedisFactory.createConnection(redisConfig);
295
296 // Wrap services with caching - different TTLs for different services
297 iframelyService = new CachedMetadataService(
298 baseIframelyService,
299 redis,
300 'iframely',
301 3600 * 24 * 7, // 7 day TTL
302 );
303 citoidService = new CachedMetadataService(
304 baseCitoidService,
305 redis,
306 'citoid',
307 3600 * 24 * 7, // 7 day TTL
308 );
309 }
310
311 // Create composite metadata service
312 const metadataService = new CompositeMetadataService(
313 iframelyService,
314 citoidService,
315 );
316
317 // Profile Service with Redis caching
318 const baseProfileService = new BlueskyProfileService(
319 atProtoAgentService,
320 repositories.followsRepository,
321 );
322
323 let profileService: IProfileService;
324
325 // caching requires persistence
326 if (useMockPersistence) {
327 profileService = baseProfileService;
328 } else {
329 // Create Redis connection for caching
330 const redisConfig = configService.getRedisConfig();
331 const redis = RedisFactory.createConnection(redisConfig);
332 profileService = new CachedBlueskyProfileService(
333 baseProfileService,
334 redis,
335 repositories.followsRepository,
336 );
337 }
338
339 // Feed Service with distributed locking
340 const distributedLockService = DistributedLockServiceFactory.create();
341 const feedService = new FeedService(
342 repositories.feedRepository,
343 distributedLockService,
344 );
345
346 // Notification Service
347 const notificationService = new NotificationService(
348 repositories.notificationRepository,
349 );
350
351 // Identity Resolution Service
352 const identityResolutionService = new ATProtoIdentityResolutionService(
353 atProtoAgentService,
354 );
355
356 // Cookie Service
357 const cookieService = new CookieService(configService);
358
359 // Create vector database and search service (shared by both web app and workers)
360 const useMockVectorDb = configService.shouldUseMockVectorDb();
361
362 const vectorDatabase: IVectorDatabase = useMockVectorDb
363 ? InMemoryVectorDatabase.getInstance()
364 : new UpstashVectorDatabase(
365 configService.getUpstashConfig().vectorUrl,
366 configService.getUpstashConfig().vectorToken,
367 );
368
369 const searchService = new SearchService(
370 vectorDatabase,
371 metadataService,
372 repositories.cardQueryRepository,
373 );
374
375 // Create LeafletSearchService with caching
376 let leafletSearchService: ILeafletSearchService;
377
378 if (useMockPersistence) {
379 // Use fake implementation for mock persistence
380 leafletSearchService = new FakeLeafletSearchService();
381 } else {
382 // Use real Constellation service with caching
383 const baseLeafletSearchService = new ConstellationLeafletSearchService(
384 metadataService,
385 );
386 const redisConfig = configService.getRedisConfig();
387 const redis = RedisFactory.createConnection(redisConfig);
388 leafletSearchService = new CachedLeafletSearchService(
389 baseLeafletSearchService,
390 redis,
391 );
392 }
393
394 // Create publishers needed for shared services
395 const useFakePublishers = configService.shouldUseFakePublishers();
396 const collections = configService.getAtProtoCollections();
397
398 const collectionPublisher = useFakePublishers
399 ? new FakeCollectionPublisher()
400 : new ATProtoCollectionPublisher(
401 atProtoAgentService,
402 collections.collection,
403 collections.collectionLink,
404 collections.collectionLinkRemoval,
405 );
406
407 const followPublisher = useFakePublishers
408 ? new FakeFollowPublisher()
409 : new ATProtoFollowPublisher(
410 atProtoAgentService,
411 collections.follow,
412 repositories.collectionRepository,
413 );
414
415 const cardPublisher = useFakePublishers
416 ? new FakeCardPublisher()
417 : new ATProtoCardPublisher(atProtoAgentService, collections.card);
418
419 // Create domain services
420 const cardCollectionService = new CardCollectionService(
421 repositories.collectionRepository,
422 collectionPublisher,
423 repositories.cardRepository,
424 );
425 const cardLibraryService = new CardLibraryService(
426 repositories.cardRepository,
427 cardPublisher,
428 repositories.collectionRepository,
429 cardCollectionService,
430 );
431
432 // Create event publisher
433 const useInMemoryEvents = configService.shouldUseInMemoryEvents();
434
435 let eventPublisher: IEventPublisher;
436 if (useInMemoryEvents) {
437 eventPublisher = new InMemoryEventPublisher();
438 } else {
439 const redisConnection = RedisFactory.createConnection(
440 configService.getWorkersConfig().redisConfig,
441 );
442 eventPublisher = new BullMQEventPublisher(redisConnection);
443 }
444
445 return {
446 tokenService,
447 userAuthService,
448 atProtoAgentService,
449 atProtoRepoService,
450 metadataService,
451 profileService,
452 feedService,
453 notificationService,
454 nodeOauthClient,
455 identityResolutionService,
456 configService,
457 cookieService,
458 searchService,
459 leafletSearchService,
460 cardLibraryService,
461 cardCollectionService,
462 eventPublisher,
463 collectionPublisher,
464 followPublisher,
465 };
466 }
467}