diff --git a/apps/web-next/next.config.mjs b/apps/web-next/next.config.mjs index 79b66a2..eaa8607 100644 --- a/apps/web-next/next.config.mjs +++ b/apps/web-next/next.config.mjs @@ -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", diff --git a/apps/web-next/package.json b/apps/web-next/package.json index eb5412c..af939c2 100644 --- a/apps/web-next/package.json +++ b/apps/web-next/package.json @@ -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:*", diff --git a/apps/web-next/src/__tests__/sign-up-welcome-email.test.ts b/apps/web-next/src/__tests__/sign-up-welcome-email.test.ts index 530d51d..c197c8d 100644 --- a/apps/web-next/src/__tests__/sign-up-welcome-email.test.ts +++ b/apps/web-next/src/__tests__/sign-up-welcome-email.test.ts @@ -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( @@ -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([]); }); }); diff --git a/apps/web-next/src/server/bind-production.test.ts b/apps/web-next/src/server/bind-production.test.ts index 474f5c5..184ad11 100644 --- a/apps/web-next/src/server/bind-production.test.ts +++ b/apps/web-next/src/server/bind-production.test.ts @@ -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); }); }); diff --git a/apps/web-next/src/server/bind-production.ts b/apps/web-next/src/server/bind-production.ts index 229f5d2..69d2131 100644 --- a/apps/web-next/src/server/bind-production.ts +++ b/apps/web-next/src/server/bind-production.ts @@ -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...` 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 { 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 = { + const ctx: BindProductionContext = { config: resolvedConfig, tracer, logger, - bus, queue, }; @@ -129,12 +115,11 @@ export async function bindAllDevSeed(): Promise { if (bound) return; bound = true; const { tracer, logger } = resolveInstrumentation(); // Rule 0 - const { bus, queue } = resolveEventsAndJobsDevSeed(); + const { queue } = resolveJobsDevSeed(); - const ctx: BindContext = { + const ctx: BindContext = { tracer, logger, - bus, queue, }; @@ -157,9 +142,10 @@ export async function bindAllDevSeed(): Promise { * 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 { 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; } diff --git a/packages/auth/package.json b/packages/auth/package.json index 668dd65..c8229cd 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -20,7 +20,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/packages/auth/src/di/module.ts b/packages/auth/src/di/module.ts index 6a314d5..51f7246 100644 --- a/packages/auth/src/di/module.ts +++ b/packages/auth/src/di/module.ts @@ -1,5 +1,4 @@ import { ContainerModule, type interfaces } from "inversify"; -import { InMemoryEventBus } from "@repo/core-events"; import type { IUsersRepository } from "../application/repositories/users.repository.interface"; import type { IAuthenticationService } from "../application/services/authentication.service.interface"; @@ -45,13 +44,13 @@ export const AuthModule = new ContainerModule((bind: interfaces.Bind) => { ); bind(AUTH_SYMBOLS.ISignUpUseCase).toDynamicValue((ctx) => - // Default fallback uses a fresh InMemoryEventBus — real cross-feature - // wiring runs through bindProductionAuth / bindDevSeedAuth where the - // bindAll() dispatcher passes a shared bus instance. + // No default bus — real cross-feature wiring runs through + // bindProductionAuth / bindDevSeedAuth where bindAll() passes a shared + // bus instance when @repo/core-events is scaffolded. signUpUseCase( ctx.container.get(AUTH_SYMBOLS.IUsersRepository), ctx.container.get(AUTH_SYMBOLS.IAuthenticationService), - new InMemoryEventBus(), + undefined, ), ); diff --git a/packages/auth/src/events/user-signed-up.event.ts b/packages/auth/src/events/user-signed-up.event.ts index 0d26d36..ccb2a0e 100644 --- a/packages/auth/src/events/user-signed-up.event.ts +++ b/packages/auth/src/events/user-signed-up.event.ts @@ -1,6 +1,5 @@ // packages/auth/src/events/user-signed-up.event.ts import { z } from "zod"; -import { defineEvent } from "@repo/core-events"; export const userSignedUpEventSchema = z .object({ @@ -12,7 +11,9 @@ export const userSignedUpEventSchema = z export type UserSignedUpEvent = z.infer; -export const userSignedUpEvent = defineEvent( - "auth.user.signed-up", - userSignedUpEventSchema, -); +// Inline event descriptor — core-events is optional. The shape { name, schema } +// satisfies EventBusProtocol.publish / subscribe when the bus is present. +export const userSignedUpEvent = { + name: "auth.user.signed-up" as const, + schema: userSignedUpEventSchema, +}; diff --git a/packages/blog/package.json b/packages/blog/package.json index b803124..48490ff 100644 --- a/packages/blog/package.json +++ b/packages/blog/package.json @@ -18,7 +18,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/packages/core-eslint/base.js b/packages/core-eslint/base.js index d35a7c7..4dbf05a 100644 --- a/packages/core-eslint/base.js +++ b/packages/core-eslint/base.js @@ -111,64 +111,6 @@ export default [ // Events + jobs rules are added here when @repo/core-events is scaffolded // via `pnpm turbo gen core-package events`. // - { - files: ["**/*.{ts,tsx,mjs,cjs,js}"], - rules: { - "no-restricted-syntax": [ - "error", - { - selector: - "ExportNamedDeclaration[source.value=/\\/events\\/handlers\\//]", - message: - "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", - }, - { - selector: - "ExportAllDeclaration[source.value=/\\/events\\/handlers\\//]", - message: - "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", - }, - { - selector: - "MemberExpression[object.type='MemberExpression'][object.object.type='Identifier'][object.object.name='payload'][object.property.type='Identifier'][object.property.name='jobs']", - message: - "Direct `payload.jobs.*` access is not allowed here. Use IJobQueue (from @repo/core-shared/jobs) instead. Allowed only in **/integrations/cms/jobs/** and **/core-shared/src/jobs/**.", - }, - ], - }, - }, - // J — `payload.jobs.*` is allowed only in the integration layer. - // In these paths, no-restricted-syntax is narrowed to keep E1 active but - // drop the payload.jobs check. - // Note: "**/core-shared/src/jobs/**" does not match from within a package-local - // ESLint run because ESLint resolves globs relative to the config file location. - // The pattern is kept for documentation; in practice, the PayloadJobQueue class - // uses `this.payload.jobs.*` which the selector already ignores (it only catches - // bare `payload.jobs.*`). Any new file added there that does use bare `payload.jobs.*` - // would need this allowlist to be expressed as "**/jobs/payload-*" or similar. - { - files: [ - "**/integrations/cms/jobs/**", - "**/core-shared/src/jobs/**", - ], - rules: { - "no-restricted-syntax": [ - "error", - { - selector: - "ExportNamedDeclaration[source.value=/\\/events\\/handlers\\//]", - message: - "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", - }, - { - selector: - "ExportAllDeclaration[source.value=/\\/events\\/handlers\\//]", - message: - "Event handlers (events/handlers/*.handler.ts) must not be re-exported. Wire them only inside the consumer feature's bind-production / bind-dev-seed (Rule E1).", - }, - ], - }, - }, // R2 / R1 (ADR-016) — realtime-specific ESLint rules (no-direct-socket-io, // no-realtime-handler-reexport) are added here when @repo/core-realtime is // scaffolded via `pnpm turbo gen core-package realtime`. diff --git a/packages/core-events/AGENTS.md b/packages/core-events/AGENTS.md deleted file mode 100644 index 4ea8fe0..0000000 --- a/packages/core-events/AGENTS.md +++ /dev/null @@ -1,9 +0,0 @@ -# @repo/core-events - -Owns the cross-feature event bus: `IEventBus`, `defineEvent`, and two implementations (`InMemoryEventBus`, `PayloadJobsEventBus`). - -**Boundary tag:** core. May be imported by feature, core, core-composition, app. May import from core-shared, tooling. - -**Public surface:** `IEventBus`, `EventDescriptor`, `defineEvent`, `EventHandler`, `CORE_EVENTS_SYMBOLS`, both implementations. - -**See:** `docs/decisions/adr-015-events-and-jobs.md` (pending), `docs/guides/events-and-jobs.md` (pending), `docs/superpowers/specs/2026-05-08-events-and-jobs-design.md`. diff --git a/packages/core-events/eslint.config.js b/packages/core-events/eslint.config.js deleted file mode 100644 index 7440d8f..0000000 --- a/packages/core-events/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import baseConfig from "@repo/core-eslint/base"; - -export default baseConfig; diff --git a/packages/core-events/package.json b/packages/core-events/package.json deleted file mode 100644 index 31c4be8..0000000 --- a/packages/core-events/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@repo/core-events", - "version": "0.0.1", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "scripts": { - "build": "tsc --noEmit", - "lint": "eslint .", - "typecheck": "tsc --noEmit", - "test": "vitest run" - }, - "dependencies": { - "@repo/core-shared": "workspace:*", - "zod": "^3.23.0" - }, - "peerDependencies": { - "payload": "^3.0.0" - }, - "peerDependenciesMeta": { - "payload": { "optional": true } - }, - "devDependencies": { - "@repo/core-eslint": "workspace:*", - "@repo/core-testing": "workspace:*", - "@repo/core-typescript": "workspace:*", - "typescript": "^5.8.0", - "vitest": "^3.0.0" - } -} diff --git a/packages/core-events/src/event-bus.interface.ts b/packages/core-events/src/event-bus.interface.ts deleted file mode 100644 index abe70df..0000000 --- a/packages/core-events/src/event-bus.interface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { z } from "zod"; -import type { EventBusProtocol } from "@repo/core-shared/di/bind-protocols"; -import type { EventDescriptor } from "./event-descriptor"; - -export type EventHandler = (event: T) => Promise; - -export interface IEventBus extends EventBusProtocol { - publish( - descriptor: EventDescriptor>, - payload: T, - ): Promise; - - /** - * Subscribe a handler. `consumerFeature` is the kebab-case name of the - * subscribing feature (e.g., "marketing-pages"). It is unused by - * InMemoryEventBus; PayloadJobsEventBus uses it to name the fan-out task - * slug deterministically (`__events..`). - */ - subscribe( - descriptor: EventDescriptor>, - consumerFeature: string, - handler: EventHandler, - ): void; -} diff --git a/packages/core-events/src/event-descriptor.test.ts b/packages/core-events/src/event-descriptor.test.ts deleted file mode 100644 index 1dfc7ed..0000000 --- a/packages/core-events/src/event-descriptor.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { z } from "zod"; -import { defineEvent } from "@/event-descriptor"; - -describe("defineEvent", () => { - it("returns a descriptor with name and schema", () => { - const schema = z.object({ id: z.string() }).strict(); - const descriptor = defineEvent("test.thing.happened", schema); - expect(descriptor.name).toBe("test.thing.happened"); - expect(descriptor.schema).toBe(schema); - }); - - it("descriptor.schema parses valid payloads", () => { - const schema = z.object({ id: z.string() }).strict(); - const d = defineEvent("test.evt", schema); - expect(() => d.schema.parse({ id: "abc" })).not.toThrow(); - }); - - it("descriptor.schema rejects invalid payloads", () => { - const schema = z.object({ id: z.string() }).strict(); - const d = defineEvent("test.evt", schema); - expect(() => d.schema.parse({ id: 123 })).toThrow(); - }); -}); diff --git a/packages/core-events/src/event-descriptor.ts b/packages/core-events/src/event-descriptor.ts deleted file mode 100644 index 56cd3c7..0000000 --- a/packages/core-events/src/event-descriptor.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { z } from "zod"; - -export type EventDescriptor = { - readonly name: TName; - readonly schema: TSchema; -}; - -export function defineEvent( - name: TName, - schema: TSchema, -): EventDescriptor { - return { name, schema }; -} diff --git a/packages/core-events/src/in-memory-event-bus.test.ts b/packages/core-events/src/in-memory-event-bus.test.ts deleted file mode 100644 index 1ce7f4f..0000000 --- a/packages/core-events/src/in-memory-event-bus.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { z } from "zod"; -import { defineEvent } from "@/event-descriptor"; -import { InMemoryEventBus } from "@/in-memory-event-bus"; - -const evt = defineEvent("test.thing", z.object({ id: z.string() }).strict()); - -describe("InMemoryEventBus", () => { - it("validates the payload via the descriptor's schema before fanout", async () => { - const bus = new InMemoryEventBus(); - const handler = vi.fn(); - bus.subscribe(evt, "test-consumer", handler); - await expect(bus.publish(evt, { id: 123 } as unknown as { id: string })).rejects.toThrow(); - expect(handler).not.toHaveBeenCalled(); - }); - - it("delivers to all registered handlers in parallel", async () => { - const bus = new InMemoryEventBus(); - const a = vi.fn(); - const b = vi.fn(); - bus.subscribe(evt, "consumer-a", a); - bus.subscribe(evt, "consumer-b", b); - await bus.publish(evt, { id: "x" }); - expect(a).toHaveBeenCalledWith({ id: "x" }); - expect(b).toHaveBeenCalledWith({ id: "x" }); - }); - - it("swallows handler errors by default (publisher's publish does not throw)", async () => { - const bus = new InMemoryEventBus(); - bus.subscribe(evt, "boom", async () => { - throw new Error("subscriber blew up"); - }); - await expect(bus.publish(evt, { id: "x" })).resolves.toBeUndefined(); - }); - - it("rethrows the first handler error when failFast is true", async () => { - const bus = new InMemoryEventBus({ failFast: true }); - bus.subscribe(evt, "first", async () => { - throw new Error("first failure"); - }); - bus.subscribe(evt, "second", vi.fn()); - await expect(bus.publish(evt, { id: "x" })).rejects.toThrow("first failure"); - }); - - it("delivers nothing when no handlers are registered", async () => { - const bus = new InMemoryEventBus(); - await expect(bus.publish(evt, { id: "x" })).resolves.toBeUndefined(); - }); -}); diff --git a/packages/core-events/src/in-memory-event-bus.ts b/packages/core-events/src/in-memory-event-bus.ts deleted file mode 100644 index 6ac36e8..0000000 --- a/packages/core-events/src/in-memory-event-bus.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { z } from "zod"; -import type { EventDescriptor } from "./event-descriptor"; -import type { EventHandler, IEventBus } from "./event-bus.interface"; - -export type InMemoryEventBusOptions = { - /** When true, rethrow the first handler error (default: false — errors swallowed). */ - failFast?: boolean; -}; - -export class InMemoryEventBus implements IEventBus { - private readonly handlers = new Map[]>(); - - constructor(private readonly options: InMemoryEventBusOptions = {}) {} - - async publish( - descriptor: EventDescriptor>, - payload: T, - ): Promise { - descriptor.schema.parse(payload); - const subscribers = this.handlers.get(descriptor.name) ?? []; - if (subscribers.length === 0) return; - const settled = await Promise.allSettled( - subscribers.map((h) => h(payload)), - ); - if (this.options.failFast) { - const failure = settled.find((s) => s.status === "rejected"); - // Only the first rejection is rethrown. Other failures are intentionally - // dropped — `failFast` is a test-affordance, not a fault-tolerance design. - if (failure && failure.status === "rejected") throw failure.reason; - } - } - - subscribe( - descriptor: EventDescriptor>, - _consumerFeature: string, - handler: EventHandler, - ): void { - const arr = this.handlers.get(descriptor.name) ?? []; - arr.push(handler as EventHandler); - this.handlers.set(descriptor.name, arr); - } -} diff --git a/packages/core-events/src/index.ts b/packages/core-events/src/index.ts deleted file mode 100644 index d8e4acf..0000000 --- a/packages/core-events/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type { EventDescriptor } from "./event-descriptor"; -export { defineEvent } from "./event-descriptor"; -export type { IEventBus, EventHandler } from "./event-bus.interface"; -export { CORE_EVENTS_SYMBOLS } from "./symbols"; -export { InMemoryEventBus, type InMemoryEventBusOptions } from "./in-memory-event-bus"; -export { PayloadJobsEventBus } from "./payload-jobs-event-bus"; diff --git a/packages/core-events/src/payload-jobs-event-bus.test.ts b/packages/core-events/src/payload-jobs-event-bus.test.ts deleted file mode 100644 index 87b5a06..0000000 --- a/packages/core-events/src/payload-jobs-event-bus.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { z } from "zod"; -import { defineEvent } from "@/event-descriptor"; -import { PayloadJobsEventBus } from "@/payload-jobs-event-bus"; -import type { IJobQueue } from "@repo/core-shared/jobs"; - -const evt = defineEvent("auth.user.signed-up", z.object({ userId: z.string() }).strict()); - -function recordingQueue(): IJobQueue & { enqueued: { taskSlug: string; input: unknown }[] } { - const enqueued: { taskSlug: string; input: unknown }[] = []; - const q: IJobQueue = { - async enqueue(taskSlug, input) { - enqueued.push({ taskSlug, input }); - return { jobId: `recording-${enqueued.length}` }; - }, - }; - return Object.assign(q, { enqueued }); -} - -describe("PayloadJobsEventBus", () => { - it("validates the payload before enqueueing", async () => { - const queue = recordingQueue(); - const bus = new PayloadJobsEventBus(queue); - bus.subscribe(evt, "marketing-pages", vi.fn()); - await expect( - bus.publish(evt, { userId: 42 } as unknown as { userId: string }), - ).rejects.toThrow(); - expect(queue.enqueued).toHaveLength(0); - }); - - it("enqueues one task per subscriber, naming `__events..`", async () => { - const queue = recordingQueue(); - const bus = new PayloadJobsEventBus(queue); - bus.subscribe(evt, "marketing-pages", vi.fn()); - bus.subscribe(evt, "blog", vi.fn()); - await bus.publish(evt, { userId: "u1" }); - expect(queue.enqueued).toHaveLength(2); - expect(queue.enqueued.map((e) => e.taskSlug).sort()).toEqual([ - "__events.auth.user.signed-up.blog", - "__events.auth.user.signed-up.marketing-pages", - ]); - expect(queue.enqueued[0]!.input).toEqual({ userId: "u1" }); - }); - - it("enqueues nothing when no subscribers are registered", async () => { - const queue = recordingQueue(); - const bus = new PayloadJobsEventBus(queue); - await bus.publish(evt, { userId: "u1" }); - expect(queue.enqueued).toHaveLength(0); - }); -}); diff --git a/packages/core-events/src/payload-jobs-event-bus.ts b/packages/core-events/src/payload-jobs-event-bus.ts deleted file mode 100644 index 20227bb..0000000 --- a/packages/core-events/src/payload-jobs-event-bus.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { z } from "zod"; -import type { IJobQueue } from "@repo/core-shared/jobs"; -import type { EventDescriptor } from "./event-descriptor"; -import type { EventHandler, IEventBus } from "./event-bus.interface"; - -/** - * Production-grade bus: for each subscriber, enqueues one Payload task per - * `publish()` call. Subscribers register with their consumer-feature name so - * fan-out tasks are named deterministically: `__events..`. - * The actual handler invocation happens inside Payload's job runner — see the - * matching task config generated by `gen event consume` (Task 39). - */ -export class PayloadJobsEventBus implements IEventBus { - private readonly subscribers = new Map(); - - constructor(private readonly queue: IJobQueue) {} - - async publish( - descriptor: EventDescriptor>, - payload: T, - ): Promise { - descriptor.schema.parse(payload); - const consumers = this.subscribers.get(descriptor.name) ?? []; - await Promise.all( - consumers.map((consumerFeature) => - this.queue.enqueue( - `__events.${descriptor.name}.${consumerFeature}`, - payload, - ), - ), - ); - } - - subscribe( - descriptor: EventDescriptor>, - consumerFeature: string, - _handler: EventHandler, - ): void { - const arr = this.subscribers.get(descriptor.name) ?? []; - if (!arr.includes(consumerFeature)) arr.push(consumerFeature); - this.subscribers.set(descriptor.name, arr); - } -} diff --git a/packages/core-events/src/symbols.ts b/packages/core-events/src/symbols.ts deleted file mode 100644 index 10bb1a7..0000000 --- a/packages/core-events/src/symbols.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const CORE_EVENTS_SYMBOLS = { - IEventBus: Symbol.for("@repo/core-events/IEventBus"), -} as const; diff --git a/packages/core-events/tsconfig.json b/packages/core-events/tsconfig.json deleted file mode 100644 index 652e804..0000000 --- a/packages/core-events/tsconfig.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "@repo/core-typescript/base.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": ".", - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/core-events/turbo.json b/packages/core-events/turbo.json deleted file mode 100644 index dcb8fb3..0000000 --- a/packages/core-events/turbo.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["//"], - "tags": ["core"] -} diff --git a/packages/core-events/vitest.config.ts b/packages/core-events/vitest.config.ts deleted file mode 100644 index 2ee07c1..0000000 --- a/packages/core-events/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import path from "node:path"; -import { mergeConfig } from "vitest/config"; -import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node"; - -export default mergeConfig(nodeVitestConfig, { - resolve: { - alias: { "@": path.resolve(__dirname, "./src") }, - }, -}); diff --git a/packages/marketing-pages/package.json b/packages/marketing-pages/package.json index 5afb693..c0ec6e9 100644 --- a/packages/marketing-pages/package.json +++ b/packages/marketing-pages/package.json @@ -23,7 +23,6 @@ }, "dependencies": { "@repo/auth": "workspace:*", - "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/packages/media/package.json b/packages/media/package.json index 55f15fe..8adc0f7 100644 --- a/packages/media/package.json +++ b/packages/media/package.json @@ -18,7 +18,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/packages/navigation/package.json b/packages/navigation/package.json index 3a24e74..bb53384 100644 --- a/packages/navigation/package.json +++ b/packages/navigation/package.json @@ -18,7 +18,6 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@repo/core-events": "workspace:*", "@repo/core-shared": "workspace:*", "@trpc/server": "^11.0.0", "inversify": "^6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 34ae4c1..89fe736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -154,9 +154,6 @@ importers: '@repo/core-cms': specifier: workspace:* version: link:../../packages/core-cms - '@repo/core-events': - specifier: workspace:* - version: link:../../packages/core-events '@repo/core-shared': specifier: workspace:* version: link:../../packages/core-shared @@ -330,9 +327,6 @@ importers: packages/auth: dependencies: - '@repo/core-events': - specifier: workspace:* - version: link:../core-events '@repo/core-shared': specifier: workspace:* version: link:../core-shared @@ -373,9 +367,6 @@ importers: packages/blog: dependencies: - '@repo/core-events': - specifier: workspace:* - version: link:../core-events '@repo/core-shared': specifier: workspace:* version: link:../core-shared @@ -531,34 +522,6 @@ importers: specifier: ^3.1.0 version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0) - packages/core-events: - dependencies: - '@repo/core-shared': - specifier: workspace:* - version: link:../core-shared - payload: - specifier: ^3.0.0 - version: 3.81.0(graphql@16.13.2)(typescript@5.9.3) - zod: - specifier: ^3.23.0 - version: 3.25.76 - devDependencies: - '@repo/core-eslint': - specifier: workspace:* - version: link:../core-eslint - '@repo/core-testing': - specifier: workspace:* - version: link:../core-testing - '@repo/core-typescript': - specifier: workspace:* - version: link:../core-typescript - typescript: - specifier: ^5.8.0 - version: 5.9.3 - vitest: - specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0) - packages/core-shared: dependencies: '@sentry/nextjs': @@ -775,9 +738,6 @@ importers: '@repo/auth': specifier: workspace:* version: link:../auth - '@repo/core-events': - specifier: workspace:* - version: link:../core-events '@repo/core-shared': specifier: workspace:* version: link:../core-shared @@ -818,9 +778,6 @@ importers: packages/media: dependencies: - '@repo/core-events': - specifier: workspace:* - version: link:../core-events '@repo/core-shared': specifier: workspace:* version: link:../core-shared @@ -858,9 +815,6 @@ importers: packages/navigation: dependencies: - '@repo/core-events': - specifier: workspace:* - version: link:../core-events '@repo/core-shared': specifier: workspace:* version: link:../core-shared