refactor: remove core-events from main (scaffoldable via gen core-package events)

- Delete packages/core-events/ (15 files)
- Strip @repo/core-events from all 5 feature package.json + apps/web-next/package.json
- Strip @repo/core-events from apps/web-next/next.config.mjs transpilePackages
- Strip E1 + J no-restricted-syntax blocks from core-eslint/base.js (anchor remains)
- Update bind-production.ts: drop bus construction + IEventBus import; rename
  resolveEventsAndJobsProduction → resolveJobsProduction (queue only),
  resolveEventsAndJobsDevSeed → resolveJobsDevSeed (queue only);
  ctx no longer has bus field; BindProductionContext generic arg narrowed
- Update bind-production.test.ts: assert ctx.bus is undefined, drop
  PayloadJobsEventBus/InMemoryEventBus instanceof checks
- Update sign-up-welcome-email.test.ts: assert mailer stays empty without bus
- Inline userSignedUpEvent in auth (drop defineEvent import from core-events)
- Drop InMemoryEventBus fallback from auth/di/module.ts

Feature binders' bus?.subscribe/publish calls remain as no-ops. Scaffold
@repo/core-events back via: pnpm turbo gen core-package events

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-09 14:00:35 +02:00
parent 681fe6ada1
commit a9f27f0d7e
29 changed files with 54 additions and 500 deletions

View File

