@sincpro/client-framework 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/AGENTS.md +71 -13
  2. package/MANIFESTO.md +59 -14
  3. package/README.md +456 -55
  4. package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
  5. package/dist/circuit_breaker/domain/circuit.js +37 -0
  6. package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
  7. package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
  8. package/dist/circuit_breaker/index.d.ts +9 -0
  9. package/dist/circuit_breaker/index.js +9 -0
  10. package/dist/client/adapters/json-rpc.d.ts +40 -0
  11. package/dist/client/adapters/json-rpc.js +97 -0
  12. package/dist/client/adapters/rest-operations.d.ts +19 -0
  13. package/dist/client/adapters/rest-operations.js +66 -0
  14. package/dist/client/domain/operation.d.ts +73 -0
  15. package/dist/client/domain/operation.js +19 -0
  16. package/dist/client/index.d.ts +5 -1
  17. package/dist/client/index.js +5 -1
  18. package/dist/client/services/guarded.d.ts +17 -0
  19. package/dist/client/services/guarded.js +19 -0
  20. package/dist/client/services/propagation.d.ts +20 -7
  21. package/dist/client/services/propagation.js +44 -15
  22. package/dist/codegen/entrypoint/cli.js +7 -7
  23. package/dist/codegen/services/client.d.ts +7 -8
  24. package/dist/codegen/services/client.js +50 -148
  25. package/dist/codegen/services/scaffold.d.ts +7 -4
  26. package/dist/codegen/services/scaffold.js +23 -27
  27. package/dist/common/failures.d.ts +83 -0
  28. package/dist/common/failures.js +212 -0
  29. package/dist/common/index.d.ts +1 -0
  30. package/dist/common/index.js +1 -0
  31. package/dist/context/domain/execution.d.ts +12 -0
  32. package/dist/context/entrypoint/context.d.ts +5 -4
  33. package/dist/core/domain/bus.d.ts +16 -7
  34. package/dist/core/domain/bus.js +14 -6
  35. package/dist/core/domain/dto.d.ts +33 -3
  36. package/dist/core/domain/dto.js +24 -1
  37. package/dist/core/domain/errors.d.ts +2 -2
  38. package/dist/core/domain/pipeline.d.ts +89 -0
  39. package/dist/core/domain/pipeline.js +1 -0
  40. package/dist/core/domain/services.d.ts +16 -5
  41. package/dist/core/domain/services.js +5 -2
  42. package/dist/core/entrypoint/bounded-context.d.ts +96 -16
  43. package/dist/core/entrypoint/bounded-context.js +244 -39
  44. package/dist/core/index.d.ts +4 -2
  45. package/dist/core/index.js +3 -2
  46. package/dist/core/infrastructure/listeners.d.ts +28 -0
  47. package/dist/core/infrastructure/listeners.js +34 -0
  48. package/dist/core/services/binding.d.ts +2 -2
  49. package/dist/core/services/binding.js +6 -3
  50. package/dist/core/services/execution.d.ts +59 -12
  51. package/dist/core/services/execution.js +114 -19
  52. package/dist/core/services/interceptors.d.ts +15 -0
  53. package/dist/core/services/interceptors.js +25 -0
  54. package/dist/core/services/listening.d.ts +17 -0
  55. package/dist/core/services/listening.js +25 -0
  56. package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
  57. package/dist/event_driven/adapters/memory-event-store.js +23 -0
  58. package/dist/event_driven/adapters/stored-queue.d.ts +68 -0
  59. package/dist/event_driven/adapters/stored-queue.js +193 -0
  60. package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
  61. package/dist/event_driven/adapters/sync-queue.js +19 -0
  62. package/dist/event_driven/domain/envelope.d.ts +28 -0
  63. package/dist/event_driven/domain/envelope.js +19 -0
  64. package/dist/event_driven/domain/queue.d.ts +40 -0
  65. package/dist/event_driven/domain/queue.js +1 -0
  66. package/dist/event_driven/domain/store.d.ts +56 -0
  67. package/dist/event_driven/domain/store.js +1 -0
  68. package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
  69. package/dist/event_driven/entrypoint/publisher.js +35 -0
  70. package/dist/event_driven/entrypoint/subscriber.d.ts +42 -0
  71. package/dist/event_driven/entrypoint/subscriber.js +62 -0
  72. package/dist/event_driven/entrypoint/subscriptions.d.ts +110 -0
  73. package/dist/event_driven/entrypoint/subscriptions.js +184 -0
  74. package/dist/event_driven/index.d.ts +22 -0
  75. package/dist/event_driven/index.js +20 -0
  76. package/dist/index.d.ts +7 -2
  77. package/dist/index.js +7 -2
  78. package/dist/observability/adapters/memory.d.ts +1 -1
  79. package/dist/observability/domain/logging.d.ts +10 -4
  80. package/dist/observability/services/logging.d.ts +7 -4
  81. package/dist/observability/services/logging.js +3 -2
  82. package/dist/observability/services/outcome.d.ts +6 -6
  83. package/dist/observability/services/outcome.js +10 -13
  84. package/package.json +1 -1
