@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.
- package/AGENTS.md +71 -13
- package/MANIFESTO.md +59 -14
- package/README.md +456 -55
- package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
- package/dist/circuit_breaker/domain/circuit.js +37 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
- package/dist/circuit_breaker/index.d.ts +9 -0
- package/dist/circuit_breaker/index.js +9 -0
- package/dist/client/adapters/json-rpc.d.ts +40 -0
- package/dist/client/adapters/json-rpc.js +97 -0
- package/dist/client/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- package/dist/client/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +5 -1
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +20 -7
- package/dist/client/services/propagation.js +44 -15
- package/dist/codegen/entrypoint/cli.js +7 -7
- package/dist/codegen/services/client.d.ts +7 -8
- package/dist/codegen/services/client.js +50 -148
- package/dist/codegen/services/scaffold.d.ts +7 -4
- package/dist/codegen/services/scaffold.js +23 -27
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/context/domain/execution.d.ts +12 -0
- package/dist/context/entrypoint/context.d.ts +5 -4
- package/dist/core/domain/bus.d.ts +16 -7
- package/dist/core/domain/bus.js +14 -6
- package/dist/core/domain/dto.d.ts +33 -3
- package/dist/core/domain/dto.js +24 -1
- package/dist/core/domain/errors.d.ts +2 -2
- package/dist/core/domain/pipeline.d.ts +89 -0
- package/dist/core/domain/pipeline.js +1 -0
- package/dist/core/domain/services.d.ts +16 -5
- package/dist/core/domain/services.js +5 -2
- package/dist/core/entrypoint/bounded-context.d.ts +96 -16
- package/dist/core/entrypoint/bounded-context.js +244 -39
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.js +3 -2
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -0
- package/dist/core/services/binding.d.ts +2 -2
- package/dist/core/services/binding.js +6 -3
- package/dist/core/services/execution.d.ts +59 -12
- package/dist/core/services/execution.js +114 -19
- package/dist/core/services/interceptors.d.ts +15 -0
- package/dist/core/services/interceptors.js +25 -0
- package/dist/core/services/listening.d.ts +17 -0
- package/dist/core/services/listening.js +25 -0
- package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
- package/dist/event_driven/adapters/memory-event-store.js +23 -0
- package/dist/event_driven/adapters/stored-queue.d.ts +68 -0
- package/dist/event_driven/adapters/stored-queue.js +193 -0
- package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
- package/dist/event_driven/adapters/sync-queue.js +19 -0
- package/dist/event_driven/domain/envelope.d.ts +28 -0
- package/dist/event_driven/domain/envelope.js +19 -0
- package/dist/event_driven/domain/queue.d.ts +40 -0
- package/dist/event_driven/domain/queue.js +1 -0
- package/dist/event_driven/domain/store.d.ts +56 -0
- package/dist/event_driven/domain/store.js +1 -0
- package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
- package/dist/event_driven/entrypoint/publisher.js +35 -0
- package/dist/event_driven/entrypoint/subscriber.d.ts +42 -0
- package/dist/event_driven/entrypoint/subscriber.js +62 -0
- package/dist/event_driven/entrypoint/subscriptions.d.ts +110 -0
- package/dist/event_driven/entrypoint/subscriptions.js +184 -0
- package/dist/event_driven/index.d.ts +22 -0
- package/dist/event_driven/index.js +20 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/observability/adapters/memory.d.ts +1 -1
- package/dist/observability/domain/logging.d.ts +10 -4
- package/dist/observability/services/logging.d.ts +7 -4
- package/dist/observability/services/logging.js +3 -2
- package/dist/observability/services/outcome.d.ts +6 -6
- package/dist/observability/services/outcome.js +10 -13
- 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
|
|
9
|
+
* registered Command — or domain event, as its subscriber —; an ApplicationService composes its
|
|
10
|
+
* context's Features.
|
|
9
11
|
*
|
|
10
|
-
* @
|
|
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
|
|
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
|
|
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
|
|
3
|
+
* registered Command — or domain event, as its subscriber —; an ApplicationService composes its
|
|
4
|
+
* context's Features.
|
|
4
5
|
*
|
|
5
|
-
* @
|
|
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
|
|
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
|
|
8
|
-
execute(
|
|
10
|
+
interface Executor<C extends Executable, R> {
|
|
11
|
+
execute(executable: C): R | Promise<R>;
|
|
9
12
|
}
|
|
10
|
-
type ServiceClass<C extends
|
|
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
|
|
26
|
+
* A bounded context: the bus of its Commands and domain events, as sincpro_framework's
|
|
27
|
+
* `UseFramework`.
|
|
16
28
|
*
|
|
17
|
-
* const
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
27
|
+
* A bounded context: the bus of its Commands and domain events, as sincpro_framework's
|
|
28
|
+
* `UseFramework`.
|
|
16
29
|
*
|
|
17
|
-
* const
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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(
|
|
93
|
-
return this.registrar(
|
|
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
|
-
|
|
96
|
-
|
|
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(
|
|
125
|
-
|
|
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((
|
|
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 [
|
|
137
|
-
fresh.registrations.set(
|
|
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(
|
|
294
|
+
registrar(dto, layer, options = {}) {
|
|
157
295
|
return (Service, decorator) => {
|
|
158
|
-
const register = () => this.registerService(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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 (
|
|
214
|
-
// Exact constructor identity: a subclass of a registered
|
|
215
|
-
const target = registrations.get(Object.getPrototypeOf(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
435
|
+
root: async (executable, scope, options) => {
|
|
239
436
|
try {
|
|
240
|
-
return await dispatch(
|
|
437
|
+
return await dispatch(executable, undefined, false, scope, options);
|
|
241
438
|
}
|
|
242
439
|
catch (error) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
};
|
package/dist/core/index.d.ts
CHANGED
|
@@ -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";
|