refactor(realtime): the gateway asks a journey-access port instead of importing the journey domain

The websocket gateway injected JourneyDomainService to decide who may join
a journey's book room, so the realtime domain, which nearly every domain
imports for its broadcasts, depended on journey and closed a cycle through
all of them. It now injects a JOURNEY_ACCESS port declared in realtime;
JourneyAccessModule (global, imported by AppModule beside the gateway
module) binds it to the same JourneyDomainService method.
This commit is contained in:
Maurice
2026-10-10 12:29:19 +02:00
parent 0c2226c5f0
commit 846d8d4952
6 changed files with 42 additions and 33 deletions
@@ -2,69 +2,44 @@
"fileCycles": [],
"domainCycles": [
"accommodations -> assignments",
"accommodations -> mcp-shared",
"accommodations -> places",
"accommodations -> realtime",
"addons -> memories",
"addons -> transit",
"assignments -> days",
"assignments -> journey",
"assignments -> mcp-shared",
"assignments -> realtime",
"atlas -> addons",
"auth -> plugins",
"budget -> addons",
"budget -> mcp-shared",
"budget -> realtime",
"days -> accommodations",
"days -> assignments",
"days -> mcp-shared",
"days -> places",
"days -> realtime",
"files -> mcp-shared",
"files -> realtime",
"geo -> maps",
"integrations -> addons",
"integrations -> assignments",
"integrations -> atlas",
"integrations -> journey",
"integrations -> mcp-shared",
"integrations -> places",
"integrations -> realtime",
"integrations -> reservations",
"journey -> addons",
"journey -> atlas",
"journey -> files",
"journey -> memories",
"journey -> photos",
"journey -> realtime",
"maps -> geo",
"mcp-shared -> realtime",
"memories -> addons",
"memories -> notifications",
"memories -> photos",
"memories -> realtime",
"notifications -> realtime",
"photos -> memories",
"places -> accommodations",
"places -> assignments",
"places -> atlas",
"places -> journey",
"places -> mcp-shared",
"places -> realtime",
"plugins -> trips",
"plugins -> vacay",
"realtime -> journey",
"reservations -> accommodations",
"reservations -> assignments",
"reservations -> budget",
"reservations -> days",
"reservations -> integrations",
"reservations -> mcp-shared",
"reservations -> notifications",
"reservations -> realtime",
"transit -> days",
"transit -> mcp-shared",
"transit -> reservations",
"trip-members -> auth",
"trip-members -> trips",
+2
View File
@@ -64,6 +64,7 @@ import { SpaFallbackFilter } from './platform/spa-fallback.filter';
import { PluginsModule } from './plugins/plugins.module';
import { PublicApiModule } from './public-api/public-api.module';
import { RealtimeGatewayModule } from './realtime/realtime-gateway.module';
import { JourneyAccessModule } from './journey/journey-access.module';
import { RealtimeModule } from './realtime/realtime.module';
import { ReceiptScanModule } from './receipt-scan/receipt-scan.module';
import { ReservationImportModule } from './reservation-import/reservation-import.module';
@@ -100,6 +101,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
DemoModule,
RealtimeModule,
RealtimeGatewayModule,
JourneyAccessModule,
SchedulingModule,
McpModule.forRoot({
accessPolicy: trekMcpAccessPolicy,
@@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { JOURNEY_ACCESS } from '../realtime/journey-access.types';
import { JourneyDomainModule } from './journey-domain.module';
import { JourneyDomainService } from './journey-domain.service';
/**
* Binds the realtime gateway's JOURNEY_ACCESS port to JourneyDomainService.
* Global so RealtimeGatewayModule resolves it without importing the journey
* domain; AppModule imports it next to the gateway module.
*/
@Global()
@Module({
imports: [JourneyDomainModule],
providers: [{ provide: JOURNEY_ACCESS, useExisting: JourneyDomainService }],
exports: [JOURNEY_ACCESS],
})
export class JourneyAccessModule {}
@@ -0,0 +1,15 @@
/**
* What the gateway asks before it lets a socket into a journey's book room.
*
* A port rather than JourneyDomainService itself: realtime is imported by
* nearly every domain for the broadcast facade, so depending on journey from
* here put realtime, journey and everything between them on one import cycle.
* The journey domain binds the token (JourneyAccessModule), and the answer is
* still the one the REST routes get.
*/
export const JOURNEY_ACCESS = Symbol('JOURNEY_ACCESS');
export interface JourneyAccess {
/** Truthy when the user may open the journey. */
canAccessJourney(journeyId: number, userId: number): Promise<unknown>;
}
@@ -2,7 +2,6 @@ import { Module, type OnModuleDestroy } from '@nestjs/common';
import { MikroOrmModule } from '@mikro-orm/nestjs';
import { RealtimeGateway } from './realtime.gateway';
import { EphemeralTokenModule } from '../auth-core/ephemeral-token.module';
import { JourneyDomainModule } from '../journey/journey-domain.module';
import { AppSettings } from '../../db/entities/AppSettings.entity';
import { Users } from '../../db/entities/Users.entity';
import { Trips } from '../../db/entities/Trips.entity';
@@ -23,13 +22,13 @@ import { processRooms, RoomRegistry, roomsSlot } from './ws-state';
* TrekWsAdapter already registered.
*/
@Module({
// JourneyDomainModule for the book rooms: who may open a journey is asked
// of the same service the REST routes ask. Users/AppSettings: Plan 4 Task
// The book rooms ask JOURNEY_ACCESS, bound globally by JourneyAccessModule
// to the same service the REST routes ask. Users/AppSettings: Plan 4 Task
// 1 — the handshake's password-version and require_mfa reads, moved off
// DatabaseService onto UsersRepository/AppSettingsRepository. Trips: Plan 4
// Task 2 — handleJoin's own canAccessTrip delegate, now TripsRepository
// directly.
imports: [EphemeralTokenModule, JourneyDomainModule, MikroOrmModule.forFeature([Users, AppSettings, Trips])],
imports: [EphemeralTokenModule, MikroOrmModule.forFeature([Users, AppSettings, Trips])],
// The room registry is the process-wide in-memory one; the constructor
// below hands whichever one the container resolved to the broadcast
// functions, so the gateway and the broadcasts never use two registries.
+5 -4
View File
@@ -1,4 +1,4 @@
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
import {
ConnectedSocket,
MessageBody,
@@ -30,7 +30,7 @@ import {
userOf,
type TrekWebSocket,
} from './ws-state';
import { JourneyDomainService } from '../journey/journey-domain.service';
import { JOURNEY_ACCESS, type JourneyAccess } from './journey-access.types';
import { readAppSetting } from '../common/app-settings.registry';
import { runningVersion } from '../../app-config';
@@ -71,9 +71,10 @@ export class RealtimeGateway
/*
* For the book rooms, and injected rather than reimplemented: who may open
* a journey is one question with one answer, and a second copy of it here
* is a second thing to keep in step with the REST routes.
* is a second thing to keep in step with the REST routes. Behind a port
* (journey-access.types.ts) so realtime does not import the journey domain.
*/
private readonly journeys: JourneyDomainService,
@Inject(JOURNEY_ACCESS) private readonly journeys: JourneyAccess,
@InjectRepository(Users) private readonly users: UsersRepository,
@InjectRepository(AppSettings) private readonly appSettings: AppSettingsRepository,
// Room membership, behind its port. The container's provider, which