fix(realtime): post-review polish from final branch review

Five fixes surfaced by the branch-wide code review on the realtime layer:

- server.ts: replace dynamic `import("@repo/auth/di/container")` with a
  static top-of-file import. The dynamic-import workaround from 6a0ac63 is
  no longer needed once the root tsconfig + TSX_TSCONFIG_PATH expose
  decorator metadata to tsx; verified by booting `pnpm dev` clean.
- server.ts: correct the inline structural type for `validateSession` to
  match the real `IAuthenticationService` contract (non-nullable, throws
  on invalid session) and wrap the call in try/catch so unauthenticated
  bubbles to a `null` return instead of dead-code `result ? ... : null`.
- bind-production.ts: extract `maybeRegisterRealtimePing()` that wraps the
  built-in ping inbound handler in the same `withSpan(withCapture(...))`
  sandwich the realtime-handler generator emits (R41–R44), so the
  proof-of-life channel models the convention rather than registering raw.
- bind-production.test.ts: add 4 tests for the `REALTIME_PING_DISABLED`
  env-gate (registered when unset in both binders, not registered when
  "true", treated as enabled when "1").
- docs/guides/realtime.md: correct the integration-test reference at
  line 285 — the test does not call `bindAllDevSeed()`; it builds the
  Socket.IO server inline and exercises gates 1+2 only (gates 3+4 live in
  socket-io-realtime-server.test.ts).
- adr-016: add a "Known follow-ups" section recording 6 lower-priority
  refinements deferred from this branch (bridge stub test scaffolding,
  registry register/registerChannel precedence, channel-template dot
  constraint, server bare catch{}, BindAllDeps Partial widening, AGENTS.md
  anchor count phrasing).

CI gates: lint 0 errors / 4 warnings (pre-existing turbo.json warnings),
typecheck clean, 24 web-next tests pass (was 20; 4 new env-gate tests),
boundaries 0 issues across 504 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 10:16:46 +02:00
parent 6a0ac63bb9
commit f8013451de
5 changed files with 101 additions and 23 deletions

View File

@@ -11,9 +11,17 @@ import {
type IRealtimeAuthenticator,
} from "@repo/core-realtime";
import { SESSION_COOKIE } from "@repo/auth";
import { authContainer } from "@repo/auth/di/container";
import { AUTH_SYMBOLS } from "@repo/auth/di/symbols";
import { bindAll } from "./src/server/bind-production.js";
// Real shape of IAuthenticationService.validateSession: returns non-nullable
// on success and throws UnauthenticatedError on missing/invalid sessions.
// Kept as an inline structural type to avoid leaking auth's internal interface.
type AuthService = {
validateSession: (id: string) => Promise<{ user: { id: string }; session: unknown }>;
};
const dev = process.env.NODE_ENV !== "production";
const port = Number(process.env.PORT ?? 3000);
@@ -34,22 +42,17 @@ const authenticator: IRealtimeAuthenticator = {
authenticate: async ({ cookies }) => {
const sessionId = cookies[SESSION_COOKIE];
if (!sessionId) return null;
// Lazy-import the auth container after bindAll() has already populated it.
// Dynamic import defers tsx's module transformation to runtime, which lets
// reflect-metadata and the DI decorators resolve correctly in this server
// entry point.
const { authContainer } = await import("@repo/auth/di/container");
const authService = authContainer.get<{
validateSession: (id: string) => Promise<{ user: { id: string }; session: unknown } | null>;
}>(AUTH_SYMBOLS.IAuthenticationService);
const result = await authService.validateSession(sessionId);
return result
? {
userId: result.user.id,
// Roles are not yet in the session shape; extend here when DB-backed roles ship.
roles: (result as unknown as { roles?: string[] }).roles ?? [],
}
: null;
const authService = authContainer.get<AuthService>(AUTH_SYMBOLS.IAuthenticationService);
try {
const { user } = await authService.validateSession(sessionId);
// Roles are not yet in the session shape; extend here when DB-backed roles ship.
return { userId: user.id, roles: (user as { roles?: string[] }).roles ?? [] };
} catch {
// Invalid/expired session → reject the connection. Real auth-service errors
// (DB outages etc.) intentionally collapse to "unauthenticated" here too,
// which is the conservative choice for a public-facing socket.
return null;
}
},
};

View File

