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