From 990f641425e5108e548dee94c334200403ef4152 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Sat, 9 May 2026 12:51:15 +0200 Subject: [PATCH] docs(html): di + data-flow explainers reflect BindContext --- docs/architecture/data-flow-explainer.html | 13 +++++++------ docs/architecture/di-explainer.html | 6 ++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/architecture/data-flow-explainer.html b/docs/architecture/data-flow-explainer.html index 7b9691d..8c53af1 100644 --- a/docs/architecture/data-flow-explainer.html +++ b/docs/architecture/data-flow-explainer.html @@ -1664,7 +1664,7 @@ footer .colophon {

Two binding modes, one symbol.

-

The BlogModule binds IArticlesRepository to MockArticlesRepository by default — useful at dev/test time. At app boot, bindProductionBlog(config, tracer, logger, bus, queue) unbinds the symbol and rebinds it to new ArticlesRepository(config, tracer, logger). Use cases and controllers don't notice — they get whatever the symbol currently resolves to.

+

The BlogModule binds IArticlesRepository to MockArticlesRepository by default — useful at dev/test time. At app boot, bindProductionBlog(ctx: BindProductionContext) unbinds the symbol and rebinds it to new ArticlesRepository(ctx.config, ctx.tracer, ctx.logger). Use cases and controllers don't notice — they get whatever the symbol currently resolves to. The ctx object is built once by the app aggregator and passed to all feature binders.

This is also why the boundary stays clean: features don't import core-cms; the app passes the Payload config in.

@@ -1701,8 +1701,9 @@ footer .colophon {
app boot · production override

blog/di/bind-production.ts

-

Called from each app's bootstrap (apps/web-next/src/server/bind-production.ts) with the resolved Payload config.

-
export function bindProductionBlog(config: SanitizedConfig): void {
+      

Called from each app's bootstrap (apps/web-next/src/server/bind-production.ts) with the ctx object built once by the aggregator. BindProductionContext is imported from @repo/core-shared/di.

+
export function bindProductionBlog(ctx: BindProductionContext): void {
+  const { config, tracer, logger, bus, queue, realtime, realtimeRegistry } = ctx;
   if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) {
     blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository);
   }
@@ -1773,7 +1774,7 @@ footer .colophon {
 
     

The mock is reached from two directions. Both are legitimate, neither is "the test version":

    -
  1. By the DI container at runtime. BlogModule binds IArticlesRepository to MockArticlesRepository at module-load time. Anything resolving that symbol — use cases, controllers, tRPC procedures, the dev server — gets the mock until bindProductionBlog(config, tracer, logger, bus, queue) swaps it for the real Payload-backed one. See §03.
  2. +
  3. By the DI container at runtime. BlogModule binds IArticlesRepository to MockArticlesRepository at module-load time. Anything resolving that symbol — use cases, controllers, tRPC procedures, the dev server — gets the mock until bindProductionBlog(ctx: BindProductionContext) swaps it for the real Payload-backed one. See §03.
  4. By tests, via direct construction. Unit tests skip the container entirely. They construct the mock with new MockArticlesRepository() and pass it directly into the use-case factory function. Same class, different consumer — just a closure with a fake repo.
@@ -1786,7 +1787,7 @@ footer .colophon {
show: the mock as DI binding (in module.ts)
-

This is from packages/blog/src/di/module.ts — the very first binding in the module is the mock. Everything downstream (use cases, controllers) resolves through this default. bindProductionBlog(config, tracer, logger, bus, queue) later replaces only this one line at app boot — use case + controller bindings stay put.

+

This is from packages/blog/src/di/module.ts — the very first binding in the module is the mock. Everything downstream (use cases, controllers) resolves through this default. bindProductionBlog(ctx: BindProductionContext) later replaces only this one line at app boot — use case + controller bindings stay put.

export const BlogModule = new ContainerModule((bind) => {
   // 1) Mock is the DEFAULT binding for the repo symbol.
   //    Dev server, unit tests, storybook all resolve to this.
@@ -2191,7 +2192,7 @@ footer .colophon {
     
di/bind-production.ts

Production binder

-

bindProduction<F>(config) — unbinds the mock, rebinds the real Payload-backed impl.

+

bindProduction<F>(ctx: BindProductionContext) — unbinds the mock, rebinds the real Payload-backed impl. The ctx arg carries required fields (tracer, logger, config) and optional cross-cutting deps (bus, queue, realtime, realtimeRegistry).

Pros
  • Decouples Payload config from the feature package — boundary stays clean
  • diff --git a/docs/architecture/di-explainer.html b/docs/architecture/di-explainer.html index d6e0294..9631de9 100644 --- a/docs/architecture/di-explainer.html +++ b/docs/architecture/di-explainer.html @@ -756,7 +756,7 @@ footer .colophon {
    file 04 · prod swap

    bind-production.ts

    Replaces the mock repository binding with a real Payload-backed one at app boot.

    -

    Exports bindProductionBlog(config, tracer, logger, bus, queue). Function body: blogContainer.unbind(symbol) if already bound, then .bind(symbol).toConstantValue(new ArticlesRepository(config, tracer, logger)). Use cases and controllers are wrapped via withSpan(withCapture(factory(deps))) at bind time so they inherit instrumentation without changing their factory bodies. The bus and queue params come from the app's resolveEventsAndJobs* step (ADR-015) and feed the // <gen:event-handlers> / // <gen:jobs> injection sites.

    +

    Exports bindProductionBlog(ctx: BindProductionContext). Destructures { config, tracer, logger, bus, queue, realtime, realtimeRegistry } from ctx. Function body: blogContainer.unbind(symbol) if already bound, then .bind(symbol).toConstantValue(new ArticlesRepository(config, tracer, logger)). Use cases and controllers are wrapped via withSpan(withCapture(factory(deps))) at bind time so they inherit instrumentation without changing their factory bodies. The optional bus and queue fields come from the app's resolveEventsAndJobs* step (ADR-015) and feed the // <gen:event-handlers> / // <gen:jobs> injection sites. BindProductionContext is imported from @repo/core-shared/di — features never import the optional packages directly for the binder signature.

    When it runs: called from app boot (apps/web-next/src/server/bind-production.ts) when USE_DEV_SEED ≠ "true" AND Payload config is resolvable.
@@ -1190,7 +1190,9 @@ footer .colophon { └─ Noop or Sentry binders ← bind to sharedContainer └─ resolveEventsAndJobs*() ← ADR-015 (env-driven bus + queue) └─ Payload-backed in prod, in-memory in dev-seed - └─ bindProductionX(config, tracer, logger, bus, queue) + └─ build ctx: BindProductionContext = { config, tracer, logger, bus, queue, realtime, realtimeRegistry } + └─ required: tracer, logger, config | optional: bus, queue, realtime, realtimeRegistry + └─ bindProductionX(ctx) ← single ctx object passed to each feature binder └─ feature container also binds TRACER + LOGGER └─ withSpan(withCapture(...)) at every use case + controller └─ // <gen:event-handlers> / // <gen:jobs> injection sites