@sincpro/client-framework 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +71 -13
- package/MANIFESTO.md +59 -14
- package/README.md +456 -55
- package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
- package/dist/circuit_breaker/domain/circuit.js +37 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
- package/dist/circuit_breaker/index.d.ts +9 -0
- package/dist/circuit_breaker/index.js +9 -0
- package/dist/client/adapters/json-rpc.d.ts +40 -0
- package/dist/client/adapters/json-rpc.js +97 -0
- package/dist/client/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- package/dist/client/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +5 -1
- package/dist/client/index.js +5 -1
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +20 -7
- package/dist/client/services/propagation.js +44 -15
- package/dist/codegen/entrypoint/cli.js +7 -7
- package/dist/codegen/services/client.d.ts +7 -8
- package/dist/codegen/services/client.js +50 -148
- package/dist/codegen/services/scaffold.d.ts +7 -4
- package/dist/codegen/services/scaffold.js +23 -27
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/context/domain/execution.d.ts +12 -0
- package/dist/context/entrypoint/context.d.ts +5 -4
- package/dist/core/domain/bus.d.ts +16 -7
- package/dist/core/domain/bus.js +14 -6
- package/dist/core/domain/dto.d.ts +33 -3
- package/dist/core/domain/dto.js +24 -1
- package/dist/core/domain/errors.d.ts +2 -2
- package/dist/core/domain/pipeline.d.ts +89 -0
- package/dist/core/domain/pipeline.js +1 -0
- package/dist/core/domain/services.d.ts +16 -5
- package/dist/core/domain/services.js +5 -2
- package/dist/core/entrypoint/bounded-context.d.ts +96 -16
- package/dist/core/entrypoint/bounded-context.js +244 -39
- package/dist/core/index.d.ts +4 -2
- package/dist/core/index.js +3 -2
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -0
- package/dist/core/services/binding.d.ts +2 -2
- package/dist/core/services/binding.js +6 -3
- package/dist/core/services/execution.d.ts +59 -12
- package/dist/core/services/execution.js +114 -19
- package/dist/core/services/interceptors.d.ts +15 -0
- package/dist/core/services/interceptors.js +25 -0
- package/dist/core/services/listening.d.ts +17 -0
- package/dist/core/services/listening.js +25 -0
- package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
- package/dist/event_driven/adapters/memory-event-store.js +23 -0
- package/dist/event_driven/adapters/stored-queue.d.ts +68 -0
- package/dist/event_driven/adapters/stored-queue.js +193 -0
- package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
- package/dist/event_driven/adapters/sync-queue.js +19 -0
- package/dist/event_driven/domain/envelope.d.ts +28 -0
- package/dist/event_driven/domain/envelope.js +19 -0
- package/dist/event_driven/domain/queue.d.ts +40 -0
- package/dist/event_driven/domain/queue.js +1 -0
- package/dist/event_driven/domain/store.d.ts +56 -0
- package/dist/event_driven/domain/store.js +1 -0
- package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
- package/dist/event_driven/entrypoint/publisher.js +35 -0
- package/dist/event_driven/entrypoint/subscriber.d.ts +42 -0
- package/dist/event_driven/entrypoint/subscriber.js +62 -0
- package/dist/event_driven/entrypoint/subscriptions.d.ts +110 -0
- package/dist/event_driven/entrypoint/subscriptions.js +184 -0
- package/dist/event_driven/index.d.ts +22 -0
- package/dist/event_driven/index.js +20 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/observability/adapters/memory.d.ts +1 -1
- package/dist/observability/domain/logging.d.ts +10 -4
- package/dist/observability/services/logging.d.ts +7 -4
- package/dist/observability/services/logging.js +3 -2
- package/dist/observability/services/outcome.d.ts +6 -6
- package/dist/observability/services/outcome.js +10 -13
- package/package.json +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { processContext } from "../../context/infrastructure/process.js";
|
|
2
|
+
import { optionsOf } from "../../core/domain/bus.js";
|
|
3
|
+
import { DomainEvent } from "../../core/domain/dto.js";
|
|
4
|
+
import { RESERVED_MEMBERS } from "../../core/domain/services.js";
|
|
5
|
+
import { executeService } from "../../core/services/execution.js";
|
|
6
|
+
/**
|
|
7
|
+
* An entrypoint that hears one domain event and calls the use cases it starts — as a cron calls
|
|
8
|
+
* them on a tick, or a screen on a gesture:
|
|
9
|
+
*
|
|
10
|
+
* class LoggedSuccessfully extends EventSubscriber<OdooLoggedIn> {
|
|
11
|
+
* static readonly subscriberName = "LoggedSuccessfully";
|
|
12
|
+
* declare readonly ticketsBoundedContext: BoundedContext<TicketsDependencies>;
|
|
13
|
+
*
|
|
14
|
+
* async handle(event: OdooLoggedIn) {
|
|
15
|
+
* await this.ticketsBoundedContext.execute(new CommandPrepareSession(event.uid));
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* Context: a subscriber is not a use case. It reads the event, builds the Command — or the
|
|
20
|
+
* Commands — and executes them on a bounded context it was given; what to decide, and work of
|
|
21
|
+
* several steps, is a Feature or an ApplicationService it executes. While `handle` runs,
|
|
22
|
+
* `this.span`, `this.logger`, `this.signal`, `this.execution` and every dependency are that
|
|
23
|
+
* delivery's: a bounded context it was given executes as a child of it — the same flow, caused by
|
|
24
|
+
* it (`BIND`). `subscriberName` is stable metadata, never `constructor.name`. Like a service, a
|
|
25
|
+
* subscriber is one instance for every delivery: what belongs to one goes in its locals.
|
|
26
|
+
*/
|
|
27
|
+
export class EventSubscriber {
|
|
28
|
+
}
|
|
29
|
+
function isEventClass(value) {
|
|
30
|
+
return typeof value === "function" && value.prototype instanceof DomainEvent;
|
|
31
|
+
}
|
|
32
|
+
function nameOf(Subscriber) {
|
|
33
|
+
const named = Subscriber;
|
|
34
|
+
return String(named?.subscriberName ?? named?.name ?? Subscriber);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The subscribers of one part of an application — which `EventSubscriber` hears which event —,
|
|
38
|
+
* with the dependencies they call: what a queue's `Subscriber` hands an event to, beside the
|
|
39
|
+
* bounded contexts:
|
|
40
|
+
*
|
|
41
|
+
* export const ticketsSubscriptions = new Subscriptions<TicketsEntrypoints>("tickets-subscriptions");
|
|
42
|
+
* ticketsSubscriptions.addDependency("ticketsBoundedContext", ticketsBoundedContext);
|
|
43
|
+
* ticketsSubscriptions.subscribe(OdooLoggedIn)(LoggedSuccessfully);
|
|
44
|
+
*
|
|
45
|
+
* storedQueue({ store, subscriber: new Subscriber(odooBoundedContext, ticketsSubscriptions) });
|
|
46
|
+
*
|
|
47
|
+
* Context: the shape of a bounded context, and of the backend's `Crons`: a name that is its
|
|
48
|
+
* identity in spans, logs and the queue's record of who already heard an event — so it differs
|
|
49
|
+
* from every bounded context's —, dependencies by name, and a function (or a decorator) that
|
|
50
|
+
* registers. One subscriber per event here: several reactions to one event are one
|
|
51
|
+
* ApplicationService it executes, or subscribers of other registries. Each delivery is a run of
|
|
52
|
+
* its own (`<name>/<subscriberName>`, layer `entrypoint`), continuing the envelope's flow; it is
|
|
53
|
+
* not a use case, so interceptors and listeners do not see it — they see the use cases it calls.
|
|
54
|
+
*/
|
|
55
|
+
export class Subscriptions {
|
|
56
|
+
name;
|
|
57
|
+
registrations = new Map();
|
|
58
|
+
dependencyValues = new Map();
|
|
59
|
+
constructor(name) {
|
|
60
|
+
this.name = name;
|
|
61
|
+
if (name.trim() === "" || name.includes("/")) {
|
|
62
|
+
throw new TypeError("Subscriptions need a nonempty name without '/'");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A dependency its subscribers find on `this`, bound to each delivery; `replaces` as a bounded context's. */
|
|
66
|
+
addDependency(name, dependency, options) {
|
|
67
|
+
if (RESERVED_MEMBERS.has(name))
|
|
68
|
+
throw new Error(`Reserved dependency name: ${name}`);
|
|
69
|
+
const registered = this.dependencyValues.has(name);
|
|
70
|
+
if (registered && options === undefined) {
|
|
71
|
+
throw new Error(`Dependency already registered: ${name}`);
|
|
72
|
+
}
|
|
73
|
+
if (options && (!registered || this.dependencyValues.get(name) !== options.replaces)) {
|
|
74
|
+
throw new Error(`Dependency replacement does not match the registration: ${name}`);
|
|
75
|
+
}
|
|
76
|
+
this.dependencyValues.set(name, dependency);
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Registers the subscriber that hears `event` here. With `replaces`, it takes the place of the
|
|
81
|
+
* one registered now.
|
|
82
|
+
*/
|
|
83
|
+
subscribe(event, options = {}) {
|
|
84
|
+
return (Subscriber, decorator) => {
|
|
85
|
+
const register = () => this.register(event, Subscriber, options.replaces);
|
|
86
|
+
// Standard decorators run before static fields have initialized.
|
|
87
|
+
if (decorator)
|
|
88
|
+
decorator.addInitializer(register);
|
|
89
|
+
else
|
|
90
|
+
register();
|
|
91
|
+
return Subscriber;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** The subscriber registered now for an event class — what a `replaces` names. */
|
|
95
|
+
handlerOf(event) {
|
|
96
|
+
return this.registrations.get(event)?.Subscriber;
|
|
97
|
+
}
|
|
98
|
+
/** The event class registered here under a wire name, or `undefined` when nobody here hears it. */
|
|
99
|
+
eventTypeOf(name) {
|
|
100
|
+
for (const event of this.registrations.keys()) {
|
|
101
|
+
if (event.eventName === name)
|
|
102
|
+
return event;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `event` heard by its subscriber here — continuing the envelope's flow when it is given
|
|
108
|
+
* (`execute(event, envelope)`) — and what `handle` returned.
|
|
109
|
+
*
|
|
110
|
+
* 1. The subscriber registered for the event's exact class; none: an error.
|
|
111
|
+
* 2. A run `<name>/<subscriberName>`, layer `entrypoint`: its span, log lines and — when it
|
|
112
|
+
* fails with a fault nothing reported yet — its report; its dependencies bound to it.
|
|
113
|
+
* Final: what `handle` returned; its failure, as it is, to the queue that delivered it.
|
|
114
|
+
*/
|
|
115
|
+
execute(event, options) {
|
|
116
|
+
const type = Object.getPrototypeOf(event)?.constructor;
|
|
117
|
+
const registration = this.registrations.get(type);
|
|
118
|
+
if (registration === undefined) {
|
|
119
|
+
return Promise.reject(new Error(`${this.name}: no subscriber hears ${event.name}`));
|
|
120
|
+
}
|
|
121
|
+
registration.instance ??= new registration.Subscriber();
|
|
122
|
+
const given = optionsOf(options);
|
|
123
|
+
const attributes = {
|
|
124
|
+
"sincpro.event": event.name,
|
|
125
|
+
...(registration.replaces ? { "sincpro.replaces": registration.replaces } : {}),
|
|
126
|
+
};
|
|
127
|
+
return executeService(registration.instance, event, {
|
|
128
|
+
boundedContext: this.name,
|
|
129
|
+
name: registration.name,
|
|
130
|
+
layer: "entrypoint",
|
|
131
|
+
method: "handle",
|
|
132
|
+
attributes,
|
|
133
|
+
dependencies: new Map(this.dependencyValues),
|
|
134
|
+
context: () => ({ ...processContext.values }),
|
|
135
|
+
...(given.cause ? { cause: given.cause } : {}),
|
|
136
|
+
...(given.signal ? { signal: given.signal } : {}),
|
|
137
|
+
interceptors: [],
|
|
138
|
+
expected: () => false,
|
|
139
|
+
handlers: [],
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* 1. The subscriber is an `EventSubscriber`, for a domain event class with its `eventName`.
|
|
144
|
+
* 2. The event has no subscriber here yet — or, with `replaces`, the one it has is that one.
|
|
145
|
+
* 3. Its `subscriberName` is given and no other subscriber here has it; no other event class
|
|
146
|
+
* here has the event's name.
|
|
147
|
+
* Final: registered, said on its spans when it replaced another; the next delivery hears it.
|
|
148
|
+
*/
|
|
149
|
+
register(event, Subscriber, replaces) {
|
|
150
|
+
if (!(Subscriber.prototype instanceof EventSubscriber)) {
|
|
151
|
+
throw new TypeError("The subscriber is not an EventSubscriber");
|
|
152
|
+
}
|
|
153
|
+
if (!isEventClass(event)) {
|
|
154
|
+
throw new TypeError("A subscriber is registered for a DomainEvent class");
|
|
155
|
+
}
|
|
156
|
+
if (typeof event.eventName !== "string" || event.eventName.trim() === "") {
|
|
157
|
+
throw new Error("A domain event needs a nonempty eventName");
|
|
158
|
+
}
|
|
159
|
+
const current = this.registrations.get(event);
|
|
160
|
+
if (replaces !== undefined && current?.Subscriber !== replaces) {
|
|
161
|
+
throw new Error(`Replacement does not match the subscription: ${nameOf(replaces)}`);
|
|
162
|
+
}
|
|
163
|
+
if (replaces === undefined && current) {
|
|
164
|
+
throw new Error(`Event already subscribed in ${this.name}: one subscriber per event — several reactions are one ApplicationService`);
|
|
165
|
+
}
|
|
166
|
+
const name = Subscriber.subscriberName;
|
|
167
|
+
if (typeof name !== "string" || name.trim() === "" || name.includes("/")) {
|
|
168
|
+
throw new Error("A subscriber needs a nonempty subscriberName without '/'");
|
|
169
|
+
}
|
|
170
|
+
for (const [other, registration] of this.registrations) {
|
|
171
|
+
if (registration !== current && registration.name === name) {
|
|
172
|
+
throw new Error(`Subscriber name already registered: ${name}`);
|
|
173
|
+
}
|
|
174
|
+
if (other !== event && other.eventName === event.eventName) {
|
|
175
|
+
throw new Error(`Event name already registered: ${event.eventName}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
this.registrations.set(event, {
|
|
179
|
+
name,
|
|
180
|
+
Subscriber,
|
|
181
|
+
...(current ? { replaces: current.name } : {}),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-driven: the ports and the in-memory pieces that let every platform publish domain events
|
|
3
|
+
* and subscribe to them the same way — as sincpro_framework's `event_driven`. Delivering them
|
|
4
|
+
* durably (SQLite on a phone, IndexedDB in a browser, a broker) is each platform's, behind
|
|
5
|
+
* `EventQueue`.
|
|
6
|
+
*
|
|
7
|
+
* domain/ the envelope and the ports: EventQueue, Subscribes, EventStore
|
|
8
|
+
* adapters/ syncQueue (in the call), storedQueue (store, then deliver, retry, set aside),
|
|
9
|
+
* memoryEventStore
|
|
10
|
+
* entrypoint/ Publisher (what a Feature holds), Subscriber (the queue's door: everyone who
|
|
11
|
+
* hears), Subscriptions and EventSubscriber (the entrypoint that hears an event and
|
|
12
|
+
* calls use cases)
|
|
13
|
+
*/
|
|
14
|
+
export { type MemoryEventStore, memoryEventStore, } from "./adapters/memory-event-store.js";
|
|
15
|
+
export { type StoredQueue, storedQueue, type StoredQueueOptions, } from "./adapters/stored-queue.js";
|
|
16
|
+
export { syncQueue } from "./adapters/sync-queue.js";
|
|
17
|
+
export { type Envelope, envelopeOf, } from "./domain/envelope.js";
|
|
18
|
+
export type { EventQueue, Handled, Subscribes, } from "./domain/queue.js";
|
|
19
|
+
export type { DeliveryFailure, EventStore, StoredEvent, StoredState, } from "./domain/store.js";
|
|
20
|
+
export { Publisher } from "./entrypoint/publisher.js";
|
|
21
|
+
export { type HearingContext, Subscriber, } from "./entrypoint/subscriber.js";
|
|
22
|
+
export { EventSubscriber, type Subscribing, type SubscriptionOptions, Subscriptions, } from "./entrypoint/subscriptions.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event-driven: the ports and the in-memory pieces that let every platform publish domain events
|
|
3
|
+
* and subscribe to them the same way — as sincpro_framework's `event_driven`. Delivering them
|
|
4
|
+
* durably (SQLite on a phone, IndexedDB in a browser, a broker) is each platform's, behind
|
|
5
|
+
* `EventQueue`.
|
|
6
|
+
*
|
|
7
|
+
* domain/ the envelope and the ports: EventQueue, Subscribes, EventStore
|
|
8
|
+
* adapters/ syncQueue (in the call), storedQueue (store, then deliver, retry, set aside),
|
|
9
|
+
* memoryEventStore
|
|
10
|
+
* entrypoint/ Publisher (what a Feature holds), Subscriber (the queue's door: everyone who
|
|
11
|
+
* hears), Subscriptions and EventSubscriber (the entrypoint that hears an event and
|
|
12
|
+
* calls use cases)
|
|
13
|
+
*/
|
|
14
|
+
export { memoryEventStore, } from "./adapters/memory-event-store.js";
|
|
15
|
+
export { storedQueue, } from "./adapters/stored-queue.js";
|
|
16
|
+
export { syncQueue } from "./adapters/sync-queue.js";
|
|
17
|
+
export { envelopeOf, } from "./domain/envelope.js";
|
|
18
|
+
export { Publisher } from "./entrypoint/publisher.js";
|
|
19
|
+
export { Subscriber, } from "./entrypoint/subscriber.js";
|
|
20
|
+
export { EventSubscriber, Subscriptions, } from "./entrypoint/subscriptions.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -7,19 +7,24 @@
|
|
|
7
7
|
* as sincpro_framework's are — `domain/` (ports and rules), `adapters/` (their implementations),
|
|
8
8
|
* `services/`, `infrastructure/`, `entrypoint/` — and says what it offers in its own `index.ts`:
|
|
9
9
|
*
|
|
10
|
-
* common/ the values of the wire, as the backend writes them
|
|
10
|
+
* common/ the values of the wire, as the backend writes them; what a failure is
|
|
11
|
+
* circuit_breaker/ stop calling what keeps failing; try it again once it may be back
|
|
11
12
|
* context/ the context's levels, where they live, what travels, the execution
|
|
12
13
|
* observability/ tracing, error reporting and logging: ports, console, loggers
|
|
13
14
|
* client/ the seam with a server: transports, entities, the client contract
|
|
14
15
|
* model/ model definitions
|
|
15
|
-
* core/ Commands, Features, ApplicationServices, BoundedContext, the
|
|
16
|
+
* core/ Commands, events, Features, ApplicationServices, BoundedContext, the
|
|
17
|
+
* pipeline's extension points, the process
|
|
18
|
+
* event_driven/ publishing and subscribing to domain events: ports, Publisher, Subscriber
|
|
16
19
|
*
|
|
17
20
|
* The generator is `@sincpro/client-framework/codegen`, and the in-memory observability
|
|
18
21
|
* `@sincpro/client-framework/testing`: neither is part of what an application runs.
|
|
19
22
|
*/
|
|
23
|
+
export * from "./circuit_breaker/index.js";
|
|
20
24
|
export * from "./client/index.js";
|
|
21
25
|
export * from "./common/index.js";
|
|
22
26
|
export * from "./context/index.js";
|
|
23
27
|
export * from "./core/index.js";
|
|
28
|
+
export * from "./event_driven/index.js";
|
|
24
29
|
export * from "./model/index.js";
|
|
25
30
|
export * from "./observability/index.js";
|
package/dist/index.js
CHANGED
|
@@ -7,19 +7,24 @@
|
|
|
7
7
|
* as sincpro_framework's are — `domain/` (ports and rules), `adapters/` (their implementations),
|
|
8
8
|
* `services/`, `infrastructure/`, `entrypoint/` — and says what it offers in its own `index.ts`:
|
|
9
9
|
*
|
|
10
|
-
* common/ the values of the wire, as the backend writes them
|
|
10
|
+
* common/ the values of the wire, as the backend writes them; what a failure is
|
|
11
|
+
* circuit_breaker/ stop calling what keeps failing; try it again once it may be back
|
|
11
12
|
* context/ the context's levels, where they live, what travels, the execution
|
|
12
13
|
* observability/ tracing, error reporting and logging: ports, console, loggers
|
|
13
14
|
* client/ the seam with a server: transports, entities, the client contract
|
|
14
15
|
* model/ model definitions
|
|
15
|
-
* core/ Commands, Features, ApplicationServices, BoundedContext, the
|
|
16
|
+
* core/ Commands, events, Features, ApplicationServices, BoundedContext, the
|
|
17
|
+
* pipeline's extension points, the process
|
|
18
|
+
* event_driven/ publishing and subscribing to domain events: ports, Publisher, Subscriber
|
|
16
19
|
*
|
|
17
20
|
* The generator is `@sincpro/client-framework/codegen`, and the in-memory observability
|
|
18
21
|
* `@sincpro/client-framework/testing`: neither is part of what an application runs.
|
|
19
22
|
*/
|
|
23
|
+
export * from "./circuit_breaker/index.js";
|
|
20
24
|
export * from "./client/index.js";
|
|
21
25
|
export * from "./common/index.js";
|
|
22
26
|
export * from "./context/index.js";
|
|
23
27
|
export * from "./core/index.js";
|
|
28
|
+
export * from "./event_driven/index.js";
|
|
24
29
|
export * from "./model/index.js";
|
|
25
30
|
export * from "./observability/index.js";
|
|
@@ -6,7 +6,7 @@ import type { Outcome, Tracer } from "../domain/tracing.js";
|
|
|
6
6
|
*
|
|
7
7
|
* const tracer = memoryTracer();
|
|
8
8
|
* configureProcess({ tracer });
|
|
9
|
-
* await
|
|
9
|
+
* await projectBoundedContext.execute(new CommandCloseIssue("issue-1"));
|
|
10
10
|
* tracer.spans.map((one) => one.name); // ["project/CloseIssue", "project/Store", …]
|
|
11
11
|
*
|
|
12
12
|
* Context: a use case is proven in Node with nothing of a platform; these are the platform's
|
|
@@ -5,14 +5,18 @@ import type { Attributes, Outcome } from "./tracing.js";
|
|
|
5
5
|
*
|
|
6
6
|
* Context: a line says which layer wrote it, the way sincpro_framework's architecture names
|
|
7
7
|
* them: the application layer (a bounded context and its ApplicationServices and Features,
|
|
8
|
-
* through `
|
|
9
|
-
*
|
|
8
|
+
* through `projectBoundedContext.logger` and `this.logger`), the entrypoints that call it (an
|
|
9
|
+
* `EventSubscriber`'s `this.logger`), its adapters (`loggerAdapter`) and the infrastructure
|
|
10
|
+
* around them (`logger`). A line written inside a use case carries that
|
|
10
11
|
* execution's identity; nothing writes a DTO, a response, an error body or the session's
|
|
11
12
|
* context on its own.
|
|
12
13
|
*/
|
|
13
14
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The layer a line comes from: the infrastructure, an adapter, an entrypoint that calls use cases
|
|
17
|
+
* (an `EventSubscriber`), a bounded context, an ApplicationService, a Feature.
|
|
18
|
+
*/
|
|
19
|
+
export type LogLayer = "infrastructure" | "adapter" | "entrypoint" | "bounded_context" | "application_service" | "feature";
|
|
16
20
|
/** Structured logging never implicitly includes a DTO, response, error body or session context. */
|
|
17
21
|
export type LogRecord = {
|
|
18
22
|
readonly timestamp: string;
|
|
@@ -24,6 +28,8 @@ export type LogRecord = {
|
|
|
24
28
|
readonly boundedContext?: string;
|
|
25
29
|
readonly useCase?: string;
|
|
26
30
|
readonly executionId?: string;
|
|
31
|
+
/** The chain the run belongs to: every run and event of one flow share it. */
|
|
32
|
+
readonly correlationId?: string;
|
|
27
33
|
readonly traceId?: string;
|
|
28
34
|
readonly spanId?: string;
|
|
29
35
|
readonly outcome?: Outcome;
|
|
@@ -5,14 +5,16 @@ import type { Outcome } from "../domain/tracing.js";
|
|
|
5
5
|
export declare const logger: LayerLogger;
|
|
6
6
|
/** The adapters' logger: what talks to a printer, a database, another system. */
|
|
7
7
|
export declare const loggerAdapter: LayerLogger;
|
|
8
|
-
/** @internal — a bounded context's own logger (`
|
|
8
|
+
/** @internal — a bounded context's own logger (`projectBoundedContext.logger`). */
|
|
9
9
|
export declare function boundedContextLogger(name: string): LayerLogger;
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/** A run's layer: an entrypoint's (an `EventSubscriber`), a Feature's or an ApplicationService's. */
|
|
11
|
+
type RunLogLayer = Extract<LogLayer, "entrypoint" | "feature" | "application_service">;
|
|
12
|
+
/** @internal — what a run (a use case, an entrypoint) finds as `this.logger` while it runs. */
|
|
13
|
+
export declare function executionLogger(layer: RunLogLayer, execution: Execution): LayerLogger;
|
|
12
14
|
/** Each destination keeps its own policy; one failing sink does not hold back the others. */
|
|
13
15
|
export declare function combineLoggers(...loggers: readonly Logger[]): Logger;
|
|
14
16
|
/** @internal — lifecycle records contain correlation and outcome, never business payloads. */
|
|
15
|
-
export declare function logExecution(layer:
|
|
17
|
+
export declare function logExecution(layer: RunLogLayer, execution: Execution, completed?: {
|
|
16
18
|
outcome: Outcome;
|
|
17
19
|
durationMs: number;
|
|
18
20
|
}): void;
|
|
@@ -25,3 +27,4 @@ export declare function logHandled(layer: LogLayer, correlation: {
|
|
|
25
27
|
} | {
|
|
26
28
|
boundedContext: string;
|
|
27
29
|
}): void;
|
|
30
|
+
export {};
|
|
@@ -19,6 +19,7 @@ function correlationOf(execution) {
|
|
|
19
19
|
boundedContext: execution.boundedContext,
|
|
20
20
|
useCase: execution.useCase,
|
|
21
21
|
executionId: execution.id,
|
|
22
|
+
correlationId: execution.correlationId,
|
|
22
23
|
...(execution.span.traceId ? { traceId: execution.span.traceId } : {}),
|
|
23
24
|
...(execution.span.spanId ? { spanId: execution.span.spanId } : {}),
|
|
24
25
|
};
|
|
@@ -50,11 +51,11 @@ function layerLogger(layer, sink, correlation = {}) {
|
|
|
50
51
|
export const logger = layerLogger("infrastructure", processSink);
|
|
51
52
|
/** The adapters' logger: what talks to a printer, a database, another system. */
|
|
52
53
|
export const loggerAdapter = layerLogger("adapter", processSink);
|
|
53
|
-
/** @internal — a bounded context's own logger (`
|
|
54
|
+
/** @internal — a bounded context's own logger (`projectBoundedContext.logger`). */
|
|
54
55
|
export function boundedContextLogger(name) {
|
|
55
56
|
return layerLogger("bounded_context", processSink, { boundedContext: name });
|
|
56
57
|
}
|
|
57
|
-
/** @internal — what a
|
|
58
|
+
/** @internal — what a run (a use case, an entrypoint) finds as `this.logger` while it runs. */
|
|
58
59
|
export function executionLogger(layer, execution) {
|
|
59
60
|
return layerLogger(layer, processSink, correlationOf(execution));
|
|
60
61
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Outcome } from "../domain/tracing.js";
|
|
2
2
|
/**
|
|
3
|
-
* How an error ended a use case, by default.
|
|
3
|
+
* How an error ended a use case, by default — read off the failure it is (`failureOf`).
|
|
4
4
|
*
|
|
5
5
|
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
6
|
-
* the business answering, not a fault. Here the same: a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* context adds its own expected errors
|
|
10
|
-
* (`configureProcess({ outcomeOf })`).
|
|
6
|
+
* the business answering, not a fault. Here the same: a failure that is the caller's answer (a
|
|
7
|
+
* 4xx, an error that declares a kind such as `domain` or `not_found`) is `refused`; a request
|
|
8
|
+
* the caller stopped waiting for is `aborted`; a fault — a bug, a 5xx, a server nobody could
|
|
9
|
+
* reach — is `failed`, and reported. A bounded context adds its own expected errors
|
|
10
|
+
* (`expectErrors`); a process replaces this whole (`configureProcess({ outcomeOf })`).
|
|
11
11
|
*/
|
|
12
12
|
export declare function outcomeOf(error: unknown): Outcome;
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { failureOf } from "../../common/failures.js";
|
|
2
2
|
/**
|
|
3
|
-
* How an error ended a use case, by default.
|
|
3
|
+
* How an error ended a use case, by default — read off the failure it is (`failureOf`).
|
|
4
4
|
*
|
|
5
5
|
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
6
|
-
* the business answering, not a fault. Here the same: a
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* context adds its own expected errors
|
|
10
|
-
* (`configureProcess({ outcomeOf })`).
|
|
6
|
+
* the business answering, not a fault. Here the same: a failure that is the caller's answer (a
|
|
7
|
+
* 4xx, an error that declares a kind such as `domain` or `not_found`) is `refused`; a request
|
|
8
|
+
* the caller stopped waiting for is `aborted`; a fault — a bug, a 5xx, a server nobody could
|
|
9
|
+
* reach — is `failed`, and reported. A bounded context adds its own expected errors
|
|
10
|
+
* (`expectErrors`); a process replaces this whole (`configureProcess({ outcomeOf })`).
|
|
11
11
|
*/
|
|
12
12
|
export function outcomeOf(error) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (error !== null &&
|
|
16
|
-
typeof error === "object" &&
|
|
17
|
-
error.name === "AbortError")
|
|
13
|
+
const failure = failureOf(error);
|
|
14
|
+
if (failure.aborted)
|
|
18
15
|
return "aborted";
|
|
19
|
-
return "failed";
|
|
16
|
+
return failure.fault ? "failed" : "refused";
|
|
20
17
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/client-framework",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "The client side of sincpro_framework, for every platform: Feature and ApplicationService traced and reported by default, applications made of domains, and the typed client generated from a service's contract. No React, no DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|