@sincpro/client-framework 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/AGENTS.md +27 -10
  2. package/MANIFESTO.md +8 -6
  3. package/README.md +163 -58
  4. package/dist/client/services/guarded.d.ts +1 -1
  5. package/dist/codegen/services/scaffold.d.ts +1 -1
  6. package/dist/codegen/services/scaffold.js +12 -17
  7. package/dist/context/entrypoint/context.d.ts +5 -4
  8. package/dist/core/domain/dto.d.ts +3 -3
  9. package/dist/core/domain/dto.js +3 -3
  10. package/dist/core/domain/errors.d.ts +2 -2
  11. package/dist/core/domain/pipeline.d.ts +3 -3
  12. package/dist/core/domain/services.d.ts +5 -4
  13. package/dist/core/domain/services.js +1 -1
  14. package/dist/core/entrypoint/bounded-context.d.ts +24 -13
  15. package/dist/core/entrypoint/bounded-context.js +46 -15
  16. package/dist/core/infrastructure/listeners.d.ts +1 -1
  17. package/dist/core/infrastructure/listeners.js +1 -1
  18. package/dist/core/services/execution.d.ts +33 -17
  19. package/dist/core/services/execution.js +54 -38
  20. package/dist/event_driven/adapters/stored-queue.d.ts +5 -4
  21. package/dist/event_driven/adapters/stored-queue.js +5 -4
  22. package/dist/event_driven/entrypoint/publisher.d.ts +1 -1
  23. package/dist/event_driven/entrypoint/publisher.js +1 -1
  24. package/dist/event_driven/entrypoint/subscriber.d.ts +14 -6
  25. package/dist/event_driven/entrypoint/subscriber.js +16 -5
  26. package/dist/event_driven/entrypoint/subscriptions.d.ts +110 -0
  27. package/dist/event_driven/entrypoint/subscriptions.js +184 -0
  28. package/dist/event_driven/index.d.ts +4 -1
  29. package/dist/event_driven/index.js +4 -1
  30. package/dist/observability/adapters/memory.d.ts +1 -1
  31. package/dist/observability/domain/logging.d.ts +8 -4
  32. package/dist/observability/services/logging.d.ts +7 -4
  33. package/dist/observability/services/logging.js +2 -2
  34. package/package.json +1 -1
package/AGENTS.md CHANGED
@@ -33,7 +33,7 @@ module has only the layers it needs, and says what it offers in its own `index.t
33
33
  | `src/client/` | `Transport`, `entity()`, the contract (domain); REST, memory, Parquet; discovery, propagation |
34
34
  | `src/model/` | Model definitions: `defineMeta`, from a JSON Schema, the store of known ones |
35
35
  | `src/core/` | DTOs and events, bus, services, handlers, extension points; pipeline; `BoundedContext`; process |
36
- | `src/event_driven/` | `Envelope`, `EventQueue`/`Subscribes` ports; `syncQueue`; `Publisher`, `Subscriber` |
36
+ | `src/event_driven/` | `Envelope`, `EventQueue`/`Subscribes` ports; queues; `Publisher`, `Subscriber`, `Subscriptions` |
37
37
  | `src/codegen/` | The generator and its CLI (`sincpro-client generate`) — the only place Node is allowed |
38
38
  | `src/testing/` | The public entry of the in-memory observability adapters |
39
39
  | `tests/` | vitest in Node, one folder per module; `tests/support/` holds fakes and forge's contracts |
@@ -78,18 +78,35 @@ module has only the layers it needs, and says what it offers in its own `index.t
78
78
  unless `Service` is registered now for `dto`; its spans carry `sincpro.replaces`. A service
79
79
  meant to be subclassed declares `static readonly serviceName: string = "…"`, or the literal
80
80
  type stops a subclass from renaming itself.
