@sincpro/client-framework 0.6.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 +95 -25
- package/MANIFESTO.md +103 -40
- package/README.md +608 -333
- 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/{memory.d.ts → adapters/memory.d.ts} +1 -1
- package/dist/client/{memory.js → adapters/memory.js} +2 -2
- package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
- package/dist/client/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
- package/dist/client/{rest.js → adapters/rest.js} +2 -2
- package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
- package/dist/client/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +20 -0
- package/dist/client/index.js +20 -0
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +45 -0
- package/dist/{services/execution.js → client/services/propagation.js} +49 -36
- package/dist/client/{retry.js → services/retry.js} +1 -1
- package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
- package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
- package/dist/codegen/{names.js → domain/names.js} +3 -3
- package/dist/codegen/{cli.js → entrypoint/cli.js} +15 -14
- package/dist/codegen/index.d.ts +7 -7
- package/dist/codegen/index.js +5 -10
- package/dist/codegen/services/client.d.ts +15 -0
- package/dist/codegen/services/client.js +111 -0
- package/dist/codegen/services/domain.d.ts +20 -0
- package/dist/codegen/{domain.js → services/domain.js} +82 -25
- package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
- package/dist/codegen/{entities.js → services/entities.js} +18 -4
- package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
- package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +8 -5
- package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +11 -0
- package/dist/common/index.js +11 -0
- package/dist/context/adapters/memory.d.ts +6 -0
- package/dist/context/adapters/memory.js +34 -0
- package/dist/context/adapters/plain-codec.d.ts +3 -0
- package/dist/context/adapters/plain-codec.js +16 -0
- package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
- package/dist/context/{codec.js → domain/codec.js} +0 -14
- package/dist/context/domain/execution.d.ts +45 -0
- package/dist/context/domain/execution.js +11 -0
- package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
- package/dist/context/{store.js → domain/store.js} +1 -34
- package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
- package/dist/context/index.d.ts +17 -0
- package/dist/context/index.js +15 -0
- package/dist/context/infrastructure/process.d.ts +13 -0
- package/dist/context/infrastructure/process.js +35 -0
- package/dist/core/domain/bus.d.ts +27 -0
- package/dist/core/domain/bus.js +26 -0
- package/dist/core/domain/dto.d.ts +62 -0
- package/dist/core/domain/dto.js +43 -0
- package/dist/core/domain/errors.d.ts +18 -0
- package/dist/core/domain/errors.js +1 -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 +47 -0
- package/dist/core/domain/services.js +31 -0
- package/dist/core/entrypoint/bounded-context.d.ts +139 -0
- package/dist/core/entrypoint/bounded-context.js +426 -0
- package/dist/core/entrypoint/screens.d.ts +6 -0
- package/dist/core/entrypoint/screens.js +136 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +17 -0
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -0
- package/dist/core/infrastructure/process.d.ts +56 -0
- package/dist/core/infrastructure/process.js +35 -0
- package/dist/core/services/binding.d.ts +6 -0
- package/dist/core/services/binding.js +16 -0
- package/dist/core/services/error-handlers.d.ts +11 -0
- package/dist/core/services/error-handlers.js +21 -0
- package/dist/core/services/execution.d.ts +66 -0
- package/dist/core/services/execution.js +216 -0
- 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 +21 -38
- package/dist/index.js +21 -38
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.js +10 -0
- package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
- package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
- package/dist/model/{schema.js → services/schema.js} +1 -1
- package/dist/observability/adapters/console.d.ts +12 -0
- package/dist/observability/adapters/console.js +35 -0
- package/dist/observability/adapters/memory.d.ts +41 -0
- package/dist/observability/adapters/memory.js +49 -0
- package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
- package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
- package/dist/observability/domain/errors.d.ts +23 -0
- package/dist/observability/domain/errors.js +8 -0
- package/dist/observability/domain/logging.d.ts +44 -0
- package/dist/observability/domain/logging.js +1 -0
- package/dist/observability/domain/tracing.d.ts +49 -0
- package/dist/observability/domain/tracing.js +20 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +4 -0
- package/dist/observability/infrastructure/registry.d.ts +21 -0
- package/dist/observability/infrastructure/registry.js +18 -0
- package/dist/observability/services/logging.d.ts +27 -0
- package/dist/observability/services/logging.js +106 -0
- package/dist/observability/services/outcome.d.ts +12 -0
- package/dist/observability/services/outcome.js +17 -0
- package/dist/{services/report.js → observability/services/reporting.js} +4 -3
- package/dist/testing/index.d.ts +3 -33
- package/dist/testing/index.js +5 -37
- package/package.json +16 -8
- package/dist/codegen/client.d.ts +0 -14
- package/dist/codegen/client.js +0 -201
- package/dist/codegen/domain.d.ts +0 -15
- package/dist/observability/ports.d.ts +0 -65
- package/dist/observability/ports.js +0 -21
- package/dist/services/bounded-context.d.ts +0 -79
- package/dist/services/bounded-context.js +0 -261
- package/dist/services/execution.d.ts +0 -63
- package/dist/services/feature.d.ts +0 -85
- package/dist/services/feature.js +0 -86
- package/dist/services/process.d.ts +0 -60
- package/dist/services/process.js +0 -57
- package/dist/services/screens.d.ts +0 -2
- package/dist/services/screens.js +0 -76
- /package/dist/{adapters → client/adapters}/parquet.js +0 -0
- /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
- /package/dist/client/{contract.js → domain/contract.js} +0 -0
- /package/dist/client/{entity.js → domain/entity.js} +0 -0
- /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
- /package/dist/client/{transport.js → domain/transport.js} +0 -0
- /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
- /package/dist/client/{discovery.js → services/discovery.js} +0 -0
- /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
- /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
- /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
- /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
- /package/dist/codegen/{check.js → services/check.js} +0 -0
- /package/dist/codegen/{rest.js → services/rest.js} +0 -0
- /package/dist/context/{context.js → entrypoint/context.js} +0 -0
- /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
- /package/dist/model/{meta.js → domain/meta.js} +0 -0
- /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
- /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
- /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
- /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DTOs of the application layer, as sincpro_framework's `DataTransferObject`: a Command asks
|
|
3
|
+
* for work and names, by its type, the Response it answers.
|
|
4
|
+
*
|
|
5
|
+
* class CommandCancelIssue extends Command<ResponseCancelIssue> {
|
|
6
|
+
* constructor(readonly issue_id: string) { super(); }
|
|
7
|
+
* }
|
|
8
|
+
* const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
|
|
9
|
+
*
|
|
10
|
+
* Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
|
|
11
|
+
* `execute` infers it with no second argument. The association is type-only: absent from a
|
|
12
|
+
* serialized DTO, not a runtime validator. A generated DTO is a remote contract (a type), not one
|
|
13
|
+
* of these: a local Command may implement it.
|
|
14
|
+
*/
|
|
15
|
+
export class DataTransferObject {
|
|
16
|
+
}
|
|
17
|
+
export class Response extends DataTransferObject {
|
|
18
|
+
}
|
|
19
|
+
export class Command extends DataTransferObject {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A fact that already happened, as sincpro_framework's `DomainEvent`: executed like a Command, by
|
|
23
|
+
* the Feature or ApplicationService registered for its class — its subscriber in this bounded
|
|
24
|
+
* context —, through the same pipeline. Delivering it (a queue, an outbox) is the platform's.
|
|
25
|
+
*
|
|
26
|
+
* class OrderPaid extends DomainEvent {
|
|
27
|
+
* static readonly eventName = "shop.order.v1.paid";
|
|
28
|
+
* constructor(readonly order_id: string) { super(); }
|
|
29
|
+
* }
|
|
30
|
+
* project.feature(OrderPaid)(NotifyCustomer);
|
|
31
|
+
* await project.execute(new OrderPaid("o-1"));
|
|
32
|
+
*
|
|
33
|
+
* Context: the backend names an event on the wire by `name`, `<context>.<aggregate>.vN.<fact>`,
|
|
34
|
+
* and defaults it to the class's name; a client is minified, so the name is declared, as
|
|
35
|
+
* `serviceName` is. `name` is reserved, as the backend reserves it: every event reads its class's
|
|
36
|
+
* `eventName` there, and a subclass cannot make it a field.
|
|
37
|
+
*/
|
|
38
|
+
export class DomainEvent extends DataTransferObject {
|
|
39
|
+
/** Its wire name: its class's `eventName`. */
|
|
40
|
+
get name() {
|
|
41
|
+
return this.constructor.eventName ?? "";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What answers a use case's failure instead of raising it — sincpro_framework's error handlers.
|
|
3
|
+
*
|
|
4
|
+
* project.addFeatureErrorHandler((error) => {
|
|
5
|
+
* if (error instanceof IssueNotFound) return new ResponseEmpty(); // the answer
|
|
6
|
+
* throw error; // the next handler
|
|
7
|
+
* });
|
|
8
|
+
*
|
|
9
|
+
* Context: three chains per bounded context — global (around `project.execute`), feature and
|
|
10
|
+
* application service —, each run first-registered-first. A handler that throws hands what it
|
|
11
|
+
* threw to the next; the last one's throw reaches the caller. The failure was already recorded
|
|
12
|
+
* where it happened (its span, its log line and, when unexpected, its report): a handler decides
|
|
13
|
+
* the answer, not whether it is observed. A handler that only watches must end with `throw`:
|
|
14
|
+
* what it returns is the answer.
|
|
15
|
+
*/
|
|
16
|
+
export type ErrorHandler = (error: unknown) => unknown;
|
|
17
|
+
/** An error's class, as `expectErrors` names it. */
|
|
18
|
+
export type ErrorClass = abstract new (...args: never[]) => unknown;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Execution } from "../../context/domain/execution.js";
|
|
2
|
+
import type { Executable } from "./dto.js";
|
|
3
|
+
import type { LayerLogger } from "../../observability/domain/logging.js";
|
|
4
|
+
import type { Outcome } from "../../observability/domain/tracing.js";
|
|
5
|
+
/**
|
|
6
|
+
* Where the execution pipeline is extended — as sincpro_framework's `bus_pipeline`, in two kinds:
|
|
7
|
+
*
|
|
8
|
+
* control interceptors wrap the service: look, veto, adjust or answer
|
|
9
|
+
* observation listeners a run started, answered, or a failure escaped: heard, never changed
|
|
10
|
+
*
|
|
11
|
+
* project.interceptor(CommandPay)(async (command, next, run) => {
|
|
12
|
+
* if (!credit.allows(command.customer_id)) throw new NoCredit(); // veto
|
|
13
|
+
* return next(command); // the next, or the service
|
|
14
|
+
* });
|
|
15
|
+
* project.onCompletion((done) => activity.succeeded(done.execution.useCase));
|
|
16
|
+
*
|
|
17
|
+
* Context: the core owns the flow — context, span, log lines, outcome, report, handlers — and
|
|
18
|
+
* these are the places an application changes it from outside, without touching a service. An
|
|
19
|
+
* interceptor runs inside the run: its veto is the use case's failure, observed and answered by
|
|
20
|
+
* the error handlers like any other. A listener that fails is ignored: hearing never changes
|
|
21
|
+
* the answer.
|
|
22
|
+
*/
|
|
23
|
+
export type PipelineLayer = "feature" | "application_service";
|
|
24
|
+
/** What caused a run from outside: an event delivered with its envelope continues its chain. */
|
|
25
|
+
export type Cause = {
|
|
26
|
+
/** The id of what caused it — a delivered event's. */
|
|
27
|
+
readonly id: string;
|
|
28
|
+
/** The chain it continues. */
|
|
29
|
+
readonly correlationId: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* How a Command or an event is executed: what cancels it, and — for a delivered event — what
|
|
33
|
+
* caused it.
|
|
34
|
+
*
|
|
35
|
+
* await sales.execute(new CommandQuote(3), { signal: screen.signal });
|
|
36
|
+
*
|
|
37
|
+
* Context: a screen that closes, a navigation that leaves, a user who gives up stop waiting; the
|
|
38
|
+
* run ends `aborted` (not reported), its Features inherit the signal, and the requests its
|
|
39
|
+
* transport sends are cancelled with it.
|
|
40
|
+
*/
|
|
41
|
+
export type ExecuteOptions = {
|
|
42
|
+
readonly signal?: AbortSignal;
|
|
43
|
+
readonly cause?: Cause;
|
|
44
|
+
};
|
|
45
|
+
/** The run an interceptor wraps: who runs, inside which execution, writing where. */
|
|
46
|
+
export type InterceptedRun = {
|
|
47
|
+
readonly boundedContext: string;
|
|
48
|
+
/** `<bounded context>.<serviceName>`. */
|
|
49
|
+
readonly useCase: string;
|
|
50
|
+
readonly layer: PipelineLayer;
|
|
51
|
+
/** Its id, span, context and chain. */
|
|
52
|
+
readonly execution: Execution;
|
|
53
|
+
/** The run's logger, bound to it. */
|
|
54
|
+
readonly logger: LayerLogger;
|
|
55
|
+
};
|
|
56
|
+
/** The rest of the chain: the next interceptor, or the service. */
|
|
57
|
+
export type CallNext<T extends Executable> = (executable: T) => Promise<unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* Runs around the service of a Command or an event. It passes on what it received — or another
|
|
60
|
+
* of the same class: the bus routes by class, so another class would be another use case — and
|
|
61
|
+
* returns what the next answered, or an answer of its own; it throws to veto.
|
|
62
|
+
*/
|
|
63
|
+
export type Interceptor<T extends Executable = Executable> = (executable: T, next: CallNext<T>, run: InterceptedRun) => unknown;
|
|
64
|
+
/** A run began. */
|
|
65
|
+
export type ExecutionStarted = {
|
|
66
|
+
readonly executable: Executable;
|
|
67
|
+
readonly execution: Execution;
|
|
68
|
+
readonly layer: PipelineLayer;
|
|
69
|
+
};
|
|
70
|
+
/** A run's service answered. */
|
|
71
|
+
export type ExecutionCompleted = ExecutionStarted & {
|
|
72
|
+
readonly answer: unknown;
|
|
73
|
+
readonly durationMs: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* A failure escaped a call — after every handler —, said once, where it first failed. No
|
|
77
|
+
* execution when it reached none: nothing is registered for the class.
|
|
78
|
+
*/
|
|
79
|
+
export type ExecutionFailed = {
|
|
80
|
+
readonly executable: Executable;
|
|
81
|
+
readonly execution: Execution | undefined;
|
|
82
|
+
readonly layer: PipelineLayer | undefined;
|
|
83
|
+
readonly error: unknown;
|
|
84
|
+
readonly outcome: Outcome;
|
|
85
|
+
};
|
|
86
|
+
/** Who hears a moment of the pipeline. */
|
|
87
|
+
export type Listener<E> = (heard: E) => void;
|
|
88
|
+
/** A component wired into a bounded context: its interceptors, listeners, services. */
|
|
89
|
+
export type Extension<B> = (context: B) => void;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Execution } from "../../context/domain/execution.js";
|
|
2
|
+
import type { ContextValues } from "../../context/domain/store.js";
|
|
3
|
+
import type { FeatureBus } from "./bus.js";
|
|
4
|
+
import type { Command, DomainEvent } from "./dto.js";
|
|
5
|
+
import type { LayerLogger } from "../../observability/domain/logging.js";
|
|
6
|
+
import type { Span } from "../../observability/domain/tracing.js";
|
|
7
|
+
/**
|
|
8
|
+
* The services of the application layer, as sincpro_framework's: a Feature executes one
|
|
9
|
+
* registered Command — or domain event, as its subscriber —; an ApplicationService composes its
|
|
10
|
+
* context's Features.
|
|
11
|
+
*
|
|
12
|
+
* @project.feature(CommandCancelIssue)
|
|
13
|
+
* class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
|
|
14
|
+
* static readonly serviceName = "CancelIssue";
|
|
15
|
+
* async execute(command: CommandCancelIssue) {
|
|
16
|
+
* this.logger.info("Cancelling"); // bound to this run
|
|
17
|
+
* return new ResponseCancelIssue(await this.client.cancelIssue(command));
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* Context: while a method runs, `this.context`, `this.span`, `this.logger`, `this.featureBus` and
|
|
22
|
+
* every dependency are that run's (`executeService`). `serviceName` is stable metadata, never
|
|
23
|
+
* `constructor.name`, which a minifier renames.
|
|
24
|
+
*/
|
|
25
|
+
export declare abstract class Feature<C extends Command<R> | DomainEvent, R, Context extends object = ContextValues> {
|
|
26
|
+
/** What this run reads: the process's context, its bounded context's, its scope's. */
|
|
27
|
+
readonly context: Readonly<Partial<Context>>;
|
|
28
|
+
/** This run's span: `this.span.set({...})` says more about the use case. */
|
|
29
|
+
readonly span: Span;
|
|
30
|
+
/** This run's logger: every line carries its use case, execution and trace. */
|
|
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 subscriber of a delivered
|
|
36
|
+
* event finds the envelope's id as `causationId` — the key that makes executing it twice
|
|
37
|
+
* harmless.
|
|
38
|
+
*/
|
|
39
|
+
readonly execution: Execution;
|
|
40
|
+
abstract execute(command: C): R | Promise<R>;
|
|
41
|
+
}
|
|
42
|
+
export declare abstract class ApplicationService<C extends Command<R> | DomainEvent, R, Context extends object = ContextValues> extends Feature<C, R, Context> {
|
|
43
|
+
/** The context's Features, run as children of this execution. */
|
|
44
|
+
protected readonly featureBus: FeatureBus;
|
|
45
|
+
}
|
|
46
|
+
/** @internal — what a run finds on `this` that no dependency may take. */
|
|
47
|
+
export declare const RESERVED_MEMBERS: ReadonlySet<string>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The services of the application layer, as sincpro_framework's: a Feature executes one
|
|
3
|
+
* registered Command — or domain event, as its subscriber —; an ApplicationService composes its
|
|
4
|
+
* context's Features.
|
|
5
|
+
*
|
|
6
|
+
* @project.feature(CommandCancelIssue)
|
|
7
|
+
* class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
|
|
8
|
+
* static readonly serviceName = "CancelIssue";
|
|
9
|
+
* async execute(command: CommandCancelIssue) {
|
|
10
|
+
* this.logger.info("Cancelling"); // bound to this run
|
|
11
|
+
* return new ResponseCancelIssue(await this.client.cancelIssue(command));
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* Context: while a method runs, `this.context`, `this.span`, `this.logger`, `this.featureBus` and
|
|
16
|
+
* every dependency are that run's (`executeService`). `serviceName` is stable metadata, never
|
|
17
|
+
* `constructor.name`, which a minifier renames.
|
|
18
|
+
*/
|
|
19
|
+
export class Feature {
|
|
20
|
+
}
|
|
21
|
+
export class ApplicationService extends Feature {
|
|
22
|
+
}
|
|
23
|
+
/** @internal — what a run finds on `this` that no dependency may take. */
|
|
24
|
+
export const RESERVED_MEMBERS = new Set([
|
|
25
|
+
"featureBus",
|
|
26
|
+
"context",
|
|
27
|
+
"span",
|
|
28
|
+
"logger",
|
|
29
|
+
"signal",
|
|
30
|
+
"execution",
|
|
31
|
+
]);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { ContextValues } from "../../context/domain/store.js";
|
|
2
|
+
import { type Context } from "../../context/entrypoint/context.js";
|
|
3
|
+
import { type CommandBus } from "../domain/bus.js";
|
|
4
|
+
import { type Answer, type AnyCommand, Command, type CommandClass, DomainEvent, type EventClass, type Executable, type ResponseClass } from "../domain/dto.js";
|
|
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";
|
|
8
|
+
import type { LayerLogger } from "../../observability/domain/logging.js";
|
|
9
|
+
interface Executor<C extends Executable, R> {
|
|
10
|
+
execute(executable: C): R | Promise<R>;
|
|
11
|
+
}
|
|
12
|
+
type ServiceClass<C extends Executable, R> = {
|
|
13
|
+
new (): Executor<C, R>;
|
|
14
|
+
readonly serviceName: string;
|
|
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;
|
|
24
|
+
/**
|
|
25
|
+
* A bounded context: the bus of its Commands and domain events, as sincpro_framework's
|
|
26
|
+
* `UseFramework`.
|
|
27
|
+
*
|
|
28
|
+
* const project = new BoundedContext<ProjectDependencies>("project");
|
|
29
|
+
* project.addDependency("client", clientOf(transport));
|
|
30
|
+
* project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
|
|
31
|
+
* project.feature(IssueClosed)(NotifyWatchers); // a subscriber of an event
|
|
32
|
+
* project.feature(CommandCancelIssue, { replaces: CancelIssue })(CancelAndArchive);
|
|
33
|
+
* project.addFeatureErrorHandler(toEmptyWhenMissing);
|
|
34
|
+
* project.expectErrors(IssueLocked); // refused, never reported
|
|
35
|
+
* await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
|
|
36
|
+
* await project.execute(new IssueClosed("x")); // what its subscriber returns
|
|
37
|
+
* project.logger.info("Catalog preloaded");
|
|
38
|
+
*
|
|
39
|
+
* Context: registrations, dependencies, handlers and expected errors are snapshotted per
|
|
40
|
+
* generation; a call keeps the generation it started with, and what is added later answers from
|
|
41
|
+
* the next call — as the backend's `build_root_bus`. A domain event is executed as a Command is:
|
|
42
|
+
* one subscriber per bounded context, through the same pipeline; carrying it to every bounded
|
|
43
|
+
* context that hears it (a queue, an outbox) is the platform's.
|
|
44
|
+
*/
|
|
45
|
+
export declare class BoundedContext<D extends object = Record<never, never>, C extends object = ContextValues> {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
/** Its level of the context: what every use case of it runs with, under the process's. */
|
|
48
|
+
readonly context: Context<C>;
|
|
49
|
+
/** Its own logger, named after it: for what it does outside a use case. */
|
|
50
|
+
readonly logger: LayerLogger;
|
|
51
|
+
private readonly registrations;
|
|
52
|
+
private readonly dependencyValues;
|
|
53
|
+
private readonly handlers;
|
|
54
|
+
private readonly expected;
|
|
55
|
+
private readonly interceptors;
|
|
56
|
+
private readonly listeners;
|
|
57
|
+
private revision;
|
|
58
|
+
private builtRevision;
|
|
59
|
+
private built;
|
|
60
|
+
constructor(name: string);
|
|
61
|
+
get deps(): Readonly<D>;
|
|
62
|
+
addDependency<K extends keyof D & string>(name: K, dependency: D[K], options?: {
|
|
63
|
+
replaces: D[K];
|
|
64
|
+
}): this;
|
|
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;
|
|
102
|
+
/** Answers a failure that reaches `execute` (or a scope's): after every layer's handlers. */
|
|
103
|
+
addGlobalErrorHandler(handler: ErrorHandler): this;
|
|
104
|
+
/** Answers a Feature's failure, wherever it was executed from. */
|
|
105
|
+
addFeatureErrorHandler(handler: ErrorHandler): this;
|
|
106
|
+
/** Answers an ApplicationService's failure. */
|
|
107
|
+
addAppServiceErrorHandler(handler: ErrorHandler): this;
|
|
108
|
+
/**
|
|
109
|
+
* Errors of these classes are the business answering, not a fault: the run ends `refused`,
|
|
110
|
+
* its span is no error and nothing is reported. They are still thrown — or handled.
|
|
111
|
+
*/
|
|
112
|
+
expectErrors(...types: ErrorClass[]): this;
|
|
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>;
|
|
121
|
+
scoped(context: Partial<C>): CommandBus;
|
|
122
|
+
buildRootBus(): void;
|
|
123
|
+
fresh(): BoundedContext<D, C>;
|
|
124
|
+
/** What escaped a call, told once, from where it first failed. */
|
|
125
|
+
private escaped;
|
|
126
|
+
private addErrorHandler;
|
|
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
|
+
*/
|
|
135
|
+
private registerService;
|
|
136
|
+
private generation;
|
|
137
|
+
private buildGeneration;
|
|
138
|
+
}
|
|
139
|
+
export {};
|