@sincpro/client-framework 1.0.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 (84) hide show
  1. package/AGENTS.md +71 -13
  2. package/MANIFESTO.md +59 -14
  3. package/README.md +456 -55
  4. package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
  5. package/dist/circuit_breaker/domain/circuit.js +37 -0
  6. package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
  7. package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
  8. package/dist/circuit_breaker/index.d.ts +9 -0
  9. package/dist/circuit_breaker/index.js +9 -0
  10. package/dist/client/adapters/json-rpc.d.ts +40 -0
  11. package/dist/client/adapters/json-rpc.js +97 -0
  12. package/dist/client/adapters/rest-operations.d.ts +19 -0
  13. package/dist/client/adapters/rest-operations.js +66 -0
  14. package/dist/client/domain/operation.d.ts +73 -0
  15. package/dist/client/domain/operation.js +19 -0
  16. package/dist/client/index.d.ts +5 -1
  17. package/dist/client/index.js +5 -1
  18. package/dist/client/services/guarded.d.ts +17 -0
  19. package/dist/client/services/guarded.js +19 -0
  20. package/dist/client/services/propagation.d.ts +20 -7
  21. package/dist/client/services/propagation.js +44 -15
  22. package/dist/codegen/entrypoint/cli.js +7 -7
  23. package/dist/codegen/services/client.d.ts +7 -8
  24. package/dist/codegen/services/client.js +50 -148
  25. package/dist/codegen/services/scaffold.d.ts +7 -4
  26. package/dist/codegen/services/scaffold.js +23 -27
  27. package/dist/common/failures.d.ts +83 -0
  28. package/dist/common/failures.js +212 -0
  29. package/dist/common/index.d.ts +1 -0
  30. package/dist/common/index.js +1 -0
  31. package/dist/context/domain/execution.d.ts +12 -0
  32. package/dist/context/entrypoint/context.d.ts +5 -4
  33. package/dist/core/domain/bus.d.ts +16 -7
  34. package/dist/core/domain/bus.js +14 -6
  35. package/dist/core/domain/dto.d.ts +33 -3
  36. package/dist/core/domain/dto.js +24 -1
  37. package/dist/core/domain/errors.d.ts +2 -2
  38. package/dist/core/domain/pipeline.d.ts +89 -0
  39. package/dist/core/domain/pipeline.js +1 -0
  40. package/dist/core/domain/services.d.ts +16 -5
  41. package/dist/core/domain/services.js +5 -2
  42. package/dist/core/entrypoint/bounded-context.d.ts +96 -16
  43. package/dist/core/entrypoint/bounded-context.js +244 -39
  44. package/dist/core/index.d.ts +4 -2
  45. package/dist/core/index.js +3 -2
  46. package/dist/core/infrastructure/listeners.d.ts +28 -0
  47. package/dist/core/infrastructure/listeners.js +34 -0
  48. package/dist/core/services/binding.d.ts +2 -2
  49. package/dist/core/services/binding.js +6 -3
  50. package/dist/core/services/execution.d.ts +59 -12
  51. package/dist/core/services/execution.js +114 -19
  52. package/dist/core/services/interceptors.d.ts +15 -0
  53. package/dist/core/services/interceptors.js +25 -0
  54. package/dist/core/services/listening.d.ts +17 -0
  55. package/dist/core/services/listening.js +25 -0
  56. package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
  57. package/dist/event_driven/adapters/memory-event-store.js +23 -0
  58. package/dist/event_driven/adapters/stored-queue.d.ts +68 -0
  59. package/dist/event_driven/adapters/stored-queue.js +193 -0
  60. package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
  61. package/dist/event_driven/adapters/sync-queue.js +19 -0
  62. package/dist/event_driven/domain/envelope.d.ts +28 -0
  63. package/dist/event_driven/domain/envelope.js +19 -0
  64. package/dist/event_driven/domain/queue.d.ts +40 -0
  65. package/dist/event_driven/domain/queue.js +1 -0
  66. package/dist/event_driven/domain/store.d.ts +56 -0
  67. package/dist/event_driven/domain/store.js +1 -0
  68. package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
  69. package/dist/event_driven/entrypoint/publisher.js +35 -0
  70. package/dist/event_driven/entrypoint/subscriber.d.ts +42 -0
  71. package/dist/event_driven/entrypoint/subscriber.js +62 -0
  72. package/dist/event_driven/entrypoint/subscriptions.d.ts +110 -0
  73. package/dist/event_driven/entrypoint/subscriptions.js +184 -0
  74. package/dist/event_driven/index.d.ts +22 -0
  75. package/dist/event_driven/index.js +20 -0
  76. package/dist/index.d.ts +7 -2
  77. package/dist/index.js +7 -2
  78. package/dist/observability/adapters/memory.d.ts +1 -1
  79. package/dist/observability/domain/logging.d.ts +10 -4
  80. package/dist/observability/services/logging.d.ts +7 -4
  81. package/dist/observability/services/logging.js +3 -2
  82. package/dist/observability/services/outcome.d.ts +6 -6
  83. package/dist/observability/services/outcome.js +10 -13
  84. package/package.json +1 -1
