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