@@ -0,0 +1,83 @@
1
+ import type { FailureKind, NextStep, Problem } from "@sincpro/criteria";
2
+ /**
3
+ * What kind of failure an error is, whatever raised it — the backend's `common/failures`, read
4
+ * on a client: a refusal the server explained, a request nobody answered, a run a screen
5
+ * cancelled, a local domain's own refusal, another backend's error a platform translated.
6
+ *
7
+ * failureOf(new TransportError("/pay", 409, problem)) -> { kind: "conflict", nextStep: "refresh_and_retry", … }
8
+ * failureOf(new TransportError("/pay", null, null)) -> { kind: "unknown_outcome", fault: true, … }
9
+ * failureOf(new CreditExhausted()) -> what the class declares (`failureKind`)
10
+ * failureOf(new TypeError("x")) -> { kind: "internal", fault: true, … }
11
+ *
12
+ * Context: every client decides the same things about a failure — show it or report it, retry it
13
+ * or set it aside, ask to sign in again —, and each app was deciding them its own way. The words
14
+ * are the wire's (`FailureKind`, `NextStep`, `Problem` from @sincpro/criteria), the rules the
15
+ * backend's: a retryable kind is retried after `retryAfter`; a permanent one fails the same way on
16
+ * every attempt and is set aside; a fault is the system's problem and is reported. An error says
17
+ * its own kind (`failureKind`, as the backend's errors do), so nothing here knows who raised it:
18
+ * a platform's adapter translates its backend's errors into errors that declare one.
19
+ */
20
+ export type Failure = {
21
+ readonly kind: FailureKind;
22
+ /** UPPER_SNAKE and stable: what a client switches on. */
23
+ readonly reason: string;
24
+ /** What the caller does next. */
25
+ readonly nextStep: NextStep;
26
+ /** Whether sending the same thing again — as it is, or after a fresh read — may succeed. */
27
+ readonly retryable: boolean;
28
+ /** Seconds to wait before retrying; none when retrying does not help. */
29
+ readonly retryAfter: number | undefined;
30
+ /** It fails the same way on every attempt: a queue sets it aside, never retries it. */
31
+ readonly permanent: boolean;
32
+ /** The system's problem — a bug, a server down, no answer —, not an answer to the caller. */
33
+ readonly fault: boolean;
34
+ /** Whoever asked stopped waiting: not a failure of anyone. */
35
+ readonly aborted: boolean;
36
+ /** What the server said, when it said it. */
37
+ readonly problem: Problem | undefined;
38
+ };
39
+ /**
40
+ * What an error can say about itself — on the instance or as a static of its class:
41
+ *
42
+ * class CreditExhausted extends Error {
43
+ * static readonly failureKind = "exhausted";
44
+ * readonly retryAfter = 30;
45
+ * }
46
+ */
47
+ export type DeclaredFailure = {
48
+ readonly failureKind?: FailureKind;
49
+ readonly reason?: string;
50
+ readonly retryAfter?: number;
51
+ };
52
+ /** The kinds that fail the same way on every attempt — set aside at the first failure. */
53
+ export declare const PERMANENT_KINDS: ReadonlySet<FailureKind>;
54
+ /**
55
+ * The kind each wire's own code stands for, when an answer carries no `Problem` — the backend's
56
+ * tables, read back, so a transport of any wire (REST, JSON-RPC, gRPC, another backend's) builds
57
+ * errors that declare the same kinds:
58
+ *
59
+ * class RpcError extends Error {
60
+ * constructor(readonly code: number, message: string) { super(message); }
61
+ * get failureKind() { return kindOfJsonRpcCode(this.code); }
62
+ * }
63
+ */
64
+ export declare function kindOfHttpStatus(status: number): FailureKind;
65
+ /** The kind a gRPC status code stands for — the backend's gRPC table, read back. */
66
+ export declare function kindOfGrpcCode(code: number): FailureKind;
67
+ /** The kind a JSON-RPC error code stands for — the backend's catalogue, read back. */
68
+ export declare function kindOfJsonRpcCode(code: number): FailureKind;
69
+ /**
70
+ * The failure `error` is.
71
+ *
72
+ * 1. Whoever asked stopped waiting (`AbortError`): aborted — a write may have run, so its kind is
73
+ * `unknown_outcome`, but it is nobody's failure.
74
+ * 2. What the error declares (`failureKind`, `reason`, `retryAfter`), on itself or its class.
75
+ * 3. What the server said (`problem`, a `Problem` — the same over REST, JSON-RPC and gRPC): its
76
+ * kind, reason, next step and wait.
77
+ * 4. A REST answer (`TransportError`) with a status and no problem: the kind its HTTP status
78
+ * stands for. Another wire's transport declares its kind instead (`kindOfGrpcCode`,
79
+ * `kindOfJsonRpcCode`).
80
+ * 5. A request that got no answer (`status: null`): `unknown_outcome` — it may have run.
81
+ * Final: anything else is `internal`, a fault.
82
+ */
83
+ export declare function failureOf(error: unknown): Failure;
@@ -0,0 +1,212 @@
1
+ /** The kinds that fail the same way on every attempt — set aside at the first failure. */
2
+ export const PERMANENT_KINDS = new Set([
3
+ "invalid",
4
+ "unauthenticated",
5
+ "permission_denied",
6
+ "not_found",
7
+ "key_reused",
8
+ "domain",
9
+ ]);
10
+ /** What the caller does next, by kind — the backend's `NEXT_STEP_OF`. */
11
+ const NEXT_STEP_OF = {
12
+ invalid: "fix_request",
13
+ unauthenticated: "authenticate",
14
+ permission_denied: "ask_for_access",
15
+ not_found: "do_not_retry",
16
+ conflict: "refresh_and_retry",
17
+ in_progress: "retry_later",
18
+ key_reused: "fix_request",
19
+ domain: "do_not_retry",
20
+ exhausted: "retry_later",
21
+ unavailable: "retry_later",
22
+ unknown_outcome: "verify_then_retry",
23
+ internal: "retry_later",
24
+ };
25
+ /** The kinds that are the system's problem: reported, never shown as the caller's answer. */
26
+ const FAULTS = new Set([
27
+ "internal",
28
+ "unavailable",
29
+ "unknown_outcome",
30
+ ]);
31
+ const RETRYABLE_STEPS = new Set([
32
+ "retry_later",
33
+ "refresh_and_retry",
34
+ ]);
35
+ /** A retryable kind that says nothing waits a second, as the backend's `DEFAULT_RETRY_AFTER`. */
36
+ const DEFAULT_RETRY_AFTER = 1;
37
+ const KINDS = new Set(Object.keys(NEXT_STEP_OF));
38
+ function isKind(value) {
39
+ return typeof value === "string" && KINDS.has(value);
40
+ }
41
+ /**
42
+ * The kind each wire's own code stands for, when an answer carries no `Problem` — the backend's
43
+ * tables, read back, so a transport of any wire (REST, JSON-RPC, gRPC, another backend's) builds
44
+ * errors that declare the same kinds:
45
+ *
46
+ * class RpcError extends Error {
47
+ * constructor(readonly code: number, message: string) { super(message); }
48
+ * get failureKind() { return kindOfJsonRpcCode(this.code); }
49
+ * }
50
+ */
51
+ export function kindOfHttpStatus(status) {
52
+ if (status === 401)
53
+ return "unauthenticated";
54
+ if (status === 403)
55
+ return "permission_denied";
56
+ if (status === 404 || status === 410)
57
+ return "not_found";
58
+ if (status === 409 || status === 412)
59
+ return "conflict";
60
+ if (status === 429)
61
+ return "exhausted";
62
+ if (status === 502 || status === 503)
63
+ return "unavailable";
64
+ if (status === 504)
65
+ return "unknown_outcome";
66
+ if (status >= 500)
67
+ return "internal";
68
+ return "invalid";
69
+ }
70
+ /** The kind a gRPC status code stands for — the backend's gRPC table, read back. */
71
+ export function kindOfGrpcCode(code) {
72
+ switch (code) {
73
+ case 3: // INVALID_ARGUMENT
74
+ case 11: // OUT_OF_RANGE
75
+ return "invalid";
76
+ case 16: // UNAUTHENTICATED
77
+ return "unauthenticated";
78
+ case 7: // PERMISSION_DENIED
79
+ return "permission_denied";
80
+ case 5: // NOT_FOUND
81
+ case 12: // UNIMPLEMENTED
82
+ return "not_found";
83
+ case 6: // ALREADY_EXISTS
84
+ case 10: // ABORTED — also an idempotent run in flight
85
+ return "conflict";
86
+ case 9: // FAILED_PRECONDITION — also a reused idempotency key
87
+ return "domain";
88
+ case 8: // RESOURCE_EXHAUSTED
89
+ return "exhausted";
90
+ case 14: // UNAVAILABLE
91
+ return "unavailable";
92
+ case 1: // CANCELLED
93
+ case 4: // DEADLINE_EXCEEDED
94
+ return "unknown_outcome";
95
+ default: // UNKNOWN, INTERNAL, DATA_LOSS
96
+ return "internal";
97
+ }
98
+ }
99
+ /** The kind a JSON-RPC error code stands for — the backend's catalogue, read back. */
100
+ export function kindOfJsonRpcCode(code) {
101
+ switch (code) {
102
+ case -32700:
103
+ case -32600:
104
+ case -32602:
105
+ return "invalid";
106
+ case -32601:
107
+ case -32004:
108
+ return "not_found";
109
+ case -32000:
110
+ return "unavailable";
111
+ case -32001:
112
+ return "unauthenticated";
113
+ case -32003:
114
+ return "permission_denied";
115
+ case -32009:
116
+ return "conflict";
117
+ case -32010:
118
+ return "domain";
119
+ case -32022:
120
+ return "key_reused";
121
+ case -32024:
122
+ return "unknown_outcome";
123
+ case -32029: // an idempotent run in flight, or a limit spent — the problem says which
124
+ return "exhausted";
125
+ default:
126
+ return "internal";
127
+ }
128
+ }
129
+ function upperSnake(kind) {
130
+ return kind.toUpperCase();
131
+ }
132
+ /** What the error declares: on itself first, then on its class. */
133
+ function declared(error) {
134
+ const own = error;
135
+ const type = (error.constructor ?? {});
136
+ return {
137
+ ...(isKind(own.failureKind ?? type.failureKind)
138
+ ? { failureKind: own.failureKind ?? type.failureKind }
139
+ : {}),
140
+ ...(typeof (own.reason ?? type.reason) === "string"
141
+ ? { reason: own.reason ?? type.reason }
142
+ : {}),
143
+ ...(typeof (own.retryAfter ?? type.retryAfter) === "number"
144
+ ? { retryAfter: own.retryAfter ?? type.retryAfter }
145
+ : {}),
146
+ };
147
+ }
148
+ function isProblem(value) {
149
+ return (value !== null &&
150
+ typeof value === "object" &&
151
+ isKind(value.kind) &&
152
+ typeof value.next_step === "string");
153
+ }
154
+ function failure(kind, said, aborted = false) {
155
+ const nextStep = said.nextStep ?? NEXT_STEP_OF[kind];
156
+ const retryable = RETRYABLE_STEPS.has(nextStep);
157
+ return {
158
+ kind,
159
+ reason: said.reason ?? upperSnake(kind),
160
+ nextStep,
161
+ retryable,
162
+ retryAfter: retryable ? (said.retryAfter ?? DEFAULT_RETRY_AFTER) : undefined,
163
+ permanent: !aborted && PERMANENT_KINDS.has(kind),
164
+ fault: !aborted && FAULTS.has(kind),
165
+ aborted,
166
+ problem: said.problem,
167
+ };
168
+ }
169
+ /**
170
+ * The failure `error` is.
171
+ *
172
+ * 1. Whoever asked stopped waiting (`AbortError`): aborted — a write may have run, so its kind is
173
+ * `unknown_outcome`, but it is nobody's failure.
174
+ * 2. What the error declares (`failureKind`, `reason`, `retryAfter`), on itself or its class.
175
+ * 3. What the server said (`problem`, a `Problem` — the same over REST, JSON-RPC and gRPC): its
176
+ * kind, reason, next step and wait.
177
+ * 4. A REST answer (`TransportError`) with a status and no problem: the kind its HTTP status
178
+ * stands for. Another wire's transport declares its kind instead (`kindOfGrpcCode`,
179
+ * `kindOfJsonRpcCode`).
180
+ * 5. A request that got no answer (`status: null`): `unknown_outcome` — it may have run.
181
+ * Final: anything else is `internal`, a fault.
182
+ */
183
+ export function failureOf(error) {
184
+ if (error === null || typeof error !== "object")
185
+ return failure("internal", {});
186
+ const named = error.name;
187
+ if (named === "AbortError")
188
+ return failure("unknown_outcome", {}, true);
189
+ const own = declared(error);
190
+ if (own.failureKind !== undefined)
191
+ return failure(own.failureKind, {
192
+ ...(own.reason !== undefined ? { reason: own.reason } : {}),
193
+ ...(own.retryAfter !== undefined ? { retryAfter: own.retryAfter } : {}),
194
+ });
195
+ const answered = error;
196
+ if (isProblem(answered.problem)) {
197
+ const problem = answered.problem;
198
+ return failure(problem.kind, {
199
+ reason: problem.reason,
200
+ nextStep: problem.next_step,
201
+ ...(problem.retry_after !== undefined ? { retryAfter: problem.retry_after } : {}),
202
+ problem,
203
+ });
204
+ }
205
+ if ("problem" in answered && "status" in answered) {
206
+ if (typeof answered.status === "number")
207
+ return failure(kindOfHttpStatus(answered.status), {});
208
+ if (answered.status === null)
209
+ return failure("unknown_outcome", {});
210
+ }
211
+ return failure("internal", {});
212
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export { collectionOf, type EntityCollection, type HeldCollection, isCollection, isPartial, relatedCriteria, type RelationKeys, totalOf, } from "./collection.js";
6
6
  export { Decimal, type DecimalLike } from "./decimal.js";
7
+ export { type DeclaredFailure, type Failure, failureOf, kindOfGrpcCode, kindOfHttpStatus, kindOfJsonRpcCode, PERMANENT_KINDS, } from "./failures.js";
7
8
  export { isEntityId, momentOf, newEntityId } from "./ids.js";
8
9
  export { Money } from "./money.js";
9
10
  export { BYTES, type JsonValue, pack, readValues, type Rebuild, rebuilt, unpack, valuesOf, } from "./serialization.js";
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export { collectionOf, isCollection, isPartial, relatedCriteria, totalOf, } from "./collection.js";
6
6
  export { Decimal } from "./decimal.js";
7
+ export { failureOf, kindOfGrpcCode, kindOfHttpStatus, kindOfJsonRpcCode, PERMANENT_KINDS, } from "./failures.js";
7
8
  export { isEntityId, momentOf, newEntityId } from "./ids.js";
8
9
  export { Money } from "./money.js";
9
10
  export { BYTES, pack, readValues, rebuilt, unpack, valuesOf, } from "./serialization.js";
@@ -17,6 +17,18 @@ export type Execution = {
17
17
  readonly boundedContext: string;
18
18
  /** The run that executed this one: an ApplicationService, for its Features. */
19
19
  readonly parent: Execution | undefined;
20
+ /**
21
+ * The chain it belongs to: the id of what started it all — the root run, or the first event
22
+ * of a flow it continues. Every run and event of one flow share it.
23
+ */
24
+ readonly correlationId: string;
25
+ /** What directly caused it: its parent run, or the event it executes; none for a root. */
26
+ readonly causationId: string | undefined;
27
+ /**
28
+ * What cancels it: the signal it was executed with, inherited by its Features — and by every
29
+ * request its bound transport sends.
30
+ */
31
+ readonly signal: AbortSignal | undefined;
20
32
  readonly span: Span;
21
33
  /** What it runs with, read once where the gesture started and inherited by every child. */
22
34
  readonly context: Readonly<Record<string, unknown>>;
@@ -1,18 +1,19 @@
1
1
  import type { ContextStore, ContextValues } from "../domain/store.js";
2
2
  /**
3
3
  * One level of the context: the process's (`processContext`) or a bounded context's
4
- * (`project.context`), read and written the same way on every platform.
4
+ * (`projectBoundedContext.context`), read and written the same way on every platform.
5
5
  *
6
6
  * processContext.set({ tenant_id: "acme" }); the company changed — every screen hears it
7
7
  * processContext.get("tenant_id") → "acme"
8
- * project.context.set({ warehouse_id: 4 }); only what `project` runs
8
+ * projectBoundedContext.context.set({ warehouse_id: 4 }); only what it runs
9
9
  * const stop = processContext.watch(() => redraw());
10
10
  *
11
11
  * Context: sincpro_framework reads its context from a tree — the process, the bounded context,
12
12
  * the scope, the execution — and the nearest wins. A client has the same levels: a use case runs
13
13
  * with the process's values, under them its bounded context's, under them a scope's
14
- * (`project.scoped({...})`), read once where the gesture starts and inherited by every Feature
15
- * it runs. That snapshot is `this.context` and what travels to the server in `sincpro-context`.
14
+ * (`projectBoundedContext.scoped({...})`), read once where the gesture starts and inherited by
15
+ * every Feature it runs. That snapshot is `this.context` and what travels to the server in
16
+ * `sincpro-context`.
16
17
  *
17
18
  * Where the values live is the platform's (`ContextStore`): this is only the door. `values` is
18
19
  * the same object until something changes it, so a screen can hand it straight to React's
@@ -1,12 +1,19 @@
1
- import type { AnyCommand, Command, ResponseClass } from "./dto.js";
2
- /** What executes a Command: the bounded context, a scope of it, an ApplicationService's features. */
1
+ import type { Command, DomainEvent, Executable, ResponseClass } from "./dto.js";
2
+ import type { Cause, ExecuteOptions } from "./pipeline.js";
3
+ /** What the second argument of `execute` is: options, a delivered event's cause, or a type. */
4
+ export declare function optionsOf(second: unknown): ExecuteOptions;
5
+ /**
6
+ * What executes a Command or a domain event: the bounded context, a scope of it, an
7
+ * ApplicationService's features. A Command answers its Response; an event, whatever its
8
+ * subscriber returns.
9
+ */
3
10
  export interface CommandBus {
4
11
  execute<R>(command: Command<R>, returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
12
+ execute<R>(command: Command<R>, options: ExecuteOptions): Promise<R>;
13
+ execute(event: DomainEvent, options?: ExecuteOptions | Cause): Promise<unknown>;
5
14
  }
6
- /** @internal — how a bus hands a Command to its generation. */
7
- export type Dispatch = (command: AnyCommand) => Promise<unknown>;
8
- /** @internal — the heterogeneous registry erases the type; registration checks the association. */
9
- export declare function executeTyped<R>(dispatch: Dispatch, command: Command<R>): Promise<R>;
15
+ /** @internal — how a bus hands what it executes to its generation. */
16
+ export type Dispatch = (executable: Executable, options: ExecuteOptions) => Promise<unknown>;
10
17
  /**
11
18
  * What an ApplicationService composes its context's Features through (`this.featureBus`): the
12
19
  * same pipeline, as a child of the running execution, and Features only.
@@ -14,5 +21,7 @@ export declare function executeTyped<R>(dispatch: Dispatch, command: Command<R>)
14
21
  export declare class FeatureBus implements CommandBus {
15
22
  private readonly dispatch;
16
23
  constructor(dispatch: Dispatch);
17
- execute<R>(command: Command<R>, _returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
24
+ execute<R>(command: Command<R>, returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
25
+ execute<R>(command: Command<R>, options: ExecuteOptions): Promise<R>;
26
+ execute(event: DomainEvent, options?: ExecuteOptions | Cause): Promise<unknown>;
18
27
  }
@@ -1,7 +1,14 @@
1
- /** @internal — the heterogeneous registry erases the type; registration checks the association. */
2
- export async function executeTyped(dispatch, command) {
3
- // As with Python's return_type, this does not validate the returned data at runtime.
4
- return (await dispatch(command));
1
+ /** What the second argument of `execute` is: options, a delivered event's cause, or a type. */
2
+ export function optionsOf(second) {
3
+ if (second === null || typeof second !== "object")
4
+ return {};
5
+ const given = second;
6
+ if (typeof given.id === "string" && typeof given.correlationId === "string")
7
+ return { cause: { id: given.id, correlationId: given.correlationId } };
8
+ return {
9
+ ...(given.signal ? { signal: given.signal } : {}),
10
+ ...(given.cause ? { cause: given.cause } : {}),
11
+ };
5
12
  }
6
13
  /**
7
14
  * What an ApplicationService composes its context's Features through (`this.featureBus`): the
@@ -12,7 +19,8 @@ export class FeatureBus {
12
19
  constructor(dispatch) {
13
20
  this.dispatch = dispatch;
14
21
  }
15
- execute(command, _returnType) {
16
- return executeTyped(this.dispatch, command);
22
+ execute(executable, second) {
23
+ // As with Python's return_type, the answer is not validated at runtime.
24
+ return this.dispatch(executable, optionsOf(second));
17
25
  }
18
26
  }
@@ -5,7 +5,7 @@
5
5
  * class CommandCancelIssue extends Command<ResponseCancelIssue> {
6
6
  * constructor(readonly issue_id: string) { super(); }
7
7
  * }
8
- * const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
8
+ * const answer = await projectBoundedContext.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
9
9
  *
10
10
  * Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
11
11
  * `execute` infers it with no second argument. The association is type-only: absent from a
@@ -22,11 +22,41 @@ export declare abstract class Command<R> extends DataTransferObject {
22
22
  /** Type-only association: absent from serialized DTOs and not a runtime validator. */
23
23
  readonly [responseType]: R;
24
24
  }
25
+ /**
26
+ * A fact that already happened, as sincpro_framework's `DomainEvent`: executed like a Command, by
27
+ * the Feature or ApplicationService registered for its class — its subscriber in this bounded
28
+ * context —, through the same pipeline. Delivering it (a queue, an outbox) is the platform's.
29
+ *
30
+ * class OrderPaid extends DomainEvent {
31
+ * static readonly eventName = "shop.order.v1.paid";
32
+ * constructor(readonly order_id: string) { super(); }
33
+ * }
34
+ * projectBoundedContext.feature(OrderPaid)(NotifyCustomer);
35
+ * await projectBoundedContext.execute(new OrderPaid("o-1"));
36
+ *
37
+ * Context: the backend names an event on the wire by `name`, `<context>.<aggregate>.vN.<fact>`,
38
+ * and defaults it to the class's name; a client is minified, so the name is declared, as
39
+ * `serviceName` is. `name` is reserved, as the backend reserves it: every event reads its class's
40
+ * `eventName` there, and a subclass cannot make it a field.
41
+ */
42
+ export declare abstract class DomainEvent extends DataTransferObject {
43
+ protected readonly eventBrand: void;
44
+ /** Its wire name: its class's `eventName`. */
45
+ get name(): string;
46
+ }
47
+ /** What an event class declares: the name it is known by on the wire. */
48
+ type EventNamed = {
49
+ readonly eventName: string;
50
+ };
25
51
  export type AnyCommand = Command<unknown>;
26
- /** What a Command answers. */
27
- export type Answer<C extends AnyCommand> = C[typeof responseType];
52
+ /** What a service is registered for and executes: a Command, or a domain event. */
53
+ export type Executable = AnyCommand | DomainEvent;
54
+ /** What it answers: a Command's Response; whatever its subscriber returns, for an event. */
55
+ export type Answer<T> = T extends Command<infer R> ? R : unknown;
28
56
  /** A Command's class: what a service is registered for, by exact constructor identity. */
29
57
  export type CommandClass<C extends AnyCommand> = abstract new (...args: never[]) => C;
58
+ /** A domain event's class, with its wire name. */
59
+ export type EventClass<E extends DomainEvent> = (abstract new (...args: never[]) => E) & EventNamed;
30
60
  /** A Response's class: the backend's `return_type`, accepted for the same reading. */
31
61
  export type ResponseClass<R> = abstract new (...args: never[]) => R;
32
62
  export {};
@@ -5,7 +5,7 @@
5
5
  * class CommandCancelIssue extends Command<ResponseCancelIssue> {
6
6
  * constructor(readonly issue_id: string) { super(); }
7
7
  * }
8
- * const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
8
+ * const answer = await projectBoundedContext.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
9
9
  *
10
10
  * Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
11
11
  * `execute` infers it with no second argument. The association is type-only: absent from a
@@ -18,3 +18,26 @@ export class Response extends DataTransferObject {
18
18
  }
19
19
  export class Command extends DataTransferObject {
20
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
+ * projectBoundedContext.feature(OrderPaid)(NotifyCustomer);
31
+ * await projectBoundedContext.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
+ }
@@ -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
- * project.addFeatureErrorHandler((error) => {
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 `project.execute`), feature and
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
@@ -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
+ * projectBoundedContext.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
+ * projectBoundedContext.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 salesBoundedContext.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 {};