@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
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * What answers a use case's failure instead of raising it — sincpro_framework's error handlers.
3
3
  *
4
- * project.addFeatureErrorHandler((error) => {
4
+ * projectBoundedContext.addFeatureErrorHandler((error) => {
5
5
  * if (error instanceof IssueNotFound) return new ResponseEmpty(); // the answer
6
6
  * throw error; // the next handler
7
7
  * });
8
8
  *
9
- * Context: three chains per bounded context — global (around `project.execute`), feature and
9
+ * Context: three chains per bounded context — global (around `projectBoundedContext.execute`), feature and
10
10
  * application service —, each run first-registered-first. A handler that throws hands what it
11
11
  * threw to the next; the last one's throw reaches the caller. The failure was already recorded
12
12
  * where it happened (its span, its log line and, when unexpected, its report): a handler decides
@@ -8,11 +8,11 @@ import type { Outcome } from "../../observability/domain/tracing.js";
8
8
  * control interceptors wrap the service: look, veto, adjust or answer
9
9
  * observation listeners a run started, answered, or a failure escaped: heard, never changed
10
10
  *
11
- * project.interceptor(CommandPay)(async (command, next, run) => {
11
+ * projectBoundedContext.interceptor(CommandPay)(async (command, next, run) => {
12
12
  * if (!credit.allows(command.customer_id)) throw new NoCredit(); // veto
13
13
  * return next(command); // the next, or the service
14
14
  * });
15
- * project.onCompletion((done) => activity.succeeded(done.execution.useCase));
15
+ * projectBoundedContext.onCompletion((done) => activity.succeeded(done.execution.useCase));
16
16
  *
17
17
  * Context: the core owns the flow — context, span, log lines, outcome, report, handlers — and
18
18
  * these are the places an application changes it from outside, without touching a service. An
@@ -32,7 +32,7 @@ export type Cause = {
32
32
  * How a Command or an event is executed: what cancels it, and — for a delivered event — what
33
33
  * caused it.
34
34
  *
35
- * await sales.execute(new CommandQuote(3), { signal: screen.signal });
35
+ * await salesBoundedContext.execute(new CommandQuote(3), { signal: screen.signal });
36
36
  *
37
37
  * Context: a screen that closes, a navigation that leaves, a user who gives up stop waiting; the
38
38
  * run ends `aborted` (not reported), its Features inherit the signal, and the requests its
@@ -9,7 +9,7 @@ import type { Span } from "../../observability/domain/tracing.js";
9
9
  * registered Command — or domain event, as its subscriber —; an ApplicationService composes its
10
10
  * context's Features.
11
11
  *
12
- * @project.feature(CommandCancelIssue)
12
+ * @projectBoundedContext.feature(CommandCancelIssue)
13
13
  * class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
14
14
  * static readonly serviceName = "CancelIssue";
15
15
  * async execute(command: CommandCancelIssue) {
@@ -32,9 +32,10 @@ export declare abstract class Feature<C extends Command<R> | DomainEvent, R, Con
32
32
  /** What cancels this run, when it was executed with one: check it in a long loop. */
33
33
  readonly signal: AbortSignal | undefined;
34
34
  /**
35
- * This run: its id, use case, flow (`correlationId`) and cause. A subscriber of a delivered
36
- * event finds the envelope's id as `causationId` — the key that makes executing it twice
37
- * harmless.
35
+ * This run: its id, use case, flow (`correlationId`) and cause. A use case of a delivered event
36
+ * finds the envelope's id as `causationId` — the key that makes executing it twice harmless; a
37
+ * use case an `EventSubscriber` executes finds the subscriber's run, so the subscriber hands the
38
+ * envelope's id in the Command when the effect must not repeat.
38
39
  */
39
40
  readonly execution: Execution;
40
41
  abstract execute(command: C): R | Promise<R>;
@@ -3,7 +3,7 @@
3
3
  * registered Command — or domain event, as its subscriber —; an ApplicationService composes its
4
4
  * context's Features.
5
5
  *
6
- * @project.feature(CommandCancelIssue)
6
+ * @projectBoundedContext.feature(CommandCancelIssue)
7
7
  * class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
8
8
  * static readonly serviceName = "CancelIssue";
9
9
  * async execute(command: CommandCancelIssue) {
@@ -1,3 +1,4 @@
1
+ import { BIND, type Execution } from "../../context/domain/execution.js";
1
2
  import type { ContextValues } from "../../context/domain/store.js";
2
3
  import { type Context } from "../../context/entrypoint/context.js";
3
4
  import { type CommandBus } from "../domain/bus.js";
@@ -25,22 +26,24 @@ export type Registrar<T extends Executable> = <S extends ServiceClass<NoInfer<T>
25
26
  * A bounded context: the bus of its Commands and domain events, as sincpro_framework's
26
27
  * `UseFramework`.
27
28
  *
28
- * const project = new BoundedContext<ProjectDependencies>("project");
29
- * project.addDependency("client", clientOf(transport));
30
- * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
31
- * project.feature(IssueClosed)(NotifyWatchers); // a subscriber of an event
32
- * project.feature(CommandCancelIssue, { replaces: CancelIssue })(CancelAndArchive);
33
- * project.addFeatureErrorHandler(toEmptyWhenMissing);
34
- * project.expectErrors(IssueLocked); // refused, never reported
35
- * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
36
- * await project.execute(new IssueClosed("x")); // what its subscriber returns
37
- * project.logger.info("Catalog preloaded");
29
+ * const projectBoundedContext = new BoundedContext<ProjectDependencies>("project");
30
+ * projectBoundedContext.addDependency("client", clientOf(operations));
31
+ * projectBoundedContext.feature(CommandCancelIssue)(CancelIssue); // or a decorator
32
+ * projectBoundedContext.feature(IssueClosed)(ArchiveIssue); // a use case of an event
33
+ * projectBoundedContext.feature(CommandCancelIssue, { replaces: CancelIssue })(CancelAndArchive);
34
+ * projectBoundedContext.addFeatureErrorHandler(toEmptyWhenMissing);
35
+ * projectBoundedContext.expectErrors(IssueLocked); // refused, never reported
36
+ * await projectBoundedContext.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
37
+ * await projectBoundedContext.execute(new IssueClosed("x")); // what its use case returns
38
+ * projectBoundedContext.logger.info("Catalog preloaded");
38
39
  *
39
40
  * Context: registrations, dependencies, handlers and expected errors are snapshotted per
40
41
  * generation; a call keeps the generation it started with, and what is added later answers from
41
- * the next call — as the backend's `build_root_bus`. A domain event is executed as a Command is:
42
- * one subscriber per bounded context, through the same pipeline; carrying it to every bounded
43
- * context that hears it (a queue, an outbox) is the platform's.
42
+ * the next call — as the backend's `build_root_bus`. A domain event may be the input of a use
43
+ * case, executed as a Command is: one per bounded context, through the same pipeline. Carrying
44
+ * it to whoever hears it (a queue, an outbox) is the platform's; an entrypoint that hears it and
45
+ * calls use cases is an `EventSubscriber`. Injected into another run — an `EventSubscriber`, a
46
+ * Feature of another bounded context — it is bound to that run (`BIND`).
44
47
  */
45
48
  export declare class BoundedContext<D extends object = Record<never, never>, C extends object = ContextValues> {
46
49
  readonly name: string;
@@ -119,6 +122,14 @@ export declare class BoundedContext<D extends object = Record<never, never>, C e
119
122
  execute<R>(command: Command<R>, options: ExecuteOptions): Promise<R>;
120
123
  execute(event: DomainEvent, options?: ExecuteOptions | Cause): Promise<unknown>;
121
124
  scoped(context: Partial<C>): CommandBus;
125
+ /**
126
+ * This bounded context as `execution` calls it — what it is as a dependency of another run (an
127
+ * `EventSubscriber`, a Feature of another bounded context): what it executes is a child of
128
+ * that run — its span under the caller's, the same flow (`correlationId`), caused by it,
129
+ * cancelled with it, its errors reported once along it. It still runs with its own
130
+ * dependencies, context, interceptors and handlers.
131
+ */
132
+ [BIND](execution: Execution): this;
122
133
  buildRootBus(): void;
123
134
  fresh(): BoundedContext<D, C>;
124
135
  /** What escaped a call, told once, from where it first failed. */
@@ -1,3 +1,4 @@
1
+ import { BIND } from "../../context/domain/execution.js";
1
2
  import { contextAt, } from "../../context/entrypoint/context.js";
2
3
  import { processContext, processStore, } from "../../context/infrastructure/process.js";
3
4
  import { FeatureBus, optionsOf, } from "../domain/bus.js";
@@ -26,22 +27,24 @@ function nameOf(Service) {
26
27
  * A bounded context: the bus of its Commands and domain events, as sincpro_framework's
27
28
  * `UseFramework`.
28
29
  *
29
- * const project = new BoundedContext<ProjectDependencies>("project");
30
- * project.addDependency("client", clientOf(transport));
31
- * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
32
- * project.feature(IssueClosed)(NotifyWatchers); // a subscriber of an event
33
- * project.feature(CommandCancelIssue, { replaces: CancelIssue })(CancelAndArchive);
34
- * project.addFeatureErrorHandler(toEmptyWhenMissing);
35
- * project.expectErrors(IssueLocked); // refused, never reported
36
- * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
37
- * await project.execute(new IssueClosed("x")); // what its subscriber returns
38
- * project.logger.info("Catalog preloaded");
30
+ * const projectBoundedContext = new BoundedContext<ProjectDependencies>("project");
31
+ * projectBoundedContext.addDependency("client", clientOf(operations));
32
+ * projectBoundedContext.feature(CommandCancelIssue)(CancelIssue); // or a decorator
33
+ * projectBoundedContext.feature(IssueClosed)(ArchiveIssue); // a use case of an event
34
+ * projectBoundedContext.feature(CommandCancelIssue, { replaces: CancelIssue })(CancelAndArchive);
35
+ * projectBoundedContext.addFeatureErrorHandler(toEmptyWhenMissing);
36
+ * projectBoundedContext.expectErrors(IssueLocked); // refused, never reported
37
+ * await projectBoundedContext.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
38
+ * await projectBoundedContext.execute(new IssueClosed("x")); // what its use case returns
39
+ * projectBoundedContext.logger.info("Catalog preloaded");
39
40
  *
40
41
  * Context: registrations, dependencies, handlers and expected errors are snapshotted per
41
42
  * generation; a call keeps the generation it started with, and what is added later answers from
42
- * the next call — as the backend's `build_root_bus`. A domain event is executed as a Command is:
43
- * one subscriber per bounded context, through the same pipeline; carrying it to every bounded
44
- * context that hears it (a queue, an outbox) is the platform's.
43
+ * the next call — as the backend's `build_root_bus`. A domain event may be the input of a use
44
+ * case, executed as a Command is: one per bounded context, through the same pipeline. Carrying
45
+ * it to whoever hears it (a queue, an outbox) is the platform's; an entrypoint that hears it and
46
+ * calls use cases is an `EventSubscriber`. Injected into another run — an `EventSubscriber`, a
47
+ * Feature of another bounded context — it is bound to that run (`BIND`).
45
48
  */
46
49
  export class BoundedContext {
47
50
  name;
@@ -211,6 +214,31 @@ export class BoundedContext {
211
214
  const scope = { ...context };
212
215
  return new FeatureBus((executable, options) => this.generation().root(executable, scope, options));
213
216
  }
217
+ /**
218
+ * This bounded context as `execution` calls it — what it is as a dependency of another run (an
219
+ * `EventSubscriber`, a Feature of another bounded context): what it executes is a child of
220
+ * that run — its span under the caller's, the same flow (`correlationId`), caused by it,
221
+ * cancelled with it, its errors reported once along it. It still runs with its own
222
+ * dependencies, context, interceptors and handlers.
223
+ */
224
+ [BIND](execution) {
225
+ const caller = execution;
226
+ const execute = (executable, second) => this.generation().root(executable, {}, { ...optionsOf(second), caller });
227
+ const scoped = (context) => {
228
+ const scope = { ...context };
229
+ return new FeatureBus((executable, options) => this.generation().root(executable, scope, { ...options, caller }));
230
+ };
231
+ return new Proxy(this, {
232
+ get(target, property) {
233
+ if (property === "execute")
234
+ return execute;
235
+ if (property === "scoped")
236
+ return scoped;
237
+ const value = Reflect.get(target, property, target);
238
+ return typeof value === "function" ? value.bind(target) : value;
239
+ },
240
+ });
241
+ }
214
242
  buildRootBus() {
215
243
  this.generation();
216
244
  }
@@ -248,7 +276,8 @@ export class BoundedContext {
248
276
  const heard = {
249
277
  executable: at?.executable ?? executable,
250
278
  execution: at?.execution,
251
- layer: at?.layer,
279
+ // Listeners hear use cases: a failure an entrypoint's run started says no layer.
280
+ layer: at?.layer === "entrypoint" ? undefined : at?.layer,
252
281
  error,
253
282
  outcome,
254
283
  };
@@ -386,7 +415,9 @@ export class BoundedContext {
386
415
  dependencies,
387
416
  context: () => ({ ...processContext.values, ...this.context.values, ...scope }),
388
417
  ...(parent ? { parent } : {}),
389
- // Only an event comes from outside with a cause; a child continues its parent.
418
+ // A root called from another run continues it; a child continues its parent.
419
+ ...(options.caller && !parent ? { caller: options.caller } : {}),
420
+ // Only an event comes from outside with a cause.
390
421
  ...(options.cause && executable instanceof DomainEvent && !parent
391
422
  ? { cause: options.cause }
392
423
  : {}),
@@ -6,7 +6,7 @@ import { type ExecutableClass, type Hearing } from "../services/listening.js";
6
6
  *
7
7
  * const stop = processListeners.onFailure((failed) => toast(failed.execution?.useCase));
8
8
  *
9
- * Context: a bounded context's own listeners (`project.onStart`) hear it alone; these hear all of
9
+ * Context: a bounded context's own listeners (`projectBoundedContext.onStart`) hear it alone; these hear all of
10
10
  * them, after its own. They live as long as the process — `configureProcess` does not clear
11
11
  * them —, so whoever adds one keeps the function that stops it.
12
12
  */
@@ -5,7 +5,7 @@ import { listenIn, } from "../services/listening.js";
5
5
  *
6
6
  * const stop = processListeners.onFailure((failed) => toast(failed.execution?.useCase));
7
7
  *
8
- * Context: a bounded context's own listeners (`project.onStart`) hear it alone; these hear all of
8
+ * Context: a bounded context's own listeners (`projectBoundedContext.onStart`) hear it alone; these hear all of
9
9
  * them, after its own. They live as long as the process — `configureProcess` does not clear
10
10
  * them —, so whoever adds one keeps the function that stops it.
11
11
  */
@@ -4,10 +4,12 @@ import type { Executable } from "../domain/dto.js";
4
4
  import type { ErrorHandler } from "../domain/errors.js";
5
5
  import type { Cause, ExecutionCompleted, ExecutionStarted, Interceptor, PipelineLayer } from "../domain/pipeline.js";
6
6
  import { type Attributes, type Outcome } from "../../observability/domain/tracing.js";
7
- export interface RuntimeService {
8
- execute(command: object): unknown;
9
- }
10
- type Layer = PipelineLayer;
7
+ /**
8
+ * Where a run sits: a use case's layer (`PipelineLayer`), or an entrypoint that calls use cases —
9
+ * an `EventSubscriber`. Interceptors wrap and listeners hear use cases only.
10
+ */
11
+ export type RunLayer = PipelineLayer | "entrypoint";
12
+ type Layer = RunLayer;
11
13
  /** Where a failure first happened: the run, its executable, its layer and how it ended. */
12
14
  export type FailedAt = {
13
15
  readonly execution: Execution;
@@ -23,44 +25,58 @@ export interface Announcer {
23
25
  completed(heard: ExecutionCompleted): void;
24
26
  }
25
27
  export interface ExecutionOptions {
28
+ /** Its bounded context — or, for an entrypoint's run, the registry it belongs to. */
26
29
  readonly boundedContext: string;
27
30
  readonly name: string;
28
31
  readonly layer: Layer;
32
+ /** The method that runs it: a use case's `execute`, an entrypoint's `handle`. */
33
+ readonly method?: "execute" | "handle";
29
34
  /** What else its span says: the event it executes, the service it replaced. */
30
35
  readonly attributes?: Attributes;
31
36
  readonly dependencies: ReadonlyMap<string, unknown>;
32
37
  readonly context: () => ContextValues;
33
38
  readonly parent?: Execution;
39
+ /**
40
+ * The run that called this root from outside its bounded context — an `EventSubscriber`, a
41
+ * Feature of another bounded context — through a bounded context bound to it (`BIND`): its span
42
+ * is the parent, its flow continues, it is the cause and its signal cancels this one; the
43
+ * context, dependencies and handlers stay this bounded context's.
44
+ */
45
+ readonly caller?: Execution;
34
46
  /** What caused a root run from outside: a delivered event's envelope continues its chain. */
35
47
  readonly cause?: Cause;
36
48
  /** What cancels a root run; a child inherits its parent's. */
37
49
  readonly signal?: AbortSignal;
38
- readonly featureBus: (execution: Execution) => object;
50
+ /** An ApplicationService's Features; no other layer has them. */
51
+ readonly featureBus?: (execution: Execution) => object;
39
52
  /** What runs around the service, outermost first. */
40
53
  readonly interceptors: readonly Interceptor[];
41
- /** Who hears it start and answer. */
42
- readonly announce: Announcer;
54
+ /** Who hears a use case start and answer; an entrypoint's run is not announced. */
55
+ readonly announce?: Announcer;
43
56
  /** The bounded context's expected errors: they end `refused`, never reported. */
44
57
  readonly expected: (error: unknown) => boolean;
45
58
  /** The bounded context's handlers for this layer: what answers a failure instead of raising it. */
46
59
  readonly handlers: readonly ErrorHandler[];
47
60
  }
48
61
  /**
49
- * One pipeline for every registered Feature and ApplicationService.
62
+ * One pipeline for every registered Feature and ApplicationService — and for every entrypoint's
63
+ * run that calls them (an `EventSubscriber`).
50
64
  *
51
- * 1. A span `<bounded context>/<serviceName>`, a child of the parent run when there is one, and
52
- * the execution every dependency, `this.logger` and child Feature are bound to.
53
- * Its chain: the parent's correlation (or the cause's, or its own id for a root), caused by
54
- * the parent run or the delivered event.
55
- * 2. Its start is announced; the interceptors run around the service, which sees the
56
- * execution through its own view of the instance.
65
+ * 1. A span `<bounded context>/<serviceName>`, a child of the run above it when there is one (its
66
+ * parent, or the caller from outside), and the execution every dependency, `this.logger` and
67
+ * child Feature are bound to. Its chain: the run above's correlation (or the cause's, or its
68
+ * own id for a root), caused by the run above or the delivered event.
69
+ * 2. A use case's start is announced; the interceptors run around the service, which sees the
70
+ * execution through its own view of the instance. An entrypoint's run is neither announced
71
+ * nor intercepted.
57
72
  * 3. A failure is observed where it happened: its outcome (expected by the bounded context →
58
- * `refused`, else the process's `outcomeOf`), the span closed with it, its log line,
59
- * where it first failed and — when `failed`, and only once per error along the run — its
73
+ * `refused`; else the outcome it already had where it first failed — another bounded
74
+ * context's rules —; else the process's `outcomeOf`), the span closed with it, its log line,
75
+ * where it first failed and — when `failed`, and only once per error along the flow — its
60
76
  * report.
61
77
  * 4. Then the layer's error handlers answer it, in order; with none, or when the last throws,
62
78
  * the error reaches the caller.
63
79
  * Final: the service's answer — announced —, or its handlers'.
64
80
  */
65
- export declare function executeService(target: RuntimeService, command: Executable, options: ExecutionOptions): Promise<unknown>;
81
+ export declare function executeService(target: object, command: Executable, options: ExecutionOptions): Promise<unknown>;
66
82
  export {};
@@ -64,7 +64,9 @@ function serviceView(target, options, execution) {
64
64
  return execution.signal;
65
65
  if (property === "execution")
66
66
  return execution;
67
- if (property === "featureBus" && options.layer === "application_service") {
67
+ if (property === "featureBus" &&
68
+ options.layer === "application_service" &&
69
+ options.featureBus) {
68
70
  return (features ??= options.featureBus(execution));
69
71
  }
70
72
  if (typeof property === "string" && options.dependencies.has(property)) {
@@ -95,17 +97,20 @@ function serviceView(target, options, execution) {
95
97
  });
96
98
  }
97
99
  /**
98
- * One pipeline for every registered Feature and ApplicationService.
100
+ * One pipeline for every registered Feature and ApplicationService — and for every entrypoint's
101
+ * run that calls them (an `EventSubscriber`).
99
102
  *
100
- * 1. A span `<bounded context>/<serviceName>`, a child of the parent run when there is one, and
101
- * the execution every dependency, `this.logger` and child Feature are bound to.
102
- * Its chain: the parent's correlation (or the cause's, or its own id for a root), caused by
103
- * the parent run or the delivered event.
104
- * 2. Its start is announced; the interceptors run around the service, which sees the
105
- * execution through its own view of the instance.
103
+ * 1. A span `<bounded context>/<serviceName>`, a child of the run above it when there is one (its
104
+ * parent, or the caller from outside), and the execution every dependency, `this.logger` and
105
+ * child Feature are bound to. Its chain: the run above's correlation (or the cause's, or its
106
+ * own id for a root), caused by the run above or the delivered event.
107
+ * 2. A use case's start is announced; the interceptors run around the service, which sees the
108
+ * execution through its own view of the instance. An entrypoint's run is neither announced
109
+ * nor intercepted.
106
110
  * 3. A failure is observed where it happened: its outcome (expected by the bounded context →
107
- * `refused`, else the process's `outcomeOf`), the span closed with it, its log line,
108
- * where it first failed and — when `failed`, and only once per error along the run — its
111
+ * `refused`; else the outcome it already had where it first failed — another bounded
112
+ * context's rules —; else the process's `outcomeOf`), the span closed with it, its log line,
113
+ * where it first failed and — when `failed`, and only once per error along the flow — its
109
114
  * report.
110
115
  * 4. Then the layer's error handlers answer it, in order; with none, or when the last throws,
111
116
  * the error reaches the caller.
@@ -117,8 +122,12 @@ export async function executeService(target, command, options) {
117
122
  const useCase = `${options.boundedContext}.${options.name}`;
118
123
  const context = options.parent?.context ?? options.context();
119
124
  const id = newEntityId();
120
- const correlationId = options.parent?.correlationId ?? options.cause?.correlationId ?? id;
121
- const causationId = options.parent?.id ?? options.cause?.id;
125
+ // The run above this one: its parent in the bounded context, or the caller from outside.
126
+ const above = options.parent ?? options.caller;
127
+ const correlationId = above?.correlationId ?? options.cause?.correlationId ?? id;
128
+ const causationId = above?.id ?? options.cause?.id;
129
+ const layer = options.layer;
130
+ const useCaseLayer = layer === "entrypoint" ? undefined : layer;
122
131
  const attributes = {
123
132
  "sincpro.use_case": useCase,
124
133
  "sincpro.layer": options.layer,
@@ -129,32 +138,35 @@ export async function executeService(target, command, options) {
129
138
  ...options.attributes,
130
139
  };
131
140
  const span = observe(() => ports.tracer.start(`${options.boundedContext}/${options.name}`, {
132
- ...(options.parent ? { parent: options.parent.span } : {}),
141
+ ...(above ? { parent: above.span } : {}),
133
142
  attributes,
134
143
  }), NO_TRACER.start(useCase, {}));
135
144
  const execution = {
136
145
  id,
137
146
  useCase,
138
147
  boundedContext: options.boundedContext,
139
- parent: options.parent,
148
+ parent: above,
140
149
  correlationId,
141
150
  causationId,
142
- signal: options.signal ?? options.parent?.signal,
151
+ signal: options.signal ?? above?.signal,
143
152
  span,
144
153
  context,
145
154
  };
146
- const reported = options.parent
147
- ? (REPORTED_BY_EXECUTION.get(options.parent) ?? new Set())
155
+ const reported = above
156
+ ? (REPORTED_BY_EXECUTION.get(above) ?? new Set())
148
157
  : new Set();
149
158
  REPORTED_BY_EXECUTION.set(execution, reported);
150
- logExecution(options.layer, execution);
151
- observe(() => options.announce.started({ executable: command, execution, layer: options.layer }), undefined);
159
+ logExecution(layer, execution);
160
+ if (useCaseLayer)
161
+ observe(() => options.announce?.started({ executable: command, execution, layer: useCaseLayer }), undefined);
152
162
  const finish = (error) => {
153
163
  const outcome = error
154
- ? observe(() => (options.expected(error.value) ? "refused" : ports.outcomeOf(error.value)), "failed")
164
+ ? observe(() => options.expected(error.value)
165
+ ? "refused"
166
+ : (failedAt(error.value)?.outcome ?? ports.outcomeOf(error.value)), "failed")
155
167
  : "ok";
156
168
  observe(() => span.end(outcome, error?.value), undefined);
157
- logExecution(options.layer, execution, {
169
+ logExecution(layer, execution, {
158
170
  outcome,
159
171
  durationMs: Math.max(0, Date.now() - startedAt),
160
172
  });
@@ -175,25 +187,28 @@ export async function executeService(target, command, options) {
175
187
  FAILED_AT.set(error.value, {
176
188
  execution,
177
189
  executable: command,
178
- layer: options.layer,
190
+ layer,
179
191
  outcome,
180
192
  });
181
193
  return outcome;
182
194
  };
183
195
  const view = serviceView(target, options, execution);
184
- const service = (current) => Promise.resolve(Reflect.apply(target.execute, view, [current]));
196
+ const method = Reflect.get(target, options.method ?? "execute");
197
+ const service = (current) => Promise.resolve(Reflect.apply(method, view, [current]));
185
198
  let answer;
186
199
  try {
187
200
  const signal = execution.signal;
188
201
  if (signal?.aborted)
189
202
  throw abortedBy(signal);
190
- const running = runAround(options.interceptors, service, command, {
191
- boundedContext: options.boundedContext,
192
- useCase,
193
- layer: options.layer,
194
- execution,
195
- logger: executionLogger(options.layer, execution),
196
- });
203
+ const running = useCaseLayer
204
+ ? runAround(options.interceptors, service, command, {
205
+ boundedContext: options.boundedContext,
206
+ useCase,
207
+ layer: useCaseLayer,
208
+ execution,
209
+ logger: executionLogger(useCaseLayer, execution),
210
+ })
211
+ : service(command);
197
212
  answer = signal === undefined ? await running : await untilAborted(running, signal);
198
213
  }
199
214
  catch (error) {
@@ -201,16 +216,17 @@ export async function executeService(target, command, options) {
201
216
  if (options.handlers.length === 0)
202
217
  throw error;
203
218
  const recovered = await handled(options.handlers, error);
204
- logHandled(options.layer, { execution });
219
+ logHandled(layer, { execution });
205
220
  return recovered;
206
221
  }
207
222
  finish();
208
- observe(() => options.announce.completed({
209
- executable: command,
210
- execution,
211
- layer: options.layer,
212
- answer,
213
- durationMs: Math.max(0, Date.now() - startedAt),
214
- }), undefined);
223
+ if (useCaseLayer)
224
+ observe(() => options.announce?.completed({
225
+ executable: command,
226
+ execution,
227
+ layer: useCaseLayer,
228
+ answer,
229
+ durationMs: Math.max(0, Date.now() - startedAt),
230
+ }), undefined);
215
231
  return answer;
216
232
  }
@@ -41,7 +41,7 @@ export type StoredQueue = EventQueue & {
41
41
  * each platform plugs.
42
42
  *
43
43
  * const queue = storedQueue({ store: sqliteEventStore(db), subscriber, ready: online });
44
- * shop.addDependency("publisher", new Publisher(queue));
44
+ * shopBoundedContext.addDependency("publisher", new Publisher(queue));
45
45
  * connectivity.onBack(() => queue.drain());
46
46
  *
47
47
  * Context: what a phone did by hand in its event bus, the same on every platform. `publish`
@@ -60,8 +60,9 @@ export type StoredQueue = EventQueue & {
60
60
  * refused it for good or ran out of attempts; `onSettled` hears either.
61
61
  *
62
62
  * Delivery is at least once: a process that stops after a context executed an event and before
63
- * the store saved it executes it there again on the next drain. A subscriber that must not act
64
- * twice keys what it does by the envelope's id — `this.execution.causationId` in its run. And one
65
- * process drains a store: two tabs over one IndexedDB claim it first (the Web Locks API).
63
+ * the store saved it executes it there again on the next drain. What must not act twice keys what
64
+ * it does by the envelope's id — `this.execution.causationId` in a use case of the event, or in an
65
+ * `EventSubscriber`, which hands it to the Command it executes — or by the entity the event names.
66
+ * And one process drains a store: two tabs over one IndexedDB claim it first (the Web Locks API).
66
67
  */
67
68
  export declare function storedQueue(options: StoredQueueOptions): StoredQueue;
@@ -22,7 +22,7 @@ function described(boundedContext, error) {
22
22
  * each platform plugs.
23
23
  *
24
24
  * const queue = storedQueue({ store: sqliteEventStore(db), subscriber, ready: online });
25
- * shop.addDependency("publisher", new Publisher(queue));
25
+ * shopBoundedContext.addDependency("publisher", new Publisher(queue));
26
26
  * connectivity.onBack(() => queue.drain());
27
27
  *
28
28
  * Context: what a phone did by hand in its event bus, the same on every platform. `publish`
@@ -41,9 +41,10 @@ function described(boundedContext, error) {
41
41
  * refused it for good or ran out of attempts; `onSettled` hears either.
42
42
  *
43
43
  * Delivery is at least once: a process that stops after a context executed an event and before
44
- * the store saved it executes it there again on the next drain. A subscriber that must not act
45
- * twice keys what it does by the envelope's id — `this.execution.causationId` in its run. And one
46
- * process drains a store: two tabs over one IndexedDB claim it first (the Web Locks API).
44
+ * the store saved it executes it there again on the next drain. What must not act twice keys what
45
+ * it does by the envelope's id — `this.execution.causationId` in a use case of the event, or in an
46
+ * `EventSubscriber`, which hands it to the Command it executes — or by the entity the event names.
47
+ * And one process drains a store: two tabs over one IndexedDB claim it first (the Web Locks API).
47
48
  */
48
49
  export function storedQueue(options) {
49
50
  const { store, subscriber } = options;
@@ -7,7 +7,7 @@ import type { EventQueue } from "../domain/queue.js";
7
7
  * Puts events into a queue — what a Feature holds to tell whoever listens, as the backend's
8
8
  * `Publisher`:
9
9
  *
10
- * shop.addDependency("publisher", new Publisher(queue));
10
+ * shopBoundedContext.addDependency("publisher", new Publisher(queue));
11
11
  * // in a Feature:
12
12
  * await this.publisher.publish(new OrderPaid(order.id)); // its Envelope
13
13
  *
@@ -4,7 +4,7 @@ import { envelopeOf, } from "../domain/envelope.js";
4
4
  * Puts events into a queue — what a Feature holds to tell whoever listens, as the backend's
5
5
  * `Publisher`:
6
6
  *
7
- * shop.addDependency("publisher", new Publisher(queue));
7
+ * shopBoundedContext.addDependency("publisher", new Publisher(queue));
8
8
  * // in a Feature:
9
9
  * await this.publisher.publish(new OrderPaid(order.id)); // its Envelope
10
10
  *
@@ -2,7 +2,11 @@ import type { DomainEvent, EventClass } from "../../core/domain/dto.js";
2
2
  import type { Cause } from "../../core/domain/pipeline.js";
3
3
  import type { Envelope } from "../domain/envelope.js";
4
4
  import type { Handled, Subscribes } from "../domain/queue.js";
5
- /** What the subscriber needs of a bounded context: whether it hears an event, and running it. */
5
+ /**
6
+ * What the subscriber needs of whoever hears events — a bounded context (a use case of the event)
7
+ * or `Subscriptions` (an `EventSubscriber` of it): its name, whether it hears an event, and
8
+ * running it.
9
+ */
6
10
  export type HearingContext = {
7
11
  readonly name: string;
8
12
  handlerOf(dto: abstract new (...args: never[]) => DomainEvent): unknown;
@@ -10,14 +14,18 @@ export type HearingContext = {
10
14
  execute(event: DomainEvent, cause?: Cause): Promise<unknown>;
11
15
  };
12
16
  /**
13
- * Hands an event to every bounded context that hears it — the backend's `Subscriber(buses)`:
17
+ * The door of a queue: hands an event to everyone who hears it — the backend's
18
+ * `Subscriber(buses)`:
14
19
  *
15
- * const subscriber = new Subscriber(shop, mail, stock);
20
+ * const subscriber = new Subscriber(shopBoundedContext, mailBoundedContext, ticketsSubscriptions);
16
21
  * const queue = syncQueue(subscriber); // or a platform's durable queue
17
22
  *
18
- * Context: one subscriber per bounded context (its Feature or ApplicationService registered for
19
- * the event class); several contexts hearing one event is several executions, one each, in the
20
- * order named. A context that fails does not stop the others: each answer, or failure, is said.
23
+ * Context: who hears an event is a bounded context with a use case of it (a Feature or an
24
+ * ApplicationService registered for its class) or `Subscriptions` with an `EventSubscriber` of
25
+ * it — the entrypoint that calls use cases. Several hearing one event is several executions, one
26
+ * each, in the order named; one that fails does not stop the others: each answer, or failure, is
27
+ * said under its name — which a stored queue keeps, so a redelivery never runs again one that
28
+ * already heard it. Two of them never share a name.
21
29
  */
22
30
  export declare class Subscriber implements Subscribes {
23
31
  private readonly contexts;