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