refactor: remove core-realtime from main (scaffoldable via gen core-package realtime)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -8,7 +8,6 @@ const nextConfig = {
|
||||
"@repo/core-api",
|
||||
"@repo/core-cms",
|
||||
"@repo/core-events",
|
||||
"@repo/core-realtime",
|
||||
"@repo/core-shared",
|
||||
"@repo/core-trpc",
|
||||
"@repo/core-ui",
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@repo/core-cms": "workspace:*",
|
||||
"@repo/core-events": "workspace:*",
|
||||
"@repo/core-realtime": "workspace:*",
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@repo/core-trpc": "workspace:*",
|
||||
"@repo/core-ui": "workspace:*",
|
||||
@@ -35,12 +34,10 @@
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"socket.io": "^4.7.0",
|
||||
"superjson": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0",
|
||||
"socket.io-client": "^4.7.0",
|
||||
"@repo/core-eslint": "workspace:*",
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
// apps/web-next/server.ts
|
||||
// SERVER-ONLY entry. Boots Next.js + Socket.IO on the same Node http server.
|
||||
// SERVER-ONLY entry. Custom Next.js server for local development.
|
||||
// When @repo/core-realtime is scaffolded, this file is extended to boot
|
||||
// Socket.IO alongside Next (see pnpm turbo gen core-package realtime).
|
||||
import "reflect-metadata";
|
||||
import { createServer } from "node:http";
|
||||
import next from "next";
|
||||
import { Server as IOServer } from "socket.io";
|
||||
import {
|
||||
RealtimeHandlerRegistry,
|
||||
SocketIORealtimeBroadcaster,
|
||||
SocketIORealtimeServer,
|
||||
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);
|
||||
|
||||
@@ -30,40 +15,9 @@ const handle = app.getRequestHandler();
|
||||
|
||||
await app.prepare();
|
||||
|
||||
await bindAll();
|
||||
|
||||
const httpServer = createServer((req, res) => handle(req, res));
|
||||
const io = new IOServer(httpServer);
|
||||
|
||||
const broadcaster = new SocketIORealtimeBroadcaster(io);
|
||||
const registry = new RealtimeHandlerRegistry();
|
||||
|
||||
await bindAll({ realtime: broadcaster, realtimeRegistry: registry });
|
||||
|
||||
const authenticator: IRealtimeAuthenticator = {
|
||||
authenticate: async ({ cookies }) => {
|
||||
const sessionId = cookies[SESSION_COOKIE];
|
||||
if (!sessionId) return 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const realtimeServer = new SocketIORealtimeServer({
|
||||
httpServer,
|
||||
io,
|
||||
authenticator,
|
||||
registry,
|
||||
});
|
||||
await realtimeServer.start();
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.log(`> Ready on http://localhost:${port}`);
|
||||
});
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
// e2e proof-of-life: connect → subscribe → emit ping → receive pong via the
|
||||
// production-shaped binder + Socket.IO server, with cookie-session auth
|
||||
// against a seeded test session.
|
||||
import "reflect-metadata";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { createServer, type Server as HttpServer } from "node:http";
|
||||
import { Server as IOServer } from "socket.io";
|
||||
import { io as ioClient } from "socket.io-client";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import {
|
||||
RealtimeHandlerRegistry,
|
||||
SocketIORealtimeBroadcaster,
|
||||
SocketIORealtimeServer,
|
||||
realtimePingInboundDescriptor,
|
||||
realtimePongChannel,
|
||||
type IRealtimeAuthenticator,
|
||||
} from "@repo/core-realtime";
|
||||
|
||||
describe("e2e: realtime-ping exercises all four checkpoints", () => {
|
||||
let httpServer: HttpServer;
|
||||
let realtimeServer: SocketIORealtimeServer;
|
||||
let port: number;
|
||||
|
||||
beforeEach(async () => {
|
||||
httpServer = createServer();
|
||||
const io = new IOServer(httpServer);
|
||||
const broadcaster = new SocketIORealtimeBroadcaster(io);
|
||||
const registry = new RealtimeHandlerRegistry();
|
||||
registry.register(realtimePingInboundDescriptor(broadcaster));
|
||||
registry.registerChannel(realtimePongChannel);
|
||||
|
||||
const authenticator: IRealtimeAuthenticator = {
|
||||
authenticate: async ({ cookies }) =>
|
||||
cookies.session === "valid-session"
|
||||
? { userId: "user_test", roles: [] }
|
||||
: null,
|
||||
};
|
||||
|
||||
realtimeServer = new SocketIORealtimeServer({
|
||||
httpServer,
|
||||
io,
|
||||
authenticator,
|
||||
registry,
|
||||
});
|
||||
await realtimeServer.start();
|
||||
|
||||
await new Promise<void>((r) => httpServer.listen(0, r));
|
||||
port = (httpServer.address() as AddressInfo).port;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await realtimeServer.stop();
|
||||
await new Promise<void>((r) => httpServer.close(() => r()));
|
||||
});
|
||||
|
||||
it("authenticated client gets pong after ping", async () => {
|
||||
const client = ioClient(`http://localhost:${port}`, {
|
||||
extraHeaders: { Cookie: "session=valid-session" },
|
||||
});
|
||||
await new Promise<void>((r) => client.on("connect", () => r()));
|
||||
|
||||
const subAck = await new Promise<{ ok: boolean }>((r) =>
|
||||
client.emit("subscribe", "realtime.pong", r),
|
||||
);
|
||||
expect(subAck.ok).toBe(true);
|
||||
|
||||
const pongs: { at: string; echo: string }[] = [];
|
||||
client.on("realtime.pong", (p) => pongs.push(p));
|
||||
|
||||
const sentAt = "2026-05-08T12:00:00.000Z";
|
||||
const pingAck = await new Promise<{ ok: boolean }>((r) =>
|
||||
client.emit("realtime.ping", { at: sentAt }, r),
|
||||
);
|
||||
expect(pingAck.ok).toBe(true);
|
||||
|
||||
// Pong arrives synchronously after handler runs.
|
||||
await new Promise<void>((r) => setImmediate(r));
|
||||
|
||||
expect(pongs).toHaveLength(1);
|
||||
expect(pongs[0]).toEqual({ at: sentAt, echo: "user_test" });
|
||||
|
||||
client.disconnect();
|
||||
});
|
||||
|
||||
it("anonymous client cannot subscribe to pong (authenticated scope)", async () => {
|
||||
const client = ioClient(`http://localhost:${port}`);
|
||||
await new Promise<void>((r) => client.on("connect", () => r()));
|
||||
|
||||
const subAck = await new Promise<{ ok: boolean; error?: string }>((r) =>
|
||||
client.emit("subscribe", "realtime.pong", r),
|
||||
);
|
||||
|
||||
expect(subAck.ok).toBe(false);
|
||||
expect(subAck.error).toBe("forbidden");
|
||||
client.disconnect();
|
||||
});
|
||||
});
|
||||
@@ -7,8 +7,6 @@
|
||||
import "reflect-metadata";
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { bindAllDevSeed, __resetBindStateForTests } from "@/server/bind-production";
|
||||
import { RecordingRealtimeBroadcaster } from "@repo/core-testing/instrumentation";
|
||||
import { RealtimeHandlerRegistry } from "@repo/core-realtime";
|
||||
import { authContainer } from "@repo/auth/di/container";
|
||||
import { AUTH_SYMBOLS } from "@repo/auth/di/symbols";
|
||||
import type { ISignUpController } from "@repo/auth";
|
||||
@@ -22,10 +20,7 @@ describe("e2e: sign-up triggers welcome email via cross-feature event", () => {
|
||||
});
|
||||
|
||||
it("delivers a welcome email after a successful sign-up", async () => {
|
||||
await bindAllDevSeed({
|
||||
realtime: new RecordingRealtimeBroadcaster(),
|
||||
realtimeRegistry: new RealtimeHandlerRegistry(),
|
||||
});
|
||||
await bindAllDevSeed();
|
||||
|
||||
const mailer = marketingPagesContainer.get<RecordingMailerService>(
|
||||
MARKETING_PAGES_SYMBOLS.IMailerService,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { RecordingRealtimeBroadcaster } from "@repo/core-testing/instrumentation";
|
||||
import { RealtimeHandlerRegistry } from "@repo/core-realtime";
|
||||
|
||||
vi.mock("@repo/core-cms", () => ({ default: Promise.resolve({}) }));
|
||||
vi.mock("payload", () => ({ getPayload: vi.fn(async () => ({ jobs: { queue: vi.fn() } })) }));
|
||||
@@ -23,13 +21,6 @@ vi.mock("@repo/core-shared/instrumentation", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
function makeDeps() {
|
||||
return {
|
||||
realtime: new RecordingRealtimeBroadcaster(),
|
||||
realtimeRegistry: new RealtimeHandlerRegistry(),
|
||||
};
|
||||
}
|
||||
|
||||
describe("bindAllProduction", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
@@ -44,7 +35,7 @@ describe("bindAllProduction", () => {
|
||||
const { bindProductionNavigation } = await import("@repo/navigation/di/bind-production");
|
||||
const { bindProductionMedia } = await import("@repo/media/di/bind-production");
|
||||
|
||||
await bindAllProduction(makeDeps());
|
||||
await bindAllProduction();
|
||||
|
||||
expect(bindProductionBlog).toHaveBeenCalledOnce();
|
||||
expect(bindProductionAuth).toHaveBeenCalledOnce();
|
||||
@@ -56,9 +47,8 @@ describe("bindAllProduction", () => {
|
||||
it("is idempotent — second call does not re-bind", async () => {
|
||||
const { bindAllProduction } = await import("./bind-production");
|
||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||
const deps = makeDeps();
|
||||
await bindAllProduction(deps);
|
||||
await bindAllProduction(deps);
|
||||
await bindAllProduction();
|
||||
await bindAllProduction();
|
||||
expect(bindProductionBlog).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
@@ -68,27 +58,12 @@ describe("bindAllProduction", () => {
|
||||
const { PayloadJobsEventBus } = await import("@repo/core-events");
|
||||
const { PayloadJobQueue } = await import("@repo/core-shared/jobs");
|
||||
|
||||
await bindAllProduction(makeDeps());
|
||||
await bindAllProduction();
|
||||
|
||||
const ctx = vi.mocked(bindProductionAuth).mock.calls[0]![0];
|
||||
expect(ctx.bus).toBeInstanceOf(PayloadJobsEventBus);
|
||||
expect(ctx.queue).toBeInstanceOf(PayloadJobQueue);
|
||||
});
|
||||
|
||||
it("forwards realtime and realtimeRegistry as ctx.realtime + ctx.realtimeRegistry to each production binder", async () => {
|
||||
const { RecordingRealtimeBroadcaster } = await import("@repo/core-testing/instrumentation");
|
||||
const { RealtimeHandlerRegistry } = await import("@repo/core-realtime");
|
||||
const { bindAllProduction } = await import("./bind-production");
|
||||
const { bindProductionAuth } = await import("@repo/auth/di/bind-production");
|
||||
|
||||
const realtime = new RecordingRealtimeBroadcaster();
|
||||
const realtimeRegistry = new RealtimeHandlerRegistry();
|
||||
await bindAllProduction({ realtime, realtimeRegistry });
|
||||
|
||||
const ctx = vi.mocked(bindProductionAuth).mock.calls[0]![0];
|
||||
expect(ctx.realtime).toBe(realtime);
|
||||
expect(ctx.realtimeRegistry).toBe(realtimeRegistry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindAllDevSeed", () => {
|
||||
@@ -103,27 +78,12 @@ describe("bindAllDevSeed", () => {
|
||||
const { InMemoryEventBus } = await import("@repo/core-events");
|
||||
const { InMemoryJobQueue } = await import("@repo/core-shared/jobs");
|
||||
|
||||
await bindAllDevSeed(makeDeps());
|
||||
await bindAllDevSeed();
|
||||
|
||||
const ctx = vi.mocked(bindDevSeedAuth).mock.calls[0]![0];
|
||||
expect(ctx.bus).toBeInstanceOf(InMemoryEventBus);
|
||||
expect(ctx.queue).toBeInstanceOf(InMemoryJobQueue);
|
||||
});
|
||||
|
||||
it("forwards realtime and realtimeRegistry as ctx.realtime + ctx.realtimeRegistry to each dev-seed binder", async () => {
|
||||
const { RecordingRealtimeBroadcaster } = await import("@repo/core-testing/instrumentation");
|
||||
const { RealtimeHandlerRegistry } = await import("@repo/core-realtime");
|
||||
const { bindAllDevSeed } = await import("./bind-production");
|
||||
const { bindDevSeedAuth } = await import("@repo/auth/di/bind-dev-seed");
|
||||
|
||||
const realtime = new RecordingRealtimeBroadcaster();
|
||||
const realtimeRegistry = new RealtimeHandlerRegistry();
|
||||
await bindAllDevSeed({ realtime, realtimeRegistry });
|
||||
|
||||
const ctx = vi.mocked(bindDevSeedAuth).mock.calls[0]![0];
|
||||
expect(ctx.realtime).toBe(realtime);
|
||||
expect(ctx.realtimeRegistry).toBe(realtimeRegistry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bindAll dispatcher", () => {
|
||||
@@ -144,7 +104,7 @@ describe("bindAll dispatcher", () => {
|
||||
const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed");
|
||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindDevSeedBlog).toHaveBeenCalledOnce();
|
||||
expect(bindProductionBlog).not.toHaveBeenCalled();
|
||||
@@ -156,7 +116,7 @@ describe("bindAll dispatcher", () => {
|
||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||
const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed");
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindProductionBlog).toHaveBeenCalledOnce();
|
||||
expect(bindDevSeedBlog).not.toHaveBeenCalled();
|
||||
@@ -168,7 +128,7 @@ describe("bindAll dispatcher", () => {
|
||||
const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed");
|
||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindDevSeedBlog).toHaveBeenCalledOnce();
|
||||
expect(bindProductionBlog).not.toHaveBeenCalled();
|
||||
@@ -181,55 +141,13 @@ describe("bindAll dispatcher", () => {
|
||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||
const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed");
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindProductionBlog).toHaveBeenCalledOnce();
|
||||
expect(bindDevSeedBlog).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
@@ -249,7 +167,7 @@ describe("bindAll instrumentation orthogonality (Rule 0, R47)", () => {
|
||||
"@repo/core-shared/instrumentation"
|
||||
);
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindNoopInstrumentation).toHaveBeenCalledOnce();
|
||||
expect(bindSentryInstrumentation).not.toHaveBeenCalled();
|
||||
@@ -263,7 +181,7 @@ describe("bindAll instrumentation orthogonality (Rule 0, R47)", () => {
|
||||
"@repo/core-shared/instrumentation"
|
||||
);
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindSentryInstrumentation).toHaveBeenCalledOnce();
|
||||
expect(bindNoopInstrumentation).not.toHaveBeenCalled();
|
||||
@@ -276,7 +194,7 @@ describe("bindAll instrumentation orthogonality (Rule 0, R47)", () => {
|
||||
const { bindSentryInstrumentation } = await import("@repo/core-shared/instrumentation");
|
||||
const { bindDevSeedBlog } = await import("@repo/blog/di/bind-dev-seed");
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindSentryInstrumentation).toHaveBeenCalledOnce();
|
||||
expect(bindDevSeedBlog).toHaveBeenCalledOnce();
|
||||
@@ -289,7 +207,7 @@ describe("bindAll instrumentation orthogonality (Rule 0, R47)", () => {
|
||||
const { bindNoopInstrumentation } = await import("@repo/core-shared/instrumentation");
|
||||
const { bindProductionBlog } = await import("@repo/blog/di/bind-production");
|
||||
|
||||
await bindAll(makeDeps());
|
||||
await bindAll();
|
||||
|
||||
expect(bindNoopInstrumentation).toHaveBeenCalledOnce();
|
||||
expect(bindProductionBlog).toHaveBeenCalledOnce();
|
||||
|
||||
@@ -7,8 +7,6 @@ import config from "@repo/core-cms";
|
||||
import {
|
||||
bindNoopInstrumentation,
|
||||
bindSentryInstrumentation,
|
||||
withCapture,
|
||||
withSpan,
|
||||
type ITracer,
|
||||
type ILogger,
|
||||
} from "@repo/core-shared/instrumentation";
|
||||
@@ -23,13 +21,6 @@ import {
|
||||
PayloadJobQueue,
|
||||
type IJobQueue,
|
||||
} from "@repo/core-shared/jobs";
|
||||
import {
|
||||
InMemoryRealtimeBroadcaster,
|
||||
RealtimeHandlerRegistry,
|
||||
realtimePingInboundDescriptor,
|
||||
type IRealtimeBroadcaster,
|
||||
type IRealtimeHandlerRegistry,
|
||||
} from "@repo/core-realtime";
|
||||
import { bindProductionBlog } from "@repo/blog/di/bind-production";
|
||||
import { bindProductionAuth } from "@repo/auth/di/bind-production";
|
||||
import { bindProductionMarketingPages } from "@repo/marketing-pages/di/bind-production";
|
||||
@@ -41,11 +32,6 @@ import { bindDevSeedMarketingPages } from "@repo/marketing-pages/di/bind-dev-see
|
||||
import { bindDevSeedNavigation } from "@repo/navigation/di/bind-dev-seed";
|
||||
import { bindDevSeedMedia } from "@repo/media/di/bind-dev-seed";
|
||||
|
||||
type BindAllDeps = {
|
||||
realtime: IRealtimeBroadcaster;
|
||||
realtimeRegistry: IRealtimeHandlerRegistry;
|
||||
};
|
||||
|
||||
let bound = false;
|
||||
|
||||
// Shared container holds TRACER + LOGGER bindings; per-feature containers
|
||||
@@ -112,22 +98,19 @@ function resolveEventsAndJobsDevSeed(): { bus: IEventBus; queue: IJobQueue } {
|
||||
* Payload-backed one. Constructs `new XRepository(config, tracer, logger)` per
|
||||
* feature as Phase E feature wiring lands (blog: task 18; remaining: tasks 19–22).
|
||||
*/
|
||||
export async function bindAllProduction(deps: BindAllDeps): Promise<void> {
|
||||
export async function bindAllProduction(): Promise<void> {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
const { tracer, logger } = resolveInstrumentation(); // Rule 0
|
||||
const { bus, queue } = await resolveEventsAndJobsProduction();
|
||||
const resolvedConfig = await config;
|
||||
const { realtime, realtimeRegistry } = deps;
|
||||
|
||||
const ctx: BindProductionContext<IEventBus, IRealtimeBroadcaster, IRealtimeHandlerRegistry> = {
|
||||
const ctx: BindProductionContext<IEventBus> = {
|
||||
config: resolvedConfig,
|
||||
tracer,
|
||||
logger,
|
||||
bus,
|
||||
queue,
|
||||
realtime,
|
||||
realtimeRegistry,
|
||||
};
|
||||
|
||||
bindProductionAuth(ctx); // Phase E task 19
|
||||
@@ -135,8 +118,6 @@ export async function bindAllProduction(deps: BindAllDeps): Promise<void> {
|
||||
bindProductionMarketingPages(ctx); // Phase E task 20
|
||||
bindProductionNavigation(ctx); // Phase E task 21
|
||||
bindProductionMedia(ctx); // Phase E task 22
|
||||
maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger);
|
||||
bindRealtimeBridge(bus, realtime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,20 +125,17 @@ export async function bindAllProduction(deps: BindAllDeps): Promise<void> {
|
||||
* with realistic seed data so the running app shows non-empty UI without
|
||||
* Payload booted. Mutually exclusive with `bindAllProduction()`.
|
||||
*/
|
||||
export async function bindAllDevSeed(deps: BindAllDeps): Promise<void> {
|
||||
export async function bindAllDevSeed(): Promise<void> {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
const { tracer, logger } = resolveInstrumentation(); // Rule 0
|
||||
const { bus, queue } = resolveEventsAndJobsDevSeed();
|
||||
const { realtime, realtimeRegistry } = deps;
|
||||
|
||||
const ctx: BindContext<IEventBus, IRealtimeBroadcaster, IRealtimeHandlerRegistry> = {
|
||||
const ctx: BindContext<IEventBus> = {
|
||||
tracer,
|
||||
logger,
|
||||
bus,
|
||||
queue,
|
||||
realtime,
|
||||
realtimeRegistry,
|
||||
};
|
||||
|
||||
await bindDevSeedAuth(ctx); // Phase E task 19
|
||||
@@ -165,8 +143,6 @@ export async function bindAllDevSeed(deps: BindAllDeps): Promise<void> {
|
||||
await bindDevSeedMarketingPages(ctx); // Phase E task 20
|
||||
await bindDevSeedNavigation(ctx); // Phase E task 21
|
||||
await bindDevSeedMedia(ctx); // Phase E task 22
|
||||
maybeRegisterRealtimePing(realtimeRegistry, realtime, tracer, logger);
|
||||
bindRealtimeBridge(bus, realtime);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,56 +157,20 @@ export async function bindAllDevSeed(deps: BindAllDeps): Promise<void> {
|
||||
* Rule 2: NODE_ENV === "production" → real Payload via bindAllProduction
|
||||
* Rule 3: otherwise → dev seed (developer-friendly default)
|
||||
*
|
||||
* `deps` is optional; omitting it (page-level fallback, already-bound guard)
|
||||
* uses in-memory noop implementations. In the custom-server flow, `server.ts`
|
||||
* always provides Socket.IO-backed deps before any page request runs.
|
||||
* When @repo/core-realtime is scaffolded, extend this function to accept
|
||||
* realtime deps (IRealtimeBroadcaster, IRealtimeHandlerRegistry) and pass
|
||||
* them through to bindAllProduction / bindAllDevSeed.
|
||||
*/
|
||||
export async function bindAll(deps?: Partial<BindAllDeps>): Promise<void> {
|
||||
const resolvedDeps: BindAllDeps = {
|
||||
realtime: deps?.realtime ?? new InMemoryRealtimeBroadcaster(),
|
||||
realtimeRegistry: deps?.realtimeRegistry ?? new RealtimeHandlerRegistry(),
|
||||
};
|
||||
export async function bindAll(): Promise<void> {
|
||||
if (process.env.USE_DEV_SEED === "true") {
|
||||
await bindAllDevSeed(resolvedDeps);
|
||||
await bindAllDevSeed();
|
||||
return;
|
||||
}
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
await bindAllProduction(resolvedDeps);
|
||||
await bindAllProduction();
|
||||
return;
|
||||
}
|
||||
await bindAllDevSeed(resolvedDeps);
|
||||
}
|
||||
|
||||
// Wraps the built-in realtime-ping inbound handler in the same span+capture
|
||||
// sandwich the realtime-handler generator emits (R41–R44), 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):
|
||||
// bus.subscribe(userSignedUpEvent, "realtime-bridge", async (payload) =>
|
||||
// broadcaster.broadcast(adminEventStreamChannel, { kind: "user.signed-up", payload }),
|
||||
// );
|
||||
await bindAllDevSeed();
|
||||
}
|
||||
|
||||
/** Test-only resets — not exported via package. Used by bind-production.test.ts. */
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user