81
- - **How events are carried is here; where they are kept is the platform's.** One subscriber
82
- per bounded context; a `Subscriber` executes an event in each. `storedQueue` holds the rules —
83
- store first, deliver in order, retry what may succeed, set aside what never will, never run a
84
- context twice — over an `EventStore` the platform plugs (SQLite, IndexedDB, memory). A store
85
- only keeps records; it never decides.
81
+ - **How events are carried is here; where they are kept is the platform's.** One use case of an
82
+ event per bounded context, one `EventSubscriber` per event per `Subscriptions`; a `Subscriber`
83
+ executes an event in each. `storedQueue` holds the rules — store first, deliver in order,
84
+ retry what may succeed, set aside what never will, never run one who heard it twice — over an
85
+ `EventStore` the platform plugs (SQLite, IndexedDB, memory). A store only keeps records; it
86
+ never decides.
87
+ - **A subscriber is an entrypoint, not a use case.** An `EventSubscriber` reads the event and
88
+ executes Commands on a bounded context it was given, as a cron does on a tick; what to decide,
89
+ and a workflow of several use cases, is a Feature or an ApplicationService it executes. Its run
90
+ is layer `entrypoint`: no interceptor wraps it, no listener hears it, no error handler answers
91
+ it. An event may instead be the input of a use case (`feature(Event)(Service)`) when the use
92
+ case exists because of it. A `Subscriptions` is named apart from every bounded context: the
93
+ queue keeps who heard an event by name, and `Subscriber` refuses two of one name.
94
+ - **A bounded context injected into a run is bound to it** (`BIND`): what it executes is a child
95
+ of the caller — span, flow, cause, signal, one report — with its own dependencies, context and
96
+ handlers. A failure keeps the outcome where it first failed, so a caller does not report what
97
+ the callee expected.
98
+ - **A bounded context is named for what it is.** `salesBoundedContext`, never `sales` (an
99
+ entity?) and never `ctx` or `bc`: `context` is already the run's. The scaffold writes it so.
86
100
  - **A circuit belongs where the dependency is.** `CircuitBreaker` (in the process's memory, the
87
101
  backend's semantics: `COUNTED_KINDS`, half-open with one probe) guards a transport
88
102
  (`guardedTransport`) or an adapter; its `CircuitOpen` declares `unavailable` and a wait, so
89
103
  nothing else needs to know a circuit exists.
90
- - **Delivery is at least once.** A stored event may run twice in a context after a crash; a
91
- subscriber keys its effect by `this.execution.causationId` (the envelope's id). `maxAttempts`
92
- sets aside what keeps failing. One process drains a store. `execution` is reserved.
104
+ - **Delivery is at least once.** A stored event may run twice after a crash. A use case of the
105
+ event keys its effect by `this.execution.causationId` (the envelope's id); an `EventSubscriber`
106
+ finds the same id in its own run and hands it in the Command it executes — the use case's
107
+ `causationId` is the subscriber's run —, or the effect is keyed by the entity the event names.
108
+ `maxAttempts` sets aside what keeps failing. One process drains a store. `execution` is
109
+ reserved.
93
110
  - **A failure says its kind, whatever the wire.** `failureOf` reads a declared `failureKind`, the
94
111
  `Problem` (the same over REST, JSON-RPC and gRPC), then a REST status. A transport of another
95
112
  wire, or another backend's adapter, declares the kind with `kindOfGrpcCode` /
@@ -153,7 +170,7 @@ options)`: no route, no verb, no RPC method. The contract's bindings (`rest`, `r
153
170
  - **An error handler answers; it does not observe.** The failure is recorded where it happened —
154
171
  span, log line, report — before the layer's handlers run, as in sincpro_framework. A handler
155
172
  returns the answer or throws to the next; the global chain wraps only `execute` and `scoped`.
156
- - **The application layer imports no logger.** A bounded context has `project.logger`; a run
173
+ - **The application layer imports no logger.** A bounded context has `projectBoundedContext.logger`; a run
157
174
  hands `this.logger`, bound to its execution. Only `logger` (infrastructure, the global one) and
158
175
  `loggerAdapter` are predefined; injected as a dependency they bind to the run (`BIND`), imported
159
176
  at module level they do not. No logger is predefined for an application layer. Console is the
package/MANIFESTO.md CHANGED
@@ -48,7 +48,7 @@ server-side render, in a script and in a test, unchanged.**
48
48
  in a global stack: two calls at once never mix their parents.
49
49
  5. **Bounded contexts.** Set up like the backend's — its dependencies (`addDependency`), its
50
50
  Features and ApplicationServices, registered for class DTOs — and called as
51
- `project.execute(new Command(...))`. The bounded context is the bus. An application is the set
51
+ `projectBoundedContext.execute(new Command(...))`. The bounded context is the bus. An application is the set
52
52
  of bounded contexts it loads; they never import each other's insides.
53
53
  6. **The client.** The `OperationTransport` port — an operation called by who it is, over any
54
54
  wire — with its REST and JSON-RPC adapters (a platform adds gRPC or its own); the `Transport`
@@ -67,16 +67,18 @@ server-side render, in a script and in a test, unchanged.**
67
67
  10. **The command core (`./core`).** The single model matching the backend's application
68
68
  abstractions: class DTOs and Responses, a bounded context that is a bus, Features and
69
69
  ApplicationServices registered for DTO classes, attribute-injected dependencies and a
70
- feature bus for composition, domain events executed by their subscribers, replacements,
70
+ feature bus for composition, domain events as the input of a use case, replacements,
71
71
  and the backend's error handlers — global, feature and application service — with the
72
72
  errors a bounded context expects. The internal engine uses
73
73
  the injected process ports. Remote clients and mapping remain distinct.
74
74
  11. **Event-driven, as ports (`event_driven`).** How every platform publishes domain events and
75
75
  subscribes to them the same way: the envelope that travels beside an event (its flow and
76
- its cause), the `EventQueue` and `Subscribes` ports, a `Publisher` a Feature holds, a
77
- `Subscriber` over the bounded contexts that hear, the queue that delivers in the call, and
78
- the stored queue's rules over an `EventStore` the platform plugs. The guidelines, the rules
79
- and the contracts are here; where events are kept is the platform's.
76
+ its cause), the `EventQueue` and `Subscribes` ports, a `Publisher` a Feature holds, the
77
+ `EventSubscriber` — the entrypoint that hears an event and calls use cases, as a cron does on
78
+ a tick — and its `Subscriptions`, a `Subscriber` over everyone who hears, the queue that
79
+ delivers in the call, and the stored queue's rules over an `EventStore` the platform plugs.
80
+ The guidelines, the rules and the contracts are here; where events are kept is the
81
+ platform's.
80
82
  12. **Resilience.** What a failure is (`failureOf`, the backend's kinds on every wire) and the
81
83
  backend's circuit breaker, guarding any transport or adapter.
82
84
 
package/README.md CHANGED
@@ -65,12 +65,12 @@ class Quote extends ApplicationService<CommandQuote, ResponseTotal> {
65
65
  }
66
66
  }
67
67
 
68
- const sales = new BoundedContext<{ unitPrice: number }>("sales");
69
- sales.addDependency("unitPrice", 10);
70
- sales.feature(CommandTotal)(Total);
71
- sales.appService(CommandQuote)(Quote);
68
+ const salesBoundedContext = new BoundedContext<{ unitPrice: number }>("sales");
69
+ salesBoundedContext.addDependency("unitPrice", 10);
70
+ salesBoundedContext.feature(CommandTotal)(Total);
71
+ salesBoundedContext.appService(CommandQuote)(Quote);
72
72
 
73
- const answer = await sales.execute(new CommandQuote(3)); // ResponseTotal — total 30
73
+ const answer = await salesBoundedContext.execute(new CommandQuote(3)); // ResponseTotal — total 30
74
74
  ```
75
75
 
76
76
  A Command names, by its type, the Response it answers: `execute` infers it, with no second
@@ -96,12 +96,12 @@ only). A Response may be a class, a plain object, a page, a primitive or `void`.
96
96
  ### Decorators, when the project has them
97
97
 
98
98
  ```ts
99
- @sales.feature(CommandTotal)
99
+ @salesBoundedContext.feature(CommandTotal)
100
100
  class Total extends Feature<CommandTotal, ResponseTotal> { … }
101
101
  ```
102
102
 
103
103
  The same functions work as standard decorators and as legacy ones (`experimentalDecorators`);
104
- without a decorator transform, call them: `sales.feature(CommandTotal)(Total)`. No DI library
104
+ without a decorator transform, call them: `salesBoundedContext.feature(CommandTotal)(Total)`. No DI library
105
105
  and no `reflect-metadata`. The tests compile both decorator forms with TypeScript; a bundler's
106
106
  transform (Vite, Metro/Babel) is each application's to configure and test.
107
107
 
@@ -114,8 +114,8 @@ class PrintOnBixolon extends Print {
114
114
  static override readonly serviceName = "PrintOnBixolon";
115
115
  override execute(command: CommandPrint) { … }
116
116
  }
117
- printing.feature(CommandPrint, { replaces: Print })(PrintOnBixolon);
118
- printing.handlerOf(CommandPrint); // PrintOnBixolon
117
+ printingBoundedContext.feature(CommandPrint, { replaces: Print })(PrintOnBixolon);
118
+ printingBoundedContext.handlerOf(CommandPrint); // PrintOnBixolon
119
119
  ```
120
120
 
121
121
  `replaces` names the service registered now for that class, or the registration is refused. The
@@ -126,9 +126,22 @@ dependency is replaced the same way: `addDependency(name, next, { replaces: prev
126
126
 
127
127
  ## Domain events
128
128
 
129
- A fact that already happened — `OrderPaid` — is executed as a Command is: by the Feature or
130
- ApplicationService registered for its class, its subscriber in this bounded context, through
131
- the same pipeline (span, log lines, error handlers, report):
129
+ A fact that already happened — `OrderPaid` — is heard in one of two ways:
130
+
131
+ | What hears it | When |
132
+ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
133
+ | **A use case of the event**: a Feature or ApplicationService whose input _is_ the event | the use case exists because of the event, and nothing else calls it |
134
+ | **An `EventSubscriber`**: an entrypoint that reads the event and executes Commands on the bus | the event starts use cases that have their own Command — a screen or a cron calls them too |
135
+
136
+ A subscriber is not a use case — it is a door, as a cron or a screen is: it decides nothing. Work
137
+ of several steps is an ApplicationService (the workflow) that the subscriber executes, never
138
+ logic in the subscriber.
139
+
140
+ ### A use case of the event
141
+
142
+ The event is executed as a Command is: by the Feature or ApplicationService registered for its
143
+ class in this bounded context, through the same pipeline (span, log lines, error handlers,
144
+ report):
132
145
 
133
146
  ```ts
134
147
  class OrderPaid extends DomainEvent {
@@ -143,25 +156,67 @@ class ThankCustomer extends Feature<OrderPaid, void> {
143
156
  async execute(event: OrderPaid) { … }
144
157
  }
145
158
 
146
- shop.feature(OrderPaid)(ThankCustomer);
147
- await shop.execute(new OrderPaid("o-1")); // span shop/ThankCustomer, sincpro.event = its name
159
+ shopBoundedContext.feature(OrderPaid)(ThankCustomer);
160
+ await shopBoundedContext.execute(new OrderPaid("o-1")); // span shop/ThankCustomer, sincpro.event = its name
148
161
  ```
149
162
 
150
163
  - **`eventName` is declared**, as `serviceName` is: a minifier renames classes. `event.name`
151
164
  reads it; `name` is reserved, and a subclass cannot make it a field.
152
- - **One subscriber per bounded context.** Another context that hears the event registers its
153
- own; a `Subscriber` executes it in each.
154
- - An ApplicationService can subscribe too, and compose Features — of Commands or of events —
155
- through `featureBus`. `execute(event)` answers whatever the subscriber returns (`unknown`).
165
+ - **One use case per event in a bounded context.** Another context that hears the event
166
+ registers its own; the queue's `Subscriber` executes it in each.
167
+ - An ApplicationService can take the event too, and compose Features — of Commands or of events
168
+ — through `featureBus`. `execute(event)` answers whatever its use case returns (`unknown`).
169
+
170
+ ### An `EventSubscriber`: the entrypoint that hears an event
171
+
172
+ ```ts
173
+ // entrypoints/queue/logged-successfully.subscriber.ts — reads the event, executes the Command
174
+ export class LoggedSuccessfully extends EventSubscriber<OdooLoggedIn> {
175
+ static readonly subscriberName = "LoggedSuccessfully";
176
+ declare readonly ticketsBoundedContext: BoundedContext<TicketsDependencies>;
177
+
178
+ async handle(event: OdooLoggedIn) {
179
+ await this.ticketsBoundedContext.execute(new CommandStartDay(event.uid)); // an ApplicationService
180
+ }
181
+ }
182
+
183
+ // entrypoints/queue/index.ts — who hears what, and what they call
184
+ export const ticketsSubscriptions = new Subscriptions<TicketsEntrypoints>(
185
+ "tickets-subscriptions",
186
+ );
187
+ ticketsSubscriptions.addDependency("ticketsBoundedContext", ticketsBoundedContext);
188
+ ticketsSubscriptions.subscribe(OdooLoggedIn)(LoggedSuccessfully);
189
+ ```
190
+
191
+ - **Each delivery is a run of its own**, `tickets-subscriptions/LoggedSuccessfully` in the layer
192
+ `entrypoint`: its span, its log lines, `this.logger`, `this.execution`. It continues the
193
+ envelope's flow, and the use cases it executes are its children.
194
+ - **A bounded context it was given is bound to it** (`BIND`), as a transport is: what it executes
195
+ runs under the subscriber's span, in the same flow, caused by it, cancelled with it — and still
196
+ with its own dependencies, context, interceptors and handlers. A Feature that calls another
197
+ bounded context injected into it gets the same.
198
+ - **One subscriber per event in a `Subscriptions`**; a second is refused, a replacement names the
199
+ one it replaces (`subscribe(Event, { replaces: Old })`). Several reactions to one event are one
200
+ ApplicationService, or subscribers of different registries.
201
+ - **It is not a use case**: interceptors and listeners see the use cases it calls, not it. A
202
+ failure is reported once along the flow, where it happened, with the outcome its bounded
203
+ context gave it (`expectErrors` holds).
204
+ - **Its name is its identity** in spans, logs and the stored queue's record of who already
205
+ heard an event, so it differs from every bounded context's (`<context>-subscriptions`).
156
206
 
157
207
  ### Publishing and subscribing
158
208
 
159
209
  ```ts
160
210
  import { Publisher, Subscriber, syncQueue } from "@sincpro/client-framework";
161
211
 
162
- const subscriber = new Subscriber(mail, stock); // every bounded context that hears
212
+ // the queue's door: every bounded context with a use case of the event, every Subscriptions
213
+ const subscriber = new Subscriber(
214
+ mailBoundedContext,
215
+ stockBoundedContext,
216
+ ticketsSubscriptions,
217
+ );
163
218
  const queue = syncQueue(subscriber); // in the call, in memory — or a platform's
164
- shop.addDependency("publisher", new Publisher(queue));
219
+ shopBoundedContext.addDependency("publisher", new Publisher(queue));
165
220
 
166
221
  class Pay extends Feature<CommandPay, string> {
167
222
  declare readonly publisher: Publisher;
@@ -174,37 +229,43 @@ class Pay extends Feature<CommandPay, string> {
174
229
 
175
230
  The `Publisher` is bound to the run that holds it, so the event joins that run's flow: its
176
231
  envelope carries the run's `correlationId` and names the run as its cause. The `Subscriber`
177
- executes it in `mail` and in `stock` — `context.execute(event, envelope)` —, so their runs
178
- continue the same flow; one that fails does not stop the others.
232
+ executes it in `mail`, in `stock` and in `tickets-subscriptions` — `execute(event, envelope)`
233
+ —, so their runs continue the same flow; one that fails does not stop the others. Two of them
234
+ never share a name.
179
235
 
180
236
  What is defined here, and what each platform plugs behind the ports:
181
237
 
182
- | Piece | Here | The platform's |
183
- | ------------------------- | -------------------------------------------------------------- | -------------------------------------- |
184
- | `DomainEvent`, subscriber | the class, its `eventName`, its execution | — |
185
- | `Envelope` | `name`, `id`, `correlationId`, `causationId`, `publishedAt` | storing it beside the event |
186
- | `EventQueue` port | `syncQueue` (in the call); `storedQueue` (store, then deliver) | a broker's queue, `BroadcastChannel` |
187
- | `EventStore` port | `memoryEventStore()` | mobile: a SQLite table; web: IndexedDB |
188
- | `Subscribes` port | `Subscriber(...contexts)`, `eventTypeOf(name)` | — |
238
+ | Piece | Here | The platform's |
239
+ | ----------------- | -------------------------------------------------------------- | -------------------------------------- |
240
+ | `DomainEvent` | the class, its `eventName`, its execution | — |
241
+ | `EventSubscriber` | the entrypoint, its run, `Subscriptions` | — |
242
+ | `Envelope` | `name`, `id`, `correlationId`, `causationId`, `publishedAt` | storing it beside the event |
243
+ | `EventQueue` port | `syncQueue` (in the call); `storedQueue` (store, then deliver) | a broker's queue, `BroadcastChannel` |
244
+ | `EventStore` port | `memoryEventStore()` | mobile: a SQLite table; web: IndexedDB |
245
+ | `Subscribes` port | `Subscriber(...hearing)`, `eventTypeOf(name)` | — |
189
246
 
190
247
  ### Stored events: one way to carry them, any store
191
248
 
192
249
  ```ts
193
250
  const queue = storedQueue({
194
251
  store: sqliteEventStore(db), // the platform's EventStore; memoryEventStore() in a tab or a test
195
- subscriber: new Subscriber(sales, stock, printing),
252
+ subscriber: new Subscriber(
253
+ salesBoundedContext,
254
+ stockBoundedContext,
255
+ printingBoundedContext,
256
+ ),
196
257
  ready: (stored) => connectivity.online || !needsNetwork(stored.envelope.name),
197
258
  onSettled: (stored) => activity.show(stored.envelope.name, stored.state),
198
259
  });
199
- shop.addDependency("publisher", new Publisher(queue));
260
+ shopBoundedContext.addDependency("publisher", new Publisher(queue));
200
261
  connectivity.onBack(() => queue.drain()); // and on coming back to the foreground
201
262
  ```
202
263
 
203
264
  `publish` returns once the event is stored. Then the queue delivers what waits, one at a time,
204
265
  in the order stored — the backend's store-and-forward, over whatever store the platform plugs:
205
266
 
206
- - It is rebuilt as its class — `static fromJSON` when it has one — and executed in every
207
- bounded context that hears it and does not have it yet: a retry never runs one twice.
267
+ - It is rebuilt as its class — `static fromJSON` when it has one — and executed by everyone who
268
+ hears it and does not have it yet: a retry never runs one twice.
208
269
  - A failure a retry may fix (`failureOf`: unavailable, no answer, a conflict…) is tried again
209
270
  after the backoff or the wait it asks for, and what follows waits, so the order holds.
210
271
  - A failure every attempt would repeat (`PERMANENT_KINDS`: a refusal, invalid, not found) sets
@@ -219,8 +280,9 @@ memory carry events the same way, with the same rules.
219
280
  Three things a platform keeps in mind:
220
281
 
221
282
  - **At least once.** A process that stops after a context executed an event and before the store
222
- saved it runs it there again. A subscriber that must not act twice keys what it does by the
223
- envelope's id, `this.execution.causationId` in its run.
283
+ saved it runs it there again. What must not act twice keys what it does by the envelope's id —
284
+ `this.execution.causationId` in a use case of the event, or in an `EventSubscriber`, which hands
285
+ it to the Command it executes — or by what the event names (the entity's own id).
224
286
  - **One drainer per store.** Two tabs over one IndexedDB claim it first (the Web Locks API).
225
287
  - **Values come back as JSON.** An event that carries a `Money`, a `Decimal` or a date declares
226
288
  `static fromJSON` to read them back as such.
@@ -236,19 +298,24 @@ and an application changes or hears it from outside, as the backend's `bus_pipel
236
298
 
237
299
  ```ts
238
300
  // control: around the service — look, veto (throw), adjust (same class), or answer
239
- shop.interceptor(CommandPay)(async (command, next, run) => {
301
+ shopBoundedContext.interceptor(CommandPay)(async (command, next, run) => {
240
302
  if (!credit.allows(command.customer_id)) throw new NoCredit();
241
303
  return next(command);
242
304
  });
243
- shop.interceptor()((executable, next, run) => next(executable)); // every class
305
+ shopBoundedContext.interceptor()((executable, next, run) => next(executable)); // every class
244
306
 
245
307
  // observation: never changes the answer
246
- shop.onStart((started) => activity.running(started.execution.useCase));
247
- shop.onCompletion((done) => activity.succeeded(done.execution.useCase), CommandPay);
248
- shop.onFailure((failed) => activity.failed(failed.execution?.useCase, failed.outcome));
308
+ shopBoundedContext.onStart((started) => activity.running(started.execution.useCase));
309
+ shopBoundedContext.onCompletion(
310
+ (done) => activity.succeeded(done.execution.useCase),
311
+ CommandPay,
312
+ );
313
+ shopBoundedContext.onFailure((failed) =>
314
+ activity.failed(failed.execution?.useCase, failed.outcome),
315
+ );
249
316
 
250
317
  // composition: a module or plugin wires its pieces in one place
251
- shop.extend(auditing);
318
+ shopBoundedContext.extend(auditing);
252
319
  ```
253
320
 
254
321
  - **Interceptors** run inside the run, outermost first in the order registered: a veto is the
@@ -270,14 +337,14 @@ first-registered-first. A handler returns the answer, or throws to hand the erro
270
337
  one's throw reaches the caller:
271
338
 
272
339
  ```ts
273
- sales.addFeatureErrorHandler((error) => {
340
+ salesBoundedContext.addFeatureErrorHandler((error) => {
274
341
  if (error instanceof PriceNotFound) return new ResponseTotal(0); // the answer
275
342
  throw error; // the next handler, or the caller
276
343
  });
277
- sales.addAppServiceErrorHandler(toBusinessError);
278
- sales.addGlobalErrorHandler(toProblem); // what reaches sales.execute or a scope
344
+ salesBoundedContext.addAppServiceErrorHandler(toBusinessError);
345
+ salesBoundedContext.addGlobalErrorHandler(toProblem); // what reaches salesBoundedContext.execute or a scope
279
346
 
280
- sales.expectErrors(QuoteExpired); // the business answering: `refused`, never reported
347
+ salesBoundedContext.expectErrors(QuoteExpired); // the business answering: `refused`, never reported
281
348
  ```
282
349
 
283
350
  A failure is observed where it happened, before any handler runs: its span closes with its
@@ -338,7 +405,7 @@ reports what fails outside a use case — a view, a worker —, with the same ru
338
405
 
339
406
  ```ts
340
407
  const screen = new AbortController();
341
- const answer = sales.execute(new CommandQuote(3), { signal: screen.signal });
408
+ const answer = salesBoundedContext.execute(new CommandQuote(3), { signal: screen.signal });
342
409
  screen.abort(); // the run ends `aborted` at once; nothing is reported
343
410
  ```
344
411
 
@@ -399,21 +466,21 @@ import { logger, loggerAdapter } from "@sincpro/client-framework";
399
466
 
400
467
  logger.info("Database opened"); // infrastructure — the global logger
401
468
  loggerAdapter.warn("Printer is slow", { ms: 900 }); // an adapter
402
- sales.logger.info("Catalog preloaded"); // a bounded context, outside a use case
469
+ salesBoundedContext.logger.info("Catalog preloaded"); // a bounded context, outside a use case
403
470
  this.logger.info("Totalling"); // inside a Feature or ApplicationService
404
471
  ```
405
472
 
406
- | Logger | Layer | Each line carries |
407
- | ------------------------- | --------------------- | ---------------------------------------------- |
408
- | `logger` | `infrastructure` | — |
409
- | `loggerAdapter` | `adapter` | the run, when injected as a dependency |
410
- | `sales.logger` | `bounded_context` | its bounded context |
411
- | `this.logger`, Feature | `feature` | bounded context, use case, execution and trace |
412
- | `this.logger`, AppService | `application_service` | the same |
473
+ | Logger | Layer | Each line carries |
474
+ | ---------------------------- | --------------------- | ---------------------------------------------- |
475
+ | `logger` | `infrastructure` | — |
476
+ | `loggerAdapter` | `adapter` | the run, when injected as a dependency |
477
+ | `salesBoundedContext.logger` | `bounded_context` | its bounded context |
478
+ | `this.logger`, Feature | `feature` | bounded context, use case, execution and trace |
479
+ | `this.logger`, AppService | `application_service` | the same |
413
480
 
414
481
  `this.logger` keeps its run across awaits and concurrent calls, without a global stack. An
415
482
  adapter that imports `loggerAdapter` at module level writes without the run; inject it
416
- (`sales.addDependency("log", loggerAdapter)`) or pass `this.logger` when a line must carry it.
483
+ (`salesBoundedContext.addDependency("log", loggerAdapter)`) or pass `this.logger` when a line must carry it.
417
484
 
418
485
  One configuration, two doors — `configureProcess({ logger })` replaces every process port,
419
486
  `configureLogger(...)` only logging (an in-app debug switch):
@@ -441,8 +508,8 @@ snapshot, and what travels goes to the server in `sincpro-context`.
441
508
 
442
509
  ```ts
443
510
  processContext.set({ tenant_id: "acme" });
444
- sales.context.set({ warehouse_id: 4 });
445
- await sales.scoped({ company_id: 2 }).execute(new CommandQuote(1));
511
+ salesBoundedContext.context.set({ warehouse_id: 4 });
512
+ await salesBoundedContext.scoped({ company_id: 2 }).execute(new CommandQuote(1));
446
513
  ```
447
514
 
448
515
  Where the values live is a port (`ContextStore`): synchronous reads, a version per change and an
@@ -612,6 +679,44 @@ never a `Date`. `valuesOf`/`pack` write; `readValues` reads tagged values back;
612
679
  use a reader given explicitly. Cycles, functions and non-finite numbers are refused. How a value
613
680
  is shown, typed or stored is the platform's.
614
681
 
682
+ ## An application's layout
683
+
684
+ An application is laid out as a sincpro_framework service is — the entrypoints call the bus, the
685
+ bus runs the use cases, the use cases use the adapters:
686
+
687
+ ```text
688
+ src/
689
+ entrypoints/main.ts(x) configureProcess, the stored queue and its Subscriber
690
+ domains/
691
+ common/infrastructure/operations.ts export const operations — the wire every client uses
692
+ <bounded context>/
693
+ generated/ sincpro-client generate — never edited
694
+ domain/ local Commands, Responses, DomainEvents, value rules
695
+ adapters/ repositories, printers, other systems
696
+ services/ the use cases, one folder per aggregate or entity
697
+ <aggregate>/ sale_order/: its Features and ApplicationServices
698
+ confirm-quotation.feature.ts
699
+ <entity of the aggregate>/ payments/: a related entity with use cases of its own
700
+ register-payment.feature.ts
701
+ reconcile-payments.app-service.ts
702
+ index.ts registerServices: each Command with its service
703
+ infrastructure/ dependencies.ts, bounded_context.ts (service bases)
704
+ entrypoints/ the doors: they translate, they decide nothing
705
+ queue/ EventSubscribers: event → Command → bus; index.ts: Subscriptions
706
+ cron/ the platform's scheduler: tick → Command → bus
707
+ index.ts export const <context>BoundedContext — ready to execute
708
+ ```
709
+
710
+ - **Services by aggregate.** A simple entity is one folder with its Features (a CRUD). When an
711
+ aggregate holds related entities with use cases of their own — the payments of a sale order —,
712
+ they get a folder inside it, and its Features and ApplicationServices live there, grouped by
713
+ what they are about.
714
+ - **Entrypoints translate.** A subscriber, a cron and a screen build a Command and execute it;
715
+ a workflow of several use cases is an ApplicationService they execute. `sincpro-client check`
716
+ refuses a service that imports an entrypoint.
717
+ - **A bounded context is named for what it is**: `salesBoundedContext`, never `sales` (an
718
+ entity?) or `ctx` — `context` is already the run's (`this.context`).
719
+
615
720
  ## Package layout
616
721
 
617
722
  Each module is laid out as sincpro_framework's are, and says what it offers in its `index.ts`:
@@ -646,7 +751,7 @@ src/
646
751
  event_driven/
647
752
  domain/ Envelope, the EventQueue, Subscribes and EventStore ports
648
753
  adapters/ syncQueue, storedQueue, memoryEventStore
649
- entrypoint/ Publisher, Subscriber
754
+ entrypoint/ Publisher, Subscriber (the queue's door), Subscriptions, EventSubscriber
650
755
  codegen/ the generator and its CLI — the only place Node is allowed
651
756
  testing/ memoryTracer, memoryReporter, memoryLogger
652
757
  ```
@@ -6,7 +6,7 @@ import type { Transport } from "../domain/transport.js";
6
6
  * `Transport`:
7
7
  *
8
8
  * const operations = guardedTransport(jsonRpcOperations({ endpoint }), new CircuitBreaker("forge"));
9
- * project.addDependency("client", clientOf(operations));
9
+ * projectBoundedContext.addDependency("client", clientOf(operations));
10
10
  *
11
11
  * Context: the circuit belongs where the dependency is — the transport —, not in each Feature. A
12
12
  * run binds it as it binds any transport, so the trace and the context still travel; an open
@@ -8,7 +8,7 @@ import type { Metamodel } from "../domain/metamodel.js";
8
8
  * infrastructure/dependencies.ts what the services can count on: the generated client
9
9
  * infrastructure/bounded_context.ts its Feature and ApplicationService, and how it is made
10
10
  * services/index.ts registers command classes and their services
11
- * index.ts `export const project` — ready to use
11
+ * index.ts `export const projectBoundedContext` — ready to use
12
12
  *
13
13
  * Context: the layout sincpro_framework gives a service (`dependencies.py`, `framework.py`,
14
14
  * `services/`, `__init__.py`), so a bounded context starts the way every other does and nobody
@@ -1,10 +1,4 @@
1
1
  import { aliasOf, FRAMEWORK } from "../domain/names.js";
2
- /** Names the skeleton's `index.ts` cannot give its constant. */
3
- const TAKEN = new Set([
4
- "operations",
5
- "registerServices",
6
- ..."break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "),
7
- ]);
8
2
  /** `sales-orders` → `SalesOrders`. */
9
3
  function pascalOf(alias) {
10
4
  const joined = alias
@@ -20,10 +14,9 @@ const HEADER = (what) => `// ${what} — written once by \`sincpro-client scaffo
20
14
  export function scaffoldFilesOf(metamodel, options = {}) {
21
15
  const alias = aliasOf(metamodel);
22
16
  const Name = pascalOf(alias);
23
- const bare = `${Name.charAt(0).toLowerCase()}${Name.slice(1)}`;
24
- // A word the language keeps (`import`, `default`) or a name `index.ts` already imports cannot
25
- // be the constant's: it takes the bounded context's word instead.
26
- const name = TAKEN.has(bare) ? `${bare}BoundedContext` : bare;
17
+ // Named for what it is — `projectBoundedContext`, never `project` (an entity?) or `ctx` —:
18
+ // nobody misreads it, and no word of the language or name `index.ts` imports can take it.
19
+ const name = `${Name.charAt(0).toLowerCase()}${Name.slice(1)}BoundedContext`;
27
20
  const Dependencies = `${Name}Dependencies`;
28
21
  const client = metamodel.bindings !== undefined;
29
22
  const operations = options.operations ?? at("..", "common", "infrastructure", "operations");
@@ -44,11 +37,11 @@ export function scaffoldFilesOf(metamodel, options = {}) {
44
37
  "}",
45
38
  "",
46
39
  "export function registerDependencies(",
47
- ` bc: BoundedContext<${Dependencies}>,`,
40
+ ` boundedContext: BoundedContext<${Dependencies}>,`,
48
41
  ` ${client ? "operations" : "_operations"}: OperationTransport,`,
49
42
  ") {",
50
- ...(client ? [` bc.addDependency("client", clientOf(operations));`] : []),
51
- " return bc;",
43
+ ...(client ? [` boundedContext.addDependency("client", clientOf(operations));`] : []),
44
+ " return boundedContext;",
52
45
  "}",
53
46
  "",
54
47
  ].join("\n");
@@ -66,7 +59,7 @@ export function scaffoldFilesOf(metamodel, options = {}) {
66
59
  "",
67
60
  `import { type ${Dependencies}, registerDependencies } from "${at(".", "dependencies")}";`,
68
61
  "",
69
- `/** A Feature of «${alias}» — of a Command, or a subscriber of an event: its dependencies typed on \`this\`, and \`this.context\`. */`,
62
+ `/** A Feature of «${alias}» — of a Command, or a use case of an event: its dependencies typed on \`this\`, and \`this.context\`. */`,
70
63
  "export abstract class Feature<C extends Command<R> | DomainEvent, R> extends BaseFeature<C, R> {",
71
64
  ...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
72
65
  "}",
@@ -91,10 +84,12 @@ export function scaffoldFilesOf(metamodel, options = {}) {
91
84
  " * Generated DTO types are remote contracts, not runtime command constructors: import",
92
85
  ` * them under their own name — \`import type * as remote from "${at("..", "generated", "domain")}"\` —`,
93
86
  " * and let a local Command implement one (`implements remote.CommandCancelIssue`).",
94
- " * Register with context.feature(CommandClass)(FeatureClass) or",
95
- " * context.appService(CommandClass)(ApplicationServiceClass).",
87
+ " * Register with boundedContext.feature(CommandClass)(FeatureClass) or",
88
+ " * boundedContext.appService(CommandClass)(ApplicationServiceClass); the services live in",
89
+ " * one folder per aggregate (`services/<aggregate>/`), and an entity of that aggregate with",
90
+ " * use cases of its own in a folder inside it (`services/<aggregate>/<entity>/`).",
96
91
  " */",
97
- `export function registerServices(_context: BoundedContext<${Dependencies}>): void {}`,
92
+ `export function registerServices(_boundedContext: BoundedContext<${Dependencies}>): void {}`,
98
93
  "",
99
94
  ].join("\n");
100
95
  const index = [
@@ -1,18 +1,19 @@
1
1
  import type { ContextStore, ContextValues } from "../domain/store.js";
2
2
  /**
3
3
  * One level of the context: the process's (`processContext`) or a bounded context's
4
- * (`project.context`), read and written the same way on every platform.
4
+ * (`projectBoundedContext.context`), read and written the same way on every platform.
5
5
  *
6
6
  * processContext.set({ tenant_id: "acme" }); the company changed — every screen hears it
7
7
  * processContext.get("tenant_id") → "acme"
8
- * project.context.set({ warehouse_id: 4 }); only what `project` runs
8
+ * projectBoundedContext.context.set({ warehouse_id: 4 }); only what it runs
9
9
  * const stop = processContext.watch(() => redraw());
10
10
  *
11
11
  * Context: sincpro_framework reads its context from a tree — the process, the bounded context,
12
12
  * the scope, the execution — and the nearest wins. A client has the same levels: a use case runs
13
13
  * with the process's values, under them its bounded context's, under them a scope's
14
- * (`project.scoped({...})`), read once where the gesture starts and inherited by every Feature
15
- * it runs. That snapshot is `this.context` and what travels to the server in `sincpro-context`.
14
+ * (`projectBoundedContext.scoped({...})`), read once where the gesture starts and inherited by
15
+ * every Feature it runs. That snapshot is `this.context` and what travels to the server in
16
+ * `sincpro-context`.
16
17
  *
17
18
  * Where the values live is the platform's (`ContextStore`): this is only the door. `values` is
18
19
  * the same object until something changes it, so a screen can hand it straight to React's
@@ -5,7 +5,7 @@
5
5
  * class CommandCancelIssue extends Command<ResponseCancelIssue> {
6
6
  * constructor(readonly issue_id: string) { super(); }
7
7
  * }
8
- * const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
8
+ * const answer = await projectBoundedContext.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
9
9
  *
10
10
  * Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
11
11
  * `execute` infers it with no second argument. The association is type-only: absent from a
@@ -31,8 +31,8 @@ export declare abstract class Command<R> extends DataTransferObject {
31
31
  * static readonly eventName = "shop.order.v1.paid";
32
32
  * constructor(readonly order_id: string) { super(); }
33
33
  * }
34
- * project.feature(OrderPaid)(NotifyCustomer);
35
- * await project.execute(new OrderPaid("o-1"));
34
+ * projectBoundedContext.feature(OrderPaid)(NotifyCustomer);
35
+ * await projectBoundedContext.execute(new OrderPaid("o-1"));
36
36
  *
37
37
  * Context: the backend names an event on the wire by `name`, `<context>.<aggregate>.vN.<fact>`,
38
38
  * and defaults it to the class's name; a client is minified, so the name is declared, as
@@ -5,7 +5,7 @@
5
5
  * class CommandCancelIssue extends Command<ResponseCancelIssue> {
6
6
  * constructor(readonly issue_id: string) { super(); }
7
7
  * }
8
- * const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
8
+ * const answer = await projectBoundedContext.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
9
9
  *
10
10
  * Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
11
11
  * `execute` infers it with no second argument. The association is type-only: absent from a
@@ -27,8 +27,8 @@ export class Command extends DataTransferObject {
27
27
  * static readonly eventName = "shop.order.v1.paid";
28
28
  * constructor(readonly order_id: string) { super(); }
29
29
  * }
30
- * project.feature(OrderPaid)(NotifyCustomer);
31
- * await project.execute(new OrderPaid("o-1"));
30
+ * projectBoundedContext.feature(OrderPaid)(NotifyCustomer);
31
+ * await projectBoundedContext.execute(new OrderPaid("o-1"));
32
32
  *
33
33
  * Context: the backend names an event on the wire by `name`, `<context>.<aggregate>.vN.<fact>`,
34
34
  * and defaults it to the class's name; a client is minified, so the name is declared, as