@@ -7,7 +7,6 @@ const nextConfig = {
"@repo/blog",
"@repo/core-api",
"@repo/core-cms",
"@repo/core-events",
"@repo/core-shared",
"@repo/core-trpc",
"@repo/core-ui",

View File

@@ -18,7 +18,6 @@
"@repo/blog": "workspace:*",
"@repo/core-api": "workspace:*",
"@repo/core-cms": "workspace:*",
"@repo/core-events": "workspace:*",
"@repo/core-shared": "workspace:*",
"@repo/core-trpc": "workspace:*",
"@repo/core-ui": "workspace:*",

View File

@@ -1,8 +1,16 @@
// Cross-feature proof-of-life: signing up in @repo/auth publishes
// userSignedUpEvent on the shared bus, marketing-pages' subscriber enqueues
// a send-welcome-email job on the shared queue, and dev-seed's
// InMemoryJobQueue.register() dispatches it to the wrapped job — which
// records the call on the bound RecordingMailerService.
// Cross-feature event bus proof-of-life (DISABLED — @repo/core-events removed).
//
// @repo/core-events is now optional. When absent, ctx.bus is undefined, and
// bus?.subscribe(...) / bus?.publish(...) calls are no-ops. Cross-feature event
// fanout does not occur until core-events is scaffolded via:
//
// pnpm turbo gen core-package events
//
// After scaffolding, restore this test and re-wire the bus in bind-production.ts
// (see the comment in bindAll()). Until then, signing up does NOT trigger a
// welcome email — the mailer queue stays empty.
//
// Replaced with a reduced test that asserts the no-bus behavior.
import "reflect-metadata";
import { describe, it, expect, beforeEach } from "vitest";
@@ -14,12 +22,12 @@ import { marketingPagesContainer } from "@repo/marketing-pages/di/container";
import { MARKETING_PAGES_SYMBOLS } from "@repo/marketing-pages/di/symbols";
import { RecordingMailerService } from "@repo/marketing-pages/services/recording-mailer";
describe("e2e: sign-up triggers welcome email via cross-feature event", () => {
describe("e2e: sign-up with no event bus (core-events not scaffolded)", () => {
beforeEach(() => {
__resetBindStateForTests();
});
it("delivers a welcome email after a successful sign-up", async () => {
it("sign-up succeeds and mailer stays empty (no cross-feature fanout without bus)", async () => {
await bindAllDevSeed();
const mailer = marketingPagesContainer.get<RecordingMailerService>(
@@ -33,12 +41,10 @@ describe("e2e: sign-up triggers welcome email via cross-feature event", () => {
confirmPassword: "secret_password",
});
// The InMemoryJobQueue dispatches the registered handler on setImmediate;
// wait for microtasks + one setImmediate tick to settle.
// Without a bus, bus?.subscribe() is a no-op so the event handler never
// fires and the mailer receives nothing.
await new Promise((r) => setImmediate(r));
expect(mailer.sent).toEqual([
{ userId: expect.any(String), email: "testuser@example.local" },
]);
expect(mailer.sent).toEqual([]);
});
});

View File

@@ -52,16 +52,15 @@ describe("bindAllProduction", () => {
expect(bindProductionBlog).toHaveBeenCalledOnce();
});
it("passes a Payload-backed bus + queue to each per-feature binder", async () => {
it("passes a Payload-backed queue to each per-feature binder", async () => {
const { bindAllProduction } = await import("./bind-production");
const { bindProductionAuth } = await import("@repo/auth/di/bind-production");
const { PayloadJobsEventBus } = await import("@repo/core-events");
const { PayloadJobQueue } = await import("@repo/core-shared/jobs");
await bindAllProduction();
const ctx = vi.mocked(bindProductionAuth).mock.calls[0]![0];
expect(ctx.bus).toBeInstanceOf(PayloadJobsEventBus);
expect(ctx.bus).toBeUndefined();
expect(ctx.queue).toBeInstanceOf(PayloadJobQueue);
});
});
@@ -72,16 +71,15 @@ describe("bindAllDevSeed", () => {
vi.clearAllMocks();
});
it("passes an in-memory bus + queue to each per-feature dev-seed binder", async () => {
it("passes an in-memory queue (no bus) to each per-feature dev-seed binder", async () => {
const { bindAllDevSeed } = await import("./bind-production");
const { bindDevSeedAuth } = await import("@repo/auth/di/bind-dev-seed");
const { InMemoryEventBus } = await import("@repo/core-events");
const { InMemoryJobQueue } = await import("@repo/core-shared/jobs");
await bindAllDevSeed();
const ctx = vi.mocked(bindDevSeedAuth).mock.calls[0]![0];
expect(ctx.bus).toBeInstanceOf(InMemoryEventBus);
expect(ctx.bus).toBeUndefined();
expect(ctx.queue).toBeInstanceOf(InMemoryJobQueue);
});
});

View File

@@ -11,11 +11,6 @@ import {
type ILogger,
} from "@repo/core-shared/instrumentation";
import type { BindProductionContext, BindContext } from "@repo/core-shared/di";
import {
InMemoryEventBus,
PayloadJobsEventBus,
type IEventBus,
} from "@repo/core-events";
import {
InMemoryJobQueue,
PayloadJobQueue,
@@ -41,7 +36,6 @@ const sharedContainer = new Container();
let resolvedTracer: ITracer | null = null;
let resolvedLogger: ILogger | null = null;
let resolvedBus: IEventBus | null = null;
let resolvedQueue: IJobQueue | null = null;
/** Rule 0: pick instrumentation backend from DSN env (orthogonal to repo mode). */
@@ -59,38 +53,31 @@ function resolveInstrumentation(): { tracer: ITracer; logger: ILogger } {
}
/**
* Production-mode event bus + job queue: backed by Payload's job system so
* events fan out via durable Payload tasks (`PayloadJobsEventBus` enqueues
* `__events.<publisher>.<event>.<consumer>` per subscribed handler) and ad-hoc
* jobs go through `PayloadJobQueue.enqueue`. Cached after first resolution.
* Production-mode job queue: backed by Payload's job system so ad-hoc jobs go
* through `PayloadJobQueue.enqueue`. Cached after first resolution.
*
* Note: @repo/core-events (IEventBus) is optional — scaffold via
* `pnpm turbo gen core-package events` to re-enable cross-feature event fanout.
*/
async function resolveEventsAndJobsProduction(): Promise<{
bus: IEventBus;
queue: IJobQueue;
}> {
if (resolvedBus && resolvedQueue) return { bus: resolvedBus, queue: resolvedQueue };
async function resolveJobsProduction(): Promise<{ queue: IJobQueue }> {
if (resolvedQueue) return { queue: resolvedQueue };
const resolvedConfig = await config;
const payload = await getPayload({ config: resolvedConfig });
const queue = new PayloadJobQueue(payload);
const bus = new PayloadJobsEventBus(queue);
resolvedBus = bus;
resolvedQueue = queue;
return { bus, queue };
return { queue };
}
/**
* Dev-seed mode: in-process bus + queue. Per-feature binders register their
* job handlers via `queue.register(slug, handler)` and subscribe their event
* handlers via `bus.subscribe(...)` at bind time, so dev/test exercises the
* full publish → handler → enqueue path without booting Payload.
* Dev-seed mode: in-process job queue. Per-feature binders register their job
* handlers via `queue.register(slug, handler)` at bind time so dev/test
* exercises the enqueue path without booting Payload.
*/
function resolveEventsAndJobsDevSeed(): { bus: IEventBus; queue: IJobQueue } {
if (resolvedBus && resolvedQueue) return { bus: resolvedBus, queue: resolvedQueue };
function resolveJobsDevSeed(): { queue: IJobQueue } {
if (resolvedQueue) return { queue: resolvedQueue };
const queue = new InMemoryJobQueue();
const bus = new InMemoryEventBus();
resolvedBus = bus;
resolvedQueue = queue;
return { bus, queue };
return { queue };
}
/**
@@ -102,14 +89,13 @@ export async function bindAllProduction(): Promise<void> {
if (bound) return;
bound = true;
const { tracer, logger } = resolveInstrumentation(); // Rule 0
const { bus, queue } = await resolveEventsAndJobsProduction();
const { queue } = await resolveJobsProduction();
const resolvedConfig = await config;
const ctx: BindProductionContext<IEventBus> = {
const ctx: BindProductionContext = {
config: resolvedConfig,
tracer,
logger,
bus,
queue,
};
@@ -129,12 +115,11 @@ export async function bindAllDevSeed(): Promise<void> {
if (bound) return;
bound = true;
const { tracer, logger } = resolveInstrumentation(); // Rule 0
const { bus, queue } = resolveEventsAndJobsDevSeed();
const { queue } = resolveJobsDevSeed();
const ctx: BindContext<IEventBus> = {
const ctx: BindContext = {
tracer,
logger,
bus,
queue,
};
@@ -157,9 +142,10 @@ export async function bindAllDevSeed(): Promise<void> {
* Rule 2: NODE_ENV === "production" → real Payload via bindAllProduction
* Rule 3: otherwise → dev seed (developer-friendly default)
*
* When @repo/core-realtime is scaffolded, extend this function to accept
* realtime deps (IRealtimeBroadcaster, IRealtimeHandlerRegistry) and pass
* them through to bindAllProduction / bindAllDevSeed.
* When @repo/core-events is scaffolded via `pnpm turbo gen core-package events`,
* extend to construct IEventBus and pass it via ctx.bus to per-feature binders.
* When @repo/core-realtime is scaffolded, extend to accept realtime deps
* (IRealtimeBroadcaster, IRealtimeHandlerRegistry) and pass them through.
*/
export async function bindAll(): Promise<void> {
if (process.env.USE_DEV_SEED === "true") {
@@ -178,7 +164,6 @@ export function __resetBindStateForTests(): void {
bound = false;
resolvedTracer = null;
resolvedLogger = null;
resolvedBus = null;
resolvedQueue = null;
}