@@ -1,13 +1,15 @@
1
+ import type { Execution } from "../../context/domain/execution.js";
1
2
  import type { ContextValues } from "../../context/domain/store.js";
2
3
  import type { FeatureBus } from "./bus.js";
3
- import type { Command } from "./dto.js";
4
+ import type { Command, DomainEvent } from "./dto.js";
4
5
  import type { LayerLogger } from "../../observability/domain/logging.js";
5
6
  import type { Span } from "../../observability/domain/tracing.js";
6
7
  /**
7
8
  * The services of the application layer, as sincpro_framework's: a Feature executes one
8
- * registered Command; an ApplicationService composes its context's Features.
9
+ * registered Command — or domain event, as its subscriber —; an ApplicationService composes its
10
+ * context's Features.
9
11
  *
10
- * @project.feature(CommandCancelIssue)
12
+ * @projectBoundedContext.feature(CommandCancelIssue)
11
13
  * class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
12
14
  * static readonly serviceName = "CancelIssue";
13
15
  * async execute(command: CommandCancelIssue) {
@@ -20,16 +22,25 @@ import type { Span } from "../../observability/domain/tracing.js";
20
22
  * every dependency are that run's (`executeService`). `serviceName` is stable metadata, never
21
23
  * `constructor.name`, which a minifier renames.
22
24
  */