@@ -192,6 +192,48 @@ describe("bindAll dispatcher", () => {
});
});
describe("realtime-ping registration (REALTIME_PING_DISABLED env-gate)", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
vi.unstubAllEnvs();
});
afterEach(() => {
vi.unstubAllEnvs();
});
it("registers realtime.ping when REALTIME_PING_DISABLED is unset (production)", async () => {
const { bindAllProduction } = await import("./bind-production");
const deps = makeDeps();
await bindAllProduction(deps);
expect(deps.realtimeRegistry.listChannels().map((d) => d.name)).toContain("realtime.ping");
});
it("registers realtime.ping when REALTIME_PING_DISABLED is unset (dev seed)", async () => {
const { bindAllDevSeed } = await import("./bind-production");
const deps = makeDeps();
await bindAllDevSeed(deps);
expect(deps.realtimeRegistry.listChannels().map((d) => d.name)).toContain("realtime.ping");
});
it("does NOT register realtime.ping when REALTIME_PING_DISABLED='true'", async () => {
vi.stubEnv("REALTIME_PING_DISABLED", "true");
const { bindAllProduction } = await import("./bind-production");
const deps = makeDeps();
await bindAllProduction(deps);
expect(deps.realtimeRegistry.listChannels().map((d) => d.name)).not.toContain("realtime.ping");
});
it("treats REALTIME_PING_DISABLED='1' as not-disabled (only literal 'true' disables)", async () => {
vi.stubEnv("REALTIME_PING_DISABLED", "1");
const { bindAllDevSeed } = await import("./bind-production");
const deps = makeDeps();
await bindAllDevSeed(deps);
expect(deps.realtimeRegistry.listChannels().map((d) => d.name)).toContain("realtime.ping");
});
});
describe("bindAll instrumentation orthogonality (Rule 0, R47)", () => {
beforeEach(() => {
vi.resetModules();

View File

@@ -7,6 +7,8 @@ import config from "@repo/core-cms";
import {
bindNoopInstrumentation,
bindSentryInstrumentation,
withCapture,
withSpan,
type ITracer,
type ILogger,
} from "@repo/core-shared/instrumentation";
@@ -121,9 +123,7 @@ export async function bindAllProduction(deps: BindAllDeps): Promise<void> {
bindProductionMarketingPages(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 20
bindProductionNavigation(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 21
bindProductionMedia(resolvedConfig, tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 22
if (process.env.REALTIME_PING_DISABLED !== "true") {
realtimeRegistry.register(realtimePingInboundDescriptor(realtime));
}
maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger);
bindRealtimeBridge(bus, realtime);
}
@@ -143,9 +143,7 @@ export async function bindAllDevSeed(deps: BindAllDeps): Promise<void> {
await bindDevSeedMarketingPages(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 20
await bindDevSeedNavigation(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 21
await bindDevSeedMedia(tracer, logger, bus, queue, realtime, realtimeRegistry); // Phase E task 22
if (process.env.REALTIME_PING_DISABLED !== "true") {
realtimeRegistry.register(realtimePingInboundDescriptor(realtime));
}
maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger);
bindRealtimeBridge(bus, realtime);
}
@@ -181,6 +179,30 @@ export async function bindAll(deps?: Partial<BindAllDeps>): Promise<void> {
await bindAllDevSeed(resolvedDeps);
}
// Wraps the built-in realtime-ping inbound handler in the same span+capture
// sandwich the realtime-handler generator emits (R41R44), so the
// proof-of-life channel models the convention rather than registering raw.
// Skipped entirely when REALTIME_PING_DISABLED === "true".
function maybeRegisterRealtimePing(
registry: IRealtimeHandlerRegistry,
realtime: IRealtimeBroadcaster,
tracer: ITracer,
logger: ILogger,
): void {
if (process.env.REALTIME_PING_DISABLED === "true") return;
const { descriptor, handler } = realtimePingInboundDescriptor(realtime);
const wrappedHandler = withSpan(
tracer,
{ name: "core-realtime.realtimePing", op: "realtime-handler" },
withCapture(
logger,
{ feature: "core-realtime", layer: "realtime-handler", name: "core-realtime.realtimePing" },
handler,
),
);
registry.register({ descriptor, handler: wrappedHandler });
}
function bindRealtimeBridge(_bus: IEventBus, _broadcaster: IRealtimeBroadcaster): void {
// v1 ships with an empty allowlist. The dashboard PR adds the first entries here.
// Example shape (commented out so v1 doesn't try to use it):