This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / shared / infrastructure / http / factories / ServiceFactory.ts
19 kB 467 lines
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}