23
- export declare abstract class Feature<C extends Command<R>, R, Context extends object = ContextValues> {
25
+ export declare abstract class Feature<C extends Command<R> | DomainEvent, R, Context extends object = ContextValues> {
24
26
  /** What this run reads: the process's context, its bounded context's, its scope's. */
25
27
  readonly context: Readonly<Partial<Context>>;
26
28
  /** This run's span: `this.span.set({...})` says more about the use case. */
27
29
  readonly span: Span;
28
30
  /** This run's logger: every line carries its use case, execution and trace. */
29
31
  readonly logger: LayerLogger;
32
+ /** What cancels this run, when it was executed with one: check it in a long loop. */
33
+ readonly signal: AbortSignal | undefined;
34
+ /**
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.
39
+ */
40
+ readonly execution: Execution;
30
41
  abstract execute(command: C): R | Promise<R>;
31
42
  }
32
- export declare abstract class ApplicationService<C extends Command<R>, R, Context extends object = ContextValues> extends Feature<C, R, Context> {
43
+ export declare abstract class ApplicationService<C extends Command<R> | DomainEvent, R, Context extends object = ContextValues> extends Feature<C, R, Context> {
33
44
  /** The context's Features, run as children of this execution. */
34
45
  protected readonly featureBus: FeatureBus;
35
46
  }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * The services of the application layer, as sincpro_framework's: a Feature executes one
3
- * registered Command; an ApplicationService composes its context's Features.
3
+ * registered Command — or domain event, as its subscriber —; an ApplicationService composes its
4
+ * context's Features.
4
5
  *
5
- * @project.feature(CommandCancelIssue)
6
+ * @projectBoundedContext.feature(CommandCancelIssue)
6
7
  * class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
7
8
  * static readonly serviceName = "CancelIssue";
8
9
  * async execute(command: CommandCancelIssue) {
@@ -25,4 +26,6 @@ export const RESERVED_MEMBERS = new Set([
25
26
  "context",
26
27
  "span",
27
28
  "logger",
29
+ "signal",
30
+ "execution",
28
31
  ]);
@@ -1,30 +1,49 @@
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";
4
- import type { Answer, AnyCommand, Command, CommandClass, ResponseClass } from "../domain/dto.js";
5
+ import { type Answer, type AnyCommand, Command, type CommandClass, DomainEvent, type EventClass, type Executable, type ResponseClass } from "../domain/dto.js";
5
6
  import type { ErrorClass, ErrorHandler } from "../domain/errors.js";
7
+ import type { Cause, ExecuteOptions, ExecutionCompleted, ExecutionFailed, ExecutionStarted, Extension, Interceptor, Listener } from "../domain/pipeline.js";
8
+ import { type ExecutableClass } from "../services/listening.js";
6
9
  import type { LayerLogger } from "../../observability/domain/logging.js";
7
- interface Executor<C extends AnyCommand, R> {
8
- execute(command: C): R | Promise<R>;
10
+ interface Executor<C extends Executable, R> {
11
+ execute(executable: C): R | Promise<R>;
9
12
  }
10
- type ServiceClass<C extends AnyCommand, R> = {
13
+ type ServiceClass<C extends Executable, R> = {
11
14
  new (): Executor<C, R>;
12
15
  readonly serviceName: string;
13
16
  };
17
+ /** What a service is registered for: a Command's class or a domain event's. */
18
+ type DtoClass = ExecutableClass;
19
+ /** The service a registration takes the place of, as the backend's `replaces=`. */
20
+ export type RegistrationOptions = {
21
+ replaces?: abstract new (...args: never[]) => object;
22
+ };
23
+ /** What `feature(...)` and `appService(...)` answer: the function that registers the service. */
24
+ export type Registrar<T extends Executable> = <S extends ServiceClass<NoInfer<T>, Answer<NoInfer<T>>>>(Service: S, decorator?: ClassDecoratorContext<S>) => S;
14
25
  /**
15
- * A bounded context: the bus of its Commands, as sincpro_framework's `UseFramework`.
26
+ * A bounded context: the bus of its Commands and domain events, as sincpro_framework's
27
+ * `UseFramework`.
16
28
  *
17
- * const project = new BoundedContext<ProjectDependencies>("project");
18
- * project.addDependency("client", clientOf(transport));
19
- * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
20
- * project.addFeatureErrorHandler(toEmptyWhenMissing);
21
- * project.expectErrors(IssueClosed); // refused, never reported
22
- * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
23
- * 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");
24
39
  *
25
40
  * Context: registrations, dependencies, handlers and expected errors are snapshotted per
26
41
  * generation; a call keeps the generation it started with, and what is added later answers from
27
- * the next call — as the backend's `build_root_bus`.
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`).
28
47
  */
29
48
  export declare class BoundedContext<D extends object = Record<never, never>, C extends object = ContextValues> {
30
49
  readonly name: string;
@@ -36,6 +55,8 @@ export declare class BoundedContext<D extends object = Record<never, never>, C e
36
55
  private readonly dependencyValues;
37
56
  private readonly handlers;
38
57
  private readonly expected;
58
+ private readonly interceptors;
59
+ private readonly listeners;
39
60
  private revision;
40
61
  private builtRevision;
41
62
  private built;
@@ -44,8 +65,43 @@ export declare class BoundedContext<D extends object = Record<never, never>, C e
44
65
  addDependency<K extends keyof D & string>(name: K, dependency: D[K], options?: {
45
66
  replaces: D[K];
46
67
  }): this;
47
- feature<T extends AnyCommand>(command: CommandClass<T>): <S extends ServiceClass<NoInfer<T>, Answer<NoInfer<T>>>>(Service: S, decorator?: ClassDecoratorContext<S> | undefined) => S;
48
- appService<T extends AnyCommand>(command: CommandClass<T>): <S extends ServiceClass<NoInfer<T>, Answer<NoInfer<T>>>>(Service: S, decorator?: ClassDecoratorContext<S> | undefined) => S;
68
+ /**
69
+ * Registers the Feature that executes a Command — or that subscribes to a domain event. With
70
+ * `replaces`, it takes the place of the service registered now for it.
71
+ */
72
+ feature<T extends AnyCommand>(command: CommandClass<T>, options?: RegistrationOptions): Registrar<T>;
73
+ feature<E extends DomainEvent>(event: EventClass<E>, options?: RegistrationOptions): Registrar<E>;
74
+ /** Registers the ApplicationService of a Command or a domain event; `replaces` as `feature`. */
75
+ appService<T extends AnyCommand>(command: CommandClass<T>, options?: RegistrationOptions): Registrar<T>;
76
+ appService<E extends DomainEvent>(event: EventClass<E>, options?: RegistrationOptions): Registrar<E>;
77
+ /** The service registered now for a Command or an event class — what a `replaces` names. */
78
+ handlerOf(dto: DtoClass): (abstract new (...args: never[]) => object) | undefined;
79
+ /**
80
+ * The event class registered here under a wire name, or `undefined` when nothing here hears it
81
+ * — what a platform's queue rebuilds a stored event as, as the backend's `event_type(name)`.
82
+ */
83
+ eventTypeOf(name: string): EventClass<DomainEvent> | undefined;
84
+ /**
85
+ * Runs `interceptor` around every execution of these classes — of every one when none is
86
+ * named —, outermost first in the order registered. It may look, veto (throw), adjust what it
87
+ * passes on (of the same class) or answer by itself. Registered after a call, it wraps from
88
+ * the next one.
89
+ */
90
+ interceptor<T extends Executable = Executable>(...dtos: (abstract new (...args: never[]) => T)[]): <F extends Interceptor<T>>(interceptor: F) => F;
91
+ /** Stops `interceptor` wherever it was registered; from the next call. */
92
+ withoutInterceptor(interceptor: (...args: never[]) => unknown): this;
93
+ /** Hears every run of these classes (every one when none is named) begin. Answers how to stop. */
94
+ onStart(listener: Listener<ExecutionStarted>, ...dtos: DtoClass[]): () => void;
95
+ /** Hears every run of these classes whose service answered — not an answer a handler gave. */
96
+ onCompletion(listener: Listener<ExecutionCompleted>, ...dtos: DtoClass[]): () => void;
97
+ /** Hears every failure that escapes a call — after every handler —, once, where it failed. */
98
+ onFailure(listener: Listener<ExecutionFailed>, ...dtos: DtoClass[]): () => void;
99
+ /**
100
+ * Wires a component into this bounded context — its dependencies, services, interceptors,
101
+ * listeners, handlers — in one place: a module of an application, a plugin. `fresh()` copies
102
+ * all it registered.
103
+ */
104
+ extend(extension: Extension<this>): this;
49
105
  /** Answers a failure that reaches `execute` (or a scope's): after every layer's handlers. */
50
106
  addGlobalErrorHandler(handler: ErrorHandler): this;
51
107
  /** Answers a Feature's failure, wherever it was executed from. */
@@ -57,12 +113,36 @@ export declare class BoundedContext<D extends object = Record<never, never>, C e
57
113
  * its span is no error and nothing is reported. They are still thrown — or handled.
58
114
  */
59
115
  expectErrors(...types: ErrorClass[]): this;
60
- execute<R>(command: Command<R>, _returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
116
+ /**
117
+ * Executes a Command — its Response, inferred —, or a domain event — its subscriber's answer.
118
+ * `{ signal }` cancels the run (it ends `aborted`); a delivered event's envelope continues its
119
+ * flow (`execute(event, envelope)`).
120
+ */
121
+ execute<R>(command: Command<R>, returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
122
+ execute<R>(command: Command<R>, options: ExecuteOptions): Promise<R>;
123
+ execute(event: DomainEvent, options?: ExecuteOptions | Cause): Promise<unknown>;
61
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;
62
133
  buildRootBus(): void;
63
134
  fresh(): BoundedContext<D, C>;
135
+ /** What escaped a call, told once, from where it first failed. */
136
+ private escaped;
64
137
  private addErrorHandler;
65
138
  private registrar;
139
+ /**
140
+ * 1. The service is of the layer it is registered in, for a Command or a domain event class.
141
+ * 2. The class has no service yet — or, with `replaces`, the one it has is that one.
142
+ * 3. Its `serviceName` is given and no other service has it; an event's `eventName` is given
143
+ * and no other event class here has it — the name means one event.
144
+ * Final: registered, said on its spans when it replaced another; the next call answers with it.
145
+ */
66
146
  private registerService;
67
147
  private generation;
68
148
  private buildGeneration;
@@ -1,9 +1,14 @@
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
- import { executeTyped, FeatureBus, } from "../domain/bus.js";
4
+ import { FeatureBus, optionsOf, } from "../domain/bus.js";
5
+ import { Command, DomainEvent, } from "../domain/dto.js";
4
6
  import { ApplicationService, Feature, RESERVED_MEMBERS, } from "../domain/services.js";
7
+ import { processHearing } from "../infrastructure/listeners.js";
5
8
  import { handled } from "../services/error-handlers.js";
6
- import { executeService } from "../services/execution.js";
9
+ import { executeService, failedAt } from "../services/execution.js";
10
+ import { announceTo, concerns, listenIn, } from "../services/listening.js";
11
+ import { observabilityNow } from "../../observability/infrastructure/registry.js";
7
12
  import { boundedContextLogger, logHandled, } from "../../observability/services/logging.js";
8
13
  function isExecutor(value) {
9
14
  return (typeof value === "object" &&
@@ -11,20 +16,35 @@ function isExecutor(value) {
11
16
  "execute" in value &&
12
17
  typeof value.execute === "function");
13
18
  }
19
+ function isEventClass(dto) {
20
+ return dto.prototype instanceof DomainEvent;
21
+ }
22
+ function nameOf(Service) {
23
+ const named = Service;
24
+ return String(named?.serviceName ?? named?.name ?? Service);
25
+ }
14
26
  /**
15
- * A bounded context: the bus of its Commands, as sincpro_framework's `UseFramework`.
27
+ * A bounded context: the bus of its Commands and domain events, as sincpro_framework's
28
+ * `UseFramework`.
16
29
  *
17
- * const project = new BoundedContext<ProjectDependencies>("project");
18
- * project.addDependency("client", clientOf(transport));
19
- * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
20
- * project.addFeatureErrorHandler(toEmptyWhenMissing);
21
- * project.expectErrors(IssueClosed); // refused, never reported
22
- * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
23
- * 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");
24
40
  *
25
41
  * Context: registrations, dependencies, handlers and expected errors are snapshotted per
26
42
  * generation; a call keeps the generation it started with, and what is added later answers from
27
- * the next call — as the backend's `build_root_bus`.
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`).
28
48
  */
29
49
  export class BoundedContext {
30
50
  name;
@@ -40,6 +60,12 @@ export class BoundedContext {
40
60
  application_service: [],
41
61
  };
42
62
  expected = [];
63
+ interceptors = [];
64
+ listeners = {
65
+ started: new Set(),
66
+ completed: new Set(),
67
+ failed: new Set(),
68
+ };
43
69
  revision = 0;
44
70
  builtRevision = -1;
45
71
  built;
@@ -89,11 +115,70 @@ export class BoundedContext {
89
115
  this.revision++;
90
116
  return this;
91
117
  }
92
- feature(command) {
93
- return this.registrar(command, "feature");
118
+ feature(dto, options) {
119
+ return this.registrar(dto, "feature", options);
120
+ }
121
+ appService(dto, options) {
122
+ return this.registrar(dto, "application_service", options);
123
+ }
124
+ /** The service registered now for a Command or an event class — what a `replaces` names. */
125
+ handlerOf(dto) {
126
+ return this.registrations.get(dto)?.Service;
127
+ }
128
+ /**
129
+ * The event class registered here under a wire name, or `undefined` when nothing here hears it
130
+ * — what a platform's queue rebuilds a stored event as, as the backend's `event_type(name)`.
131
+ */
132
+ eventTypeOf(name) {
133
+ for (const dto of this.registrations.keys()) {
134
+ if (isEventClass(dto) && dto.eventName === name)
135
+ return dto;
136
+ }
137
+ return undefined;
138
+ }
139
+ /**
140
+ * Runs `interceptor` around every execution of these classes — of every one when none is
141
+ * named —, outermost first in the order registered. It may look, veto (throw), adjust what it
142
+ * passes on (of the same class) or answer by itself. Registered after a call, it wraps from
143
+ * the next one.
144
+ */
145
+ interceptor(...dtos) {
146
+ return (interceptor) => {
147
+ if (typeof interceptor !== "function")
148
+ throw new TypeError("An interceptor is a function");
149
+ // Registered for these classes only, it is called only with them.
150
+ this.interceptors.push({ what: interceptor, dtos });
151
+ this.revision++;
152
+ return interceptor;
153
+ };
154
+ }
155
+ /** Stops `interceptor` wherever it was registered; from the next call. */
156
+ withoutInterceptor(interceptor) {
157
+ const kept = this.interceptors.filter((one) => one.what !== interceptor);
158
+ this.interceptors.splice(0, this.interceptors.length, ...kept);
159
+ this.revision++;
160
+ return this;
161
+ }
162
+ /** Hears every run of these classes (every one when none is named) begin. Answers how to stop. */
163
+ onStart(listener, ...dtos) {
164
+ return listenIn(this.listeners.started, listener, dtos);
94
165
  }
95
- appService(command) {
96
- return this.registrar(command, "application_service");
166
+ /** Hears every run of these classes whose service answered — not an answer a handler gave. */
167
+ onCompletion(listener, ...dtos) {
168
+ return listenIn(this.listeners.completed, listener, dtos);
169
+ }
170
+ /** Hears every failure that escapes a call — after every handler —, once, where it failed. */
171
+ onFailure(listener, ...dtos) {
172
+ return listenIn(this.listeners.failed, listener, dtos);
173
+ }
174
+ /**
175
+ * Wires a component into this bounded context — its dependencies, services, interceptors,
176
+ * listeners, handlers — in one place: a module of an application, a plugin. `fresh()` copies
177
+ * all it registered.
178
+ */
179
+ extend(extension) {
180
+ extension(this);
181
+ return this;
97
182
  }
98
183
  /** Answers a failure that reaches `execute` (or a scope's): after every layer's handlers. */
99
184
  addGlobalErrorHandler(handler) {
@@ -121,20 +206,46 @@ export class BoundedContext {
121
206
  this.revision++;
122
207
  return this;
123
208
  }
124
- execute(command, _returnType) {
125
- return executeTyped((one) => this.generation().root(one, {}), command);
209
+ execute(executable, second) {
210
+ // As with Python's return_type, the answer is not validated at runtime.
211
+ return this.generation().root(executable, {}, optionsOf(second));
126
212
  }
127
213
  scoped(context) {
128
214
  const scope = { ...context };
129
- return new FeatureBus((command) => this.generation().root(command, scope));
215
+ return new FeatureBus((executable, options) => this.generation().root(executable, scope, options));
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
+ });
130
241
  }
131
242
  buildRootBus() {
132
243
  this.generation();
133
244
  }
134
245
  fresh() {
135
246
  const fresh = new BoundedContext(this.name);
136
- for (const [command, registration] of this.registrations) {
137
- fresh.registrations.set(command, registration);
247
+ for (const [dto, registration] of this.registrations) {
248
+ fresh.registrations.set(dto, registration);
138
249
  }
139
250
  for (const [name, dependency] of this.dependencyValues) {
140
251
  fresh.dependencyValues.set(name, dependency);
@@ -143,9 +254,36 @@ export class BoundedContext {
143
254
  fresh.handlers[where].push(...this.handlers[where]);
144
255
  }
145
256
  fresh.expected.push(...this.expected);
257
+ fresh.interceptors.push(...this.interceptors);
258
+ for (const moment of ["started", "completed", "failed"]) {
259
+ for (const one of this.listeners[moment])
260
+ fresh.listeners[moment].add(one);
261
+ }
146
262
  fresh.revision = this.revision;
147
263
  return fresh;
148
264
  }
265
+ /** What escaped a call, told once, from where it first failed. */
266
+ escaped(executable, error, expected) {
267
+ const at = failedAt(error);
268
+ let outcome = "failed";
269
+ try {
270
+ outcome =
271
+ at?.outcome ?? (expected(error) ? "refused" : observabilityNow().outcomeOf(error));
272
+ }
273
+ catch {
274
+ // An outcome that cannot be told is a failure.
275
+ }
276
+ const heard = {
277
+ executable: at?.executable ?? executable,
278
+ execution: at?.execution,
279
+ // Listeners hear use cases: a failure an entrypoint's run started says no layer.
280
+ layer: at?.layer === "entrypoint" ? undefined : at?.layer,
281
+ error,
282
+ outcome,
283
+ };
284
+ announceTo(this.listeners.failed, heard);
285
+ announceTo(processHearing().failed, heard);
286
+ }
149
287
  addErrorHandler(where, handler) {
150
288
  if (typeof handler !== "function")
151
289
  throw new TypeError("An error handler is a function");
@@ -153,9 +291,9 @@ export class BoundedContext {
153
291
  this.revision++;
154
292
  return this;
155
293
  }
156
- registrar(command, layer) {
294
+ registrar(dto, layer, options = {}) {
157
295
  return (Service, decorator) => {
158
- const register = () => this.registerService(command, Service, layer);
296
+ const register = () => this.registerService(dto, Service, layer, options.replaces);
159
297
  // Standard decorators run before static fields have initialized.
160
298
  if (decorator)
161
299
  decorator.addInitializer(register);
@@ -164,23 +302,54 @@ export class BoundedContext {
164
302
  return Service;
165
303
  };
166
304
  }
167
- registerService(command, Service, layer) {
305
+ /**
306
+ * 1. The service is of the layer it is registered in, for a Command or a domain event class.
307
+ * 2. The class has no service yet — or, with `replaces`, the one it has is that one.
308
+ * 3. Its `serviceName` is given and no other service has it; an event's `eventName` is given
309
+ * and no other event class here has it — the name means one event.
310
+ * Final: registered, said on its spans when it replaced another; the next call answers with it.
311
+ */
312
+ registerService(dto, Service, layer, replaces) {
168
313
  const Base = layer === "feature" ? Feature : ApplicationService;
169
314
  if (!(Service.prototype instanceof Base) ||
170
315
  (layer === "feature" && Service.prototype instanceof ApplicationService)) {
171
316
  throw new TypeError(`The service is not an ${layer}`);
172
317
  }
173
- if (this.registrations.has(command)) {
318
+ const event = isEventClass(dto);
319
+ if (!event && !(dto.prototype instanceof Command)) {
320
+ throw new TypeError("A service is registered for a Command or a DomainEvent class");
321
+ }
322
+ const current = this.registrations.get(dto);
323
+ if (replaces !== undefined && current?.Service !== replaces) {
324
+ throw new Error(`Replacement does not match the registration: ${nameOf(replaces)}`);
325
+ }
326
+ if (replaces === undefined && current) {
174
327
  throw new Error("Command already registered: one executor across both layers");
175
328
  }
176
329
  const name = Service.serviceName;
177
330
  if (typeof name !== "string" || name.trim() === "") {
178
331
  throw new Error("A service needs a nonempty registered name");
179
332
  }
180
- if ([...this.registrations.values()].some((registration) => registration.name === name)) {
181
- throw new Error(`Service name already registered: ${name}`);
333
+ for (const [other, registration] of this.registrations) {
334
+ if (registration !== current && registration.name === name) {
335
+ throw new Error(`Service name already registered: ${name}`);
336
+ }
337
+ if (event &&
338
+ other !== dto &&
339
+ isEventClass(other) &&
340
+ other.eventName === dto.eventName) {
341
+ throw new Error(`Event name already registered: ${dto.eventName}`);
342
+ }
182
343
  }
183
- this.registrations.set(command, { name, layer, Service });
344
+ if (event && (typeof dto.eventName !== "string" || dto.eventName.trim() === "")) {
345
+ throw new Error("A domain event needs a nonempty eventName");
346
+ }
347
+ this.registrations.set(dto, {
348
+ name,
349
+ layer,
350
+ Service,
351
+ ...(current ? { replaces: current.name } : {}),
352
+ });
184
353
  this.revision++;
185
354
  }
186
355
  generation() {
@@ -202,6 +371,18 @@ export class BoundedContext {
202
371
  };
203
372
  const expectedTypes = [...this.expected];
204
373
  const expected = (error) => expectedTypes.some((type) => error instanceof type);
374
+ const interceptors = [...this.interceptors];
375
+ const everyContext = processHearing();
376
+ const announce = {
377
+ started: (heard) => {
378
+ announceTo(this.listeners.started, heard);
379
+ announceTo(everyContext.started, heard);
380
+ },
381
+ completed: (heard) => {
382
+ announceTo(this.listeners.completed, heard);
383
+ announceTo(everyContext.completed, heard);
384
+ },
385
+ };
205
386
  const instances = new Map();
206
387
  for (const registration of registrations.values()) {
207
388
  const instance = new registration.Service();
@@ -210,9 +391,9 @@ export class BoundedContext {
210
391
  }
211
392
  instances.set(registration, instance);
212
393
  }
213
- const dispatch = async (command, parent, featureOnly = false, scope = {}) => {
214
- // Exact constructor identity: a subclass of a registered Command is another Command.
215
- const target = registrations.get(Object.getPrototypeOf(command)?.constructor);
394
+ const dispatch = async (executable, parent, featureOnly = false, scope = {}, options = {}) => {
395
+ // Exact constructor identity: a subclass of a registered class is another one.
396
+ const target = registrations.get(Object.getPrototypeOf(executable)?.constructor);
216
397
  if (!target)
217
398
  throw new Error("No executor registered for this command class");
218
399
  if (featureOnly && target.layer !== "feature") {
@@ -222,29 +403,53 @@ export class BoundedContext {
222
403
  if (!service) {
223
404
  throw new Error(`Registered service is unavailable: ${target.name}`);
224
405
  }
225
- return executeService(service, command, {
406
+ const attributes = {
407
+ ...(executable instanceof DomainEvent ? { "sincpro.event": executable.name } : {}),
408
+ ...(target.replaces ? { "sincpro.replaces": target.replaces } : {}),
409
+ };
410
+ return executeService(service, executable, {
226
411
  boundedContext: this.name,
227
412
  name: target.name,
228
413
  layer: target.layer,
414
+ attributes,
229
415
  dependencies,
230
416
  context: () => ({ ...processContext.values, ...this.context.values, ...scope }),
231
417
  ...(parent ? { parent } : {}),
232
- featureBus: (execution) => new FeatureBus((child) => dispatch(child, execution, true)),
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.
421
+ ...(options.cause && executable instanceof DomainEvent && !parent
422
+ ? { cause: options.cause }
423
+ : {}),
424
+ ...(options.signal ? { signal: options.signal } : {}),
425
+ featureBus: (execution) => new FeatureBus((child, options) => dispatch(child, execution, true, {}, options)),
233
426
  expected,
234
427
  handlers: handlers[target.layer],
428
+ interceptors: interceptors
429
+ .filter((one) => concerns(one.dtos, executable))
430
+ .map((one) => one.what),
431
+ announce,
235
432
  });
236
433
  };
237
434
  return {
238
- root: async (command, scope) => {
435
+ root: async (executable, scope, options) => {
239
436
  try {
240
- return await dispatch(command, undefined, false, scope);
437
+ return await dispatch(executable, undefined, false, scope, options);
241
438
  }
242
439
  catch (error) {
243
- if (handlers.global.length === 0)
244
- throw error;
245
- const answer = await handled(handlers.global, error);
246
- logHandled("bounded_context", { boundedContext: this.name });
247
- return answer;
440
+ let escaping = error;
441
+ if (handlers.global.length > 0) {
442
+ try {
443
+ const answer = await handled(handlers.global, error);
444
+ logHandled("bounded_context", { boundedContext: this.name });
445
+ return answer;
446
+ }
447
+ catch (thrown) {
448
+ escaping = thrown;
449
+ }
450
+ }
451
+ this.escaped(executable, escaping, expected);
452
+ throw escaping;
248
453
  }
249
454
  },
250
455
  };
@@ -9,9 +9,11 @@
9
9
  * infrastructure/ `configureProcess`, `configureLogger`
10
10
  */
11
11
  export { type CommandBus, FeatureBus } from "./domain/bus.js";
12
- export { Command, DataTransferObject, Response, } from "./domain/dto.js";
12
+ export { Command, DataTransferObject, DomainEvent, type EventClass, Response, } from "./domain/dto.js";
13
13
  export type { ErrorClass, ErrorHandler } from "./domain/errors.js";
14
+ export type { CallNext, Cause, ExecuteOptions, ExecutionCompleted, ExecutionFailed, ExecutionStarted, Extension, InterceptedRun, Interceptor, Listener, PipelineLayer, } from "./domain/pipeline.js";
14
15
  export { ApplicationService, Feature } from "./domain/services.js";
15
- export { BoundedContext } from "./entrypoint/bounded-context.js";
16
+ export { BoundedContext, type Registrar, type RegistrationOptions, } from "./entrypoint/bounded-context.js";
16
17
  export { screensTransport } from "./entrypoint/screens.js";
18
+ export { processListeners } from "./infrastructure/listeners.js";
17
19
  export { configureLogger, configureProcess, type LoggerConfiguration, type ProcessOptions, } from "./infrastructure/process.js";