@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.
Files changed (176) hide show
  1. package/AGENTS.md +95 -25
  2. package/MANIFESTO.md +103 -40
  3. package/README.md +608 -333
  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/{memory.d.ts → adapters/memory.d.ts} +1 -1
  13. package/dist/client/{memory.js → adapters/memory.js} +2 -2
  14. package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
  15. package/dist/client/adapters/rest-operations.d.ts +19 -0
  16. package/dist/client/adapters/rest-operations.js +66 -0
  17. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
  18. package/dist/client/{rest.js → adapters/rest.js} +2 -2
  19. package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
  20. package/dist/client/domain/operation.d.ts +73 -0
  21. package/dist/client/domain/operation.js +19 -0
  22. package/dist/client/index.d.ts +20 -0
  23. package/dist/client/index.js +20 -0
  24. package/dist/client/services/guarded.d.ts +17 -0
  25. package/dist/client/services/guarded.js +19 -0
  26. package/dist/client/services/propagation.d.ts +45 -0
  27. package/dist/{services/execution.js → client/services/propagation.js} +49 -36
  28. package/dist/client/{retry.js → services/retry.js} +1 -1
  29. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
  30. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  31. package/dist/codegen/{names.js → domain/names.js} +3 -3
  32. package/dist/codegen/{cli.js → entrypoint/cli.js} +15 -14
  33. package/dist/codegen/index.d.ts +7 -7
  34. package/dist/codegen/index.js +5 -10
  35. package/dist/codegen/services/client.d.ts +15 -0
  36. package/dist/codegen/services/client.js +111 -0
  37. package/dist/codegen/services/domain.d.ts +20 -0
  38. package/dist/codegen/{domain.js → services/domain.js} +82 -25
  39. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
  40. package/dist/codegen/{entities.js → services/entities.js} +18 -4
  41. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  42. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +8 -5
  43. package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
  44. package/dist/common/failures.d.ts +83 -0
  45. package/dist/common/failures.js +212 -0
  46. package/dist/common/index.d.ts +11 -0
  47. package/dist/common/index.js +11 -0
  48. package/dist/context/adapters/memory.d.ts +6 -0
  49. package/dist/context/adapters/memory.js +34 -0
  50. package/dist/context/adapters/plain-codec.d.ts +3 -0
  51. package/dist/context/adapters/plain-codec.js +16 -0
  52. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  53. package/dist/context/{codec.js → domain/codec.js} +0 -14
  54. package/dist/context/domain/execution.d.ts +45 -0
  55. package/dist/context/domain/execution.js +11 -0
  56. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  57. package/dist/context/{store.js → domain/store.js} +1 -34
  58. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  59. package/dist/context/index.d.ts +17 -0
  60. package/dist/context/index.js +15 -0
  61. package/dist/context/infrastructure/process.d.ts +13 -0
  62. package/dist/context/infrastructure/process.js +35 -0
  63. package/dist/core/domain/bus.d.ts +27 -0
  64. package/dist/core/domain/bus.js +26 -0
  65. package/dist/core/domain/dto.d.ts +62 -0
  66. package/dist/core/domain/dto.js +43 -0
  67. package/dist/core/domain/errors.d.ts +18 -0
  68. package/dist/core/domain/errors.js +1 -0
  69. package/dist/core/domain/pipeline.d.ts +89 -0
  70. package/dist/core/domain/pipeline.js +1 -0
  71. package/dist/core/domain/services.d.ts +47 -0
  72. package/dist/core/domain/services.js +31 -0
  73. package/dist/core/entrypoint/bounded-context.d.ts +139 -0
  74. package/dist/core/entrypoint/bounded-context.js +426 -0
  75. package/dist/core/entrypoint/screens.d.ts +6 -0
  76. package/dist/core/entrypoint/screens.js +136 -0
  77. package/dist/core/index.d.ts +19 -0
  78. package/dist/core/index.js +17 -0
  79. package/dist/core/infrastructure/listeners.d.ts +28 -0
  80. package/dist/core/infrastructure/listeners.js +34 -0
  81. package/dist/core/infrastructure/process.d.ts +56 -0
  82. package/dist/core/infrastructure/process.js +35 -0
  83. package/dist/core/services/binding.d.ts +6 -0
  84. package/dist/core/services/binding.js +16 -0
  85. package/dist/core/services/error-handlers.d.ts +11 -0
  86. package/dist/core/services/error-handlers.js +21 -0
  87. package/dist/core/services/execution.d.ts +66 -0
  88. package/dist/core/services/execution.js +216 -0
  89. package/dist/core/services/interceptors.d.ts +15 -0
  90. package/dist/core/services/interceptors.js +25 -0
  91. package/dist/core/services/listening.d.ts +17 -0
  92. package/dist/core/services/listening.js +25 -0
  93. package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
  94. package/dist/event_driven/adapters/memory-event-store.js +23 -0
  95. package/dist/event_driven/adapters/stored-queue.d.ts +67 -0
  96. package/dist/event_driven/adapters/stored-queue.js +192 -0
  97. package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
  98. package/dist/event_driven/adapters/sync-queue.js +19 -0
  99. package/dist/event_driven/domain/envelope.d.ts +28 -0
  100. package/dist/event_driven/domain/envelope.js +19 -0
  101. package/dist/event_driven/domain/queue.d.ts +40 -0
  102. package/dist/event_driven/domain/queue.js +1 -0
  103. package/dist/event_driven/domain/store.d.ts +56 -0
  104. package/dist/event_driven/domain/store.js +1 -0
  105. package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
  106. package/dist/event_driven/entrypoint/publisher.js +35 -0
  107. package/dist/event_driven/entrypoint/subscriber.d.ts +34 -0
  108. package/dist/event_driven/entrypoint/subscriber.js +51 -0
  109. package/dist/event_driven/index.d.ts +19 -0
  110. package/dist/event_driven/index.js +17 -0
  111. package/dist/index.d.ts +21 -38
  112. package/dist/index.js +21 -38
  113. package/dist/model/index.d.ts +10 -0
  114. package/dist/model/index.js +10 -0
  115. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  116. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  117. package/dist/model/{schema.js → services/schema.js} +1 -1
  118. package/dist/observability/adapters/console.d.ts +12 -0
  119. package/dist/observability/adapters/console.js +35 -0
  120. package/dist/observability/adapters/memory.d.ts +41 -0
  121. package/dist/observability/adapters/memory.js +49 -0
  122. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  123. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  124. package/dist/observability/domain/errors.d.ts +23 -0
  125. package/dist/observability/domain/errors.js +8 -0
  126. package/dist/observability/domain/logging.d.ts +44 -0
  127. package/dist/observability/domain/logging.js +1 -0
  128. package/dist/observability/domain/tracing.d.ts +49 -0
  129. package/dist/observability/domain/tracing.js +20 -0
  130. package/dist/observability/index.d.ts +16 -0
  131. package/dist/observability/index.js +4 -0
  132. package/dist/observability/infrastructure/registry.d.ts +21 -0
  133. package/dist/observability/infrastructure/registry.js +18 -0
  134. package/dist/observability/services/logging.d.ts +27 -0
  135. package/dist/observability/services/logging.js +106 -0
  136. package/dist/observability/services/outcome.d.ts +12 -0
  137. package/dist/observability/services/outcome.js +17 -0
  138. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  139. package/dist/testing/index.d.ts +3 -33
  140. package/dist/testing/index.js +5 -37
  141. package/package.json +16 -8
  142. package/dist/codegen/client.d.ts +0 -14
  143. package/dist/codegen/client.js +0 -201
  144. package/dist/codegen/domain.d.ts +0 -15
  145. package/dist/observability/ports.d.ts +0 -65
  146. package/dist/observability/ports.js +0 -21
  147. package/dist/services/bounded-context.d.ts +0 -79
  148. package/dist/services/bounded-context.js +0 -261
  149. package/dist/services/execution.d.ts +0 -63
  150. package/dist/services/feature.d.ts +0 -85
  151. package/dist/services/feature.js +0 -86
  152. package/dist/services/process.d.ts +0 -60
  153. package/dist/services/process.js +0 -57
  154. package/dist/services/screens.d.ts +0 -2
  155. package/dist/services/screens.js +0 -76
  156. /package/dist/{adapters → client/adapters}/parquet.js +0 -0
  157. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  158. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  159. /package/dist/client/{entity.js → domain/entity.js} +0 -0
  160. /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
  161. /package/dist/client/{transport.js → domain/transport.js} +0 -0
  162. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  163. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  164. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  165. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  166. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  167. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  168. /package/dist/codegen/{check.js → services/check.js} +0 -0
  169. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  170. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  171. /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
  172. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  173. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  174. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  175. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  176. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
@@ -0,0 +1,426 @@
1
+ import { contextAt, } from "../../context/entrypoint/context.js";
2
+ import { processContext, processStore, } from "../../context/infrastructure/process.js";
3
+ import { FeatureBus, optionsOf, } from "../domain/bus.js";
4
+ import { Command, DomainEvent, } from "../domain/dto.js";
5
+ import { ApplicationService, Feature, RESERVED_MEMBERS, } from "../domain/services.js";
6
+ import { processHearing } from "../infrastructure/listeners.js";
7
+ import { handled } from "../services/error-handlers.js";
8
+ import { executeService, failedAt } from "../services/execution.js";
9
+ import { announceTo, concerns, listenIn, } from "../services/listening.js";
10
+ import { observabilityNow } from "../../observability/infrastructure/registry.js";
11
+ import { boundedContextLogger, logHandled, } from "../../observability/services/logging.js";
12
+ function isExecutor(value) {
13
+ return (typeof value === "object" &&
14
+ value !== null &&
15
+ "execute" in value &&
16
+ typeof value.execute === "function");
17
+ }
18
+ function isEventClass(dto) {
19
+ return dto.prototype instanceof DomainEvent;
20
+ }
21
+ function nameOf(Service) {
22
+ const named = Service;
23
+ return String(named?.serviceName ?? named?.name ?? Service);
24
+ }
25
+ /**
26
+ * A bounded context: the bus of its Commands and domain events, as sincpro_framework's
27
+ * `UseFramework`.
28
+ *
29
+ * const project = new BoundedContext<ProjectDependencies>("project");
30
+ * project.addDependency("client", clientOf(transport));
31
+ * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
32
+ * project.feature(IssueClosed)(NotifyWatchers); // a subscriber of an event
33
+ * project.feature(CommandCancelIssue, { replaces: CancelIssue })(CancelAndArchive);
34
+ * project.addFeatureErrorHandler(toEmptyWhenMissing);
35
+ * project.expectErrors(IssueLocked); // refused, never reported
36
+ * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
37
+ * await project.execute(new IssueClosed("x")); // what its subscriber returns
38
+ * project.logger.info("Catalog preloaded");
39
+ *
40
+ * Context: registrations, dependencies, handlers and expected errors are snapshotted per
41
+ * generation; a call keeps the generation it started with, and what is added later answers from
42
+ * the next call — as the backend's `build_root_bus`. A domain event is executed as a Command is:
43
+ * one subscriber per bounded context, through the same pipeline; carrying it to every bounded
44
+ * context that hears it (a queue, an outbox) is the platform's.
45
+ */
46
+ export class BoundedContext {
47
+ name;
48
+ /** Its level of the context: what every use case of it runs with, under the process's. */
49
+ context;
50
+ /** Its own logger, named after it: for what it does outside a use case. */
51
+ logger;
52
+ registrations = new Map();
53
+ dependencyValues = new Map();
54
+ handlers = {
55
+ global: [],
56
+ feature: [],
57
+ application_service: [],
58
+ };
59
+ expected = [];
60
+ interceptors = [];
61
+ listeners = {
62
+ started: new Set(),
63
+ completed: new Set(),
64
+ failed: new Set(),
65
+ };
66
+ revision = 0;
67
+ builtRevision = -1;
68
+ built;
69
+ constructor(name) {
70
+ this.name = name;
71
+ if (name.trim() === "" || name.includes("/")) {
72
+ throw new TypeError("A bounded context needs a nonempty name without '/'");
73
+ }
74
+ this.context = contextAt(`bounded_context:${name}`, processStore);
75
+ this.logger = boundedContextLogger(name);
76
+ }
77
+ get deps() {
78
+ // The dependency interface is erased; the locator checks real registrations.
79
+ return new Proxy({}, {
80
+ get: (_target, key) => {
81
+ if (typeof key !== "string" || !this.dependencyValues.has(key)) {
82
+ throw new Error(`Dependency is not registered: ${String(key)}`);
83
+ }
84
+ return this.dependencyValues.get(key);
85
+ },
86
+ set: () => {
87
+ throw new Error("Dependencies are read-only; use addDependency");
88
+ },
89
+ deleteProperty: () => {
90
+ throw new Error("Dependencies cannot be deleted");
91
+ },
92
+ defineProperty: () => {
93
+ throw new Error("Dependencies are read-only");
94
+ },
95
+ setPrototypeOf: () => {
96
+ throw new Error("Dependencies are read-only");
97
+ },
98
+ });
99
+ }
100
+ addDependency(name, dependency, options) {
101
+ if (RESERVED_MEMBERS.has(name)) {
102
+ throw new Error(`Reserved dependency name: ${name}`);
103
+ }
104
+ const registered = this.dependencyValues.has(name);
105
+ if (registered && options === undefined) {
106
+ throw new Error(`Dependency already registered: ${name}`);
107
+ }
108
+ if (options && (!registered || this.dependencyValues.get(name) !== options.replaces)) {
109
+ throw new Error(`Dependency replacement does not match the registration: ${name}`);
110
+ }
111
+ this.dependencyValues.set(name, dependency);
112
+ this.revision++;
113
+ return this;
114
+ }
115
+ feature(dto, options) {
116
+ return this.registrar(dto, "feature", options);
117
+ }
118
+ appService(dto, options) {
119
+ return this.registrar(dto, "application_service", options);
120
+ }
121
+ /** The service registered now for a Command or an event class — what a `replaces` names. */
122
+ handlerOf(dto) {
123
+ return this.registrations.get(dto)?.Service;
124
+ }
125
+ /**
126
+ * The event class registered here under a wire name, or `undefined` when nothing here hears it
127
+ * — what a platform's queue rebuilds a stored event as, as the backend's `event_type(name)`.
128
+ */
129
+ eventTypeOf(name) {
130
+ for (const dto of this.registrations.keys()) {
131
+ if (isEventClass(dto) && dto.eventName === name)
132
+ return dto;
133
+ }
134
+ return undefined;
135
+ }
136
+ /**
137
+ * Runs `interceptor` around every execution of these classes — of every one when none is
138
+ * named —, outermost first in the order registered. It may look, veto (throw), adjust what it
139
+ * passes on (of the same class) or answer by itself. Registered after a call, it wraps from
140
+ * the next one.
141
+ */
142
+ interceptor(...dtos) {
143
+ return (interceptor) => {
144
+ if (typeof interceptor !== "function")
145
+ throw new TypeError("An interceptor is a function");
146
+ // Registered for these classes only, it is called only with them.
147
+ this.interceptors.push({ what: interceptor, dtos });
148
+ this.revision++;
149
+ return interceptor;
150
+ };
151
+ }
152
+ /** Stops `interceptor` wherever it was registered; from the next call. */
153
+ withoutInterceptor(interceptor) {
154
+ const kept = this.interceptors.filter((one) => one.what !== interceptor);
155
+ this.interceptors.splice(0, this.interceptors.length, ...kept);
156
+ this.revision++;
157
+ return this;
158
+ }
159
+ /** Hears every run of these classes (every one when none is named) begin. Answers how to stop. */
160
+ onStart(listener, ...dtos) {
161
+ return listenIn(this.listeners.started, listener, dtos);
162
+ }
163
+ /** Hears every run of these classes whose service answered — not an answer a handler gave. */
164
+ onCompletion(listener, ...dtos) {
165
+ return listenIn(this.listeners.completed, listener, dtos);
166
+ }
167
+ /** Hears every failure that escapes a call — after every handler —, once, where it failed. */
168
+ onFailure(listener, ...dtos) {
169
+ return listenIn(this.listeners.failed, listener, dtos);
170
+ }
171
+ /**
172
+ * Wires a component into this bounded context — its dependencies, services, interceptors,
173
+ * listeners, handlers — in one place: a module of an application, a plugin. `fresh()` copies
174
+ * all it registered.
175
+ */
176
+ extend(extension) {
177
+ extension(this);
178
+ return this;
179
+ }
180
+ /** Answers a failure that reaches `execute` (or a scope's): after every layer's handlers. */
181
+ addGlobalErrorHandler(handler) {
182
+ return this.addErrorHandler("global", handler);
183
+ }
184
+ /** Answers a Feature's failure, wherever it was executed from. */
185
+ addFeatureErrorHandler(handler) {
186
+ return this.addErrorHandler("feature", handler);
187
+ }
188
+ /** Answers an ApplicationService's failure. */
189
+ addAppServiceErrorHandler(handler) {
190
+ return this.addErrorHandler("application_service", handler);
191
+ }
192
+ /**
193
+ * Errors of these classes are the business answering, not a fault: the run ends `refused`,
194
+ * its span is no error and nothing is reported. They are still thrown — or handled.
195
+ */
196
+ expectErrors(...types) {
197
+ for (const type of types) {
198
+ if (typeof type !== "function")
199
+ throw new TypeError("An expected error is a class");
200
+ if (!this.expected.includes(type))
201
+ this.expected.push(type);
202
+ }
203
+ this.revision++;
204
+ return this;
205
+ }
206
+ execute(executable, second) {
207
+ // As with Python's return_type, the answer is not validated at runtime.
208
+ return this.generation().root(executable, {}, optionsOf(second));
209
+ }
210
+ scoped(context) {
211
+ const scope = { ...context };
212
+ return new FeatureBus((executable, options) => this.generation().root(executable, scope, options));
213
+ }
214
+ buildRootBus() {
215
+ this.generation();
216
+ }
217
+ fresh() {
218
+ const fresh = new BoundedContext(this.name);
219
+ for (const [dto, registration] of this.registrations) {
220
+ fresh.registrations.set(dto, registration);
221
+ }
222
+ for (const [name, dependency] of this.dependencyValues) {
223
+ fresh.dependencyValues.set(name, dependency);
224
+ }
225
+ for (const where of ["global", "feature", "application_service"]) {
226
+ fresh.handlers[where].push(...this.handlers[where]);
227
+ }
228
+ fresh.expected.push(...this.expected);
229
+ fresh.interceptors.push(...this.interceptors);
230
+ for (const moment of ["started", "completed", "failed"]) {
231
+ for (const one of this.listeners[moment])
232
+ fresh.listeners[moment].add(one);
233
+ }
234
+ fresh.revision = this.revision;
235
+ return fresh;
236
+ }
237
+ /** What escaped a call, told once, from where it first failed. */
238
+ escaped(executable, error, expected) {
239
+ const at = failedAt(error);
240
+ let outcome = "failed";
241
+ try {
242
+ outcome =
243
+ at?.outcome ?? (expected(error) ? "refused" : observabilityNow().outcomeOf(error));
244
+ }
245
+ catch {
246
+ // An outcome that cannot be told is a failure.
247
+ }
248
+ const heard = {
249
+ executable: at?.executable ?? executable,
250
+ execution: at?.execution,
251
+ layer: at?.layer,
252
+ error,
253
+ outcome,
254
+ };
255
+ announceTo(this.listeners.failed, heard);
256
+ announceTo(processHearing().failed, heard);
257
+ }
258
+ addErrorHandler(where, handler) {
259
+ if (typeof handler !== "function")
260
+ throw new TypeError("An error handler is a function");
261
+ this.handlers[where].push(handler);
262
+ this.revision++;
263
+ return this;
264
+ }
265
+ registrar(dto, layer, options = {}) {
266
+ return (Service, decorator) => {
267
+ const register = () => this.registerService(dto, Service, layer, options.replaces);
268
+ // Standard decorators run before static fields have initialized.
269
+ if (decorator)
270
+ decorator.addInitializer(register);
271
+ else
272
+ register();
273
+ return Service;
274
+ };
275
+ }
276
+ /**
277
+ * 1. The service is of the layer it is registered in, for a Command or a domain event class.
278
+ * 2. The class has no service yet — or, with `replaces`, the one it has is that one.
279
+ * 3. Its `serviceName` is given and no other service has it; an event's `eventName` is given
280
+ * and no other event class here has it — the name means one event.
281
+ * Final: registered, said on its spans when it replaced another; the next call answers with it.
282
+ */
283
+ registerService(dto, Service, layer, replaces) {
284
+ const Base = layer === "feature" ? Feature : ApplicationService;
285
+ if (!(Service.prototype instanceof Base) ||
286
+ (layer === "feature" && Service.prototype instanceof ApplicationService)) {
287
+ throw new TypeError(`The service is not an ${layer}`);
288
+ }
289
+ const event = isEventClass(dto);
290
+ if (!event && !(dto.prototype instanceof Command)) {
291
+ throw new TypeError("A service is registered for a Command or a DomainEvent class");
292
+ }
293
+ const current = this.registrations.get(dto);
294
+ if (replaces !== undefined && current?.Service !== replaces) {
295
+ throw new Error(`Replacement does not match the registration: ${nameOf(replaces)}`);
296
+ }
297
+ if (replaces === undefined && current) {
298
+ throw new Error("Command already registered: one executor across both layers");
299
+ }
300
+ const name = Service.serviceName;
301
+ if (typeof name !== "string" || name.trim() === "") {
302
+ throw new Error("A service needs a nonempty registered name");
303
+ }
304
+ for (const [other, registration] of this.registrations) {
305
+ if (registration !== current && registration.name === name) {
306
+ throw new Error(`Service name already registered: ${name}`);
307
+ }
308
+ if (event &&
309
+ other !== dto &&
310
+ isEventClass(other) &&
311
+ other.eventName === dto.eventName) {
312
+ throw new Error(`Event name already registered: ${dto.eventName}`);
313
+ }
314
+ }
315
+ if (event && (typeof dto.eventName !== "string" || dto.eventName.trim() === "")) {
316
+ throw new Error("A domain event needs a nonempty eventName");
317
+ }
318
+ this.registrations.set(dto, {
319
+ name,
320
+ layer,
321
+ Service,
322
+ ...(current ? { replaces: current.name } : {}),
323
+ });
324
+ this.revision++;
325
+ }
326
+ generation() {
327
+ if (this.built && this.builtRevision === this.revision)
328
+ return this.built;
329
+ const revision = this.revision;
330
+ const built = this.buildGeneration();
331
+ this.built = built;
332
+ this.builtRevision = revision;
333
+ return built;
334
+ }
335
+ buildGeneration() {
336
+ const registrations = new Map(this.registrations);
337
+ const dependencies = new Map(this.dependencyValues);
338
+ const handlers = {
339
+ global: [...this.handlers.global],
340
+ feature: [...this.handlers.feature],
341
+ application_service: [...this.handlers.application_service],
342
+ };
343
+ const expectedTypes = [...this.expected];
344
+ const expected = (error) => expectedTypes.some((type) => error instanceof type);
345
+ const interceptors = [...this.interceptors];
346
+ const everyContext = processHearing();
347
+ const announce = {
348
+ started: (heard) => {
349
+ announceTo(this.listeners.started, heard);
350
+ announceTo(everyContext.started, heard);
351
+ },
352
+ completed: (heard) => {
353
+ announceTo(this.listeners.completed, heard);
354
+ announceTo(everyContext.completed, heard);
355
+ },
356
+ };
357
+ const instances = new Map();
358
+ for (const registration of registrations.values()) {
359
+ const instance = new registration.Service();
360
+ if (!isExecutor(instance)) {
361
+ throw new Error(`Registered service is unavailable: ${registration.name}`);
362
+ }
363
+ instances.set(registration, instance);
364
+ }
365
+ const dispatch = async (executable, parent, featureOnly = false, scope = {}, options = {}) => {
366
+ // Exact constructor identity: a subclass of a registered class is another one.
367
+ const target = registrations.get(Object.getPrototypeOf(executable)?.constructor);
368
+ if (!target)
369
+ throw new Error("No executor registered for this command class");
370
+ if (featureOnly && target.layer !== "feature") {
371
+ throw new Error("An ApplicationService may compose only Features");
372
+ }
373
+ const service = instances.get(target);
374
+ if (!service) {
375
+ throw new Error(`Registered service is unavailable: ${target.name}`);
376
+ }
377
+ const attributes = {
378
+ ...(executable instanceof DomainEvent ? { "sincpro.event": executable.name } : {}),
379
+ ...(target.replaces ? { "sincpro.replaces": target.replaces } : {}),
380
+ };
381
+ return executeService(service, executable, {
382
+ boundedContext: this.name,
383
+ name: target.name,
384
+ layer: target.layer,
385
+ attributes,
386
+ dependencies,
387
+ context: () => ({ ...processContext.values, ...this.context.values, ...scope }),
388
+ ...(parent ? { parent } : {}),
389
+ // Only an event comes from outside with a cause; a child continues its parent.
390
+ ...(options.cause && executable instanceof DomainEvent && !parent
391
+ ? { cause: options.cause }
392
+ : {}),
393
+ ...(options.signal ? { signal: options.signal } : {}),
394
+ featureBus: (execution) => new FeatureBus((child, options) => dispatch(child, execution, true, {}, options)),
395
+ expected,
396
+ handlers: handlers[target.layer],
397
+ interceptors: interceptors
398
+ .filter((one) => concerns(one.dtos, executable))
399
+ .map((one) => one.what),
400
+ announce,
401
+ });
402
+ };
403
+ return {
404
+ root: async (executable, scope, options) => {
405
+ try {
406
+ return await dispatch(executable, undefined, false, scope, options);
407
+ }
408
+ catch (error) {
409
+ let escaping = error;
410
+ if (handlers.global.length > 0) {
411
+ try {
412
+ const answer = await handled(handlers.global, error);
413
+ logHandled("bounded_context", { boundedContext: this.name });
414
+ return answer;
415
+ }
416
+ catch (thrown) {
417
+ escaping = thrown;
418
+ }
419
+ }
420
+ this.escaped(executable, escaping, expected);
421
+ throw escaping;
422
+ }
423
+ },
424
+ };
425
+ }
426
+ }
@@ -0,0 +1,6 @@
1
+ import type { Transport } from "../../client/domain/transport.js";
2
+ /**
3
+ * Screen requests enter the same command pipeline as application services. A generated
4
+ * client injected into a Feature uses its raw transport instead, continuing that execution.
5
+ */
6
+ export declare function screensTransport(transport: Transport, name?: string): Transport;
@@ -0,0 +1,136 @@
1
+ import { Command } from "../domain/dto.js";
2
+ import { Feature } from "../domain/services.js";
3
+ import { BoundedContext } from "./bounded-context.js";
4
+ class List extends Command {
5
+ route;
6
+ criteria;
7
+ signal;
8
+ options;
9
+ constructor(route, criteria, signal, options) {
10
+ super();
11
+ this.route = route;
12
+ this.criteria = criteria;
13
+ this.signal = signal;
14
+ this.options = options;
15
+ }
16
+ }
17
+ class Get extends Command {
18
+ route;
19
+ signal;
20
+ options;
21
+ constructor(route, signal, options) {
22
+ super();
23
+ this.route = route;
24
+ this.signal = signal;
25
+ this.options = options;
26
+ }
27
+ }
28
+ class Read extends Command {
29
+ route;
30
+ signal;
31
+ options;
32
+ constructor(route, signal, options) {
33
+ super();
34
+ this.route = route;
35
+ this.signal = signal;
36
+ this.options = options;
37
+ }
38
+ }
39
+ class Export extends Command {
40
+ route;
41
+ signal;
42
+ options;
43
+ constructor(route, signal, options) {
44
+ super();
45
+ this.route = route;
46
+ this.signal = signal;
47
+ this.options = options;
48
+ }
49
+ }
50
+ class Send extends Command {
51
+ method;
52
+ route;
53
+ body;
54
+ signal;
55
+ options;
56
+ constructor(method, route, body, signal, options) {
57
+ super();
58
+ this.method = method;
59
+ this.route = route;
60
+ this.body = body;
61
+ this.signal = signal;
62
+ this.options = options;
63
+ }
64
+ }
65
+ class EntityList extends Feature {
66
+ static serviceName = "EntityReads.list";
67
+ execute(dto) {
68
+ this.span.set({ "http.route": dto.route });
69
+ return this.transport.list(dto.route, dto.criteria, dto.signal, dto.options);
70
+ }
71
+ }
72
+ class EntityGet extends Feature {
73
+ static serviceName = "EntityReads.one";
74
+ execute(dto) {
75
+ this.span.set({ "http.route": dto.route });
76
+ return this.transport.one(dto.route, dto.signal, dto.options);
77
+ }
78
+ }
79
+ class EntityRead extends Feature {
80
+ static serviceName = "EntityReads.read";
81
+ execute(dto) {
82
+ this.span.set({ "http.route": dto.route });
83
+ const transport = this.transport;
84
+ if (!transport.read) {
85
+ return Promise.reject(new Error(`this transport cannot read ${dto.route}: it has no read()`));
86
+ }
87
+ return transport.read(dto.route, dto.signal, dto.options);
88
+ }
89
+ }
90
+ class EntityExport extends Feature {
91
+ static serviceName = "EntityReads.download";
92
+ execute(dto) {
93
+ this.span.set({ "http.route": dto.route });
94
+ const transport = this.transport;
95
+ if (!transport.download) {
96
+ return Promise.reject(new Error(`this transport cannot download ${dto.route}: it has no download()`));
97
+ }
98
+ return transport.download(dto.route, dto.signal, dto.options);
99
+ }
100
+ }
101
+ class EntitySend extends Feature {
102
+ static serviceName = "EntityWrites.send";
103
+ execute(dto) {
104
+ this.span.set({ "http.method": dto.method, "http.route": dto.route });
105
+ return this.transport.send(dto.method, dto.route, dto.body, dto.signal, dto.options);
106
+ }
107
+ }
108
+ const MADE = new WeakSet();
109
+ /**
110
+ * Screen requests enter the same command pipeline as application services. A generated
111
+ * client injected into a Feature uses its raw transport instead, continuing that execution.
112
+ */
113
+ export function screensTransport(transport, name = "screens") {
114
+ if (MADE.has(transport))
115
+ return transport;
116
+ const screens = new BoundedContext(name);
117
+ screens.addDependency("transport", transport);
118
+ screens.feature(List)(EntityList);
119
+ screens.feature(Get)(EntityGet);
120
+ screens.feature(Read)(EntityRead);
121
+ screens.feature(Export)(EntityExport);
122
+ screens.feature(Send)(EntitySend);
123
+ const through = {
124
+ list: (route, criteria, signal, options) => screens.execute(new List(route, criteria, signal, options)),
125
+ one: (route, signal, options) => screens.execute(new Get(route, signal, options)),
126
+ send: (method, route, body, signal, options) => screens.execute(new Send(method, route, body, signal, options)),
127
+ };
128
+ if (transport.read) {
129
+ through.read = (route, signal, options) => screens.execute(new Read(route, signal, options));
130
+ }
131
+ if (transport.download) {
132
+ through.download = (route, signal, options) => screens.execute(new Export(route, signal, options));
133
+ }
134
+ MADE.add(through);
135
+ return through;
136
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The application core (`./core`): the backend's application abstractions — class Commands and
3
+ * Responses, Features and ApplicationServices, a bounded context that is their bus, its error
4
+ * handlers — and the process they run in.
5
+ *
6
+ * domain/ DTOs, the bus, the service bases, the error-handler contract
7
+ * services/ the execution pipeline, dependency binding, the handler chain
8
+ * entrypoint/ `BoundedContext`, `screensTransport`
9
+ * infrastructure/ `configureProcess`, `configureLogger`
10
+ */
11
+ export { type CommandBus, FeatureBus } from "./domain/bus.js";
12
+ export { Command, DataTransferObject, DomainEvent, type EventClass, Response, } from "./domain/dto.js";
13
+ export type { ErrorClass, ErrorHandler } from "./domain/errors.js";
14
+ export type { CallNext, Cause, ExecuteOptions, ExecutionCompleted, ExecutionFailed, ExecutionStarted, Extension, InterceptedRun, Interceptor, Listener, PipelineLayer, } from "./domain/pipeline.js";
15
+ export { ApplicationService, Feature } from "./domain/services.js";
16
+ export { BoundedContext, type Registrar, type RegistrationOptions, } from "./entrypoint/bounded-context.js";
17
+ export { screensTransport } from "./entrypoint/screens.js";
18
+ export { processListeners } from "./infrastructure/listeners.js";
19
+ export { configureLogger, configureProcess, type LoggerConfiguration, type ProcessOptions, } from "./infrastructure/process.js";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The application core (`./core`): the backend's application abstractions — class Commands and
3
+ * Responses, Features and ApplicationServices, a bounded context that is their bus, its error
4
+ * handlers — and the process they run in.
5
+ *
6
+ * domain/ DTOs, the bus, the service bases, the error-handler contract
7
+ * services/ the execution pipeline, dependency binding, the handler chain
8
+ * entrypoint/ `BoundedContext`, `screensTransport`
9
+ * infrastructure/ `configureProcess`, `configureLogger`
10
+ */
11
+ export { FeatureBus } from "./domain/bus.js";
12
+ export { Command, DataTransferObject, DomainEvent, Response, } from "./domain/dto.js";
13
+ export { ApplicationService, Feature } from "./domain/services.js";
14
+ export { BoundedContext, } from "./entrypoint/bounded-context.js";
15
+ export { screensTransport } from "./entrypoint/screens.js";
16
+ export { processListeners } from "./infrastructure/listeners.js";
17
+ export { configureLogger, configureProcess, } from "./infrastructure/process.js";
@@ -0,0 +1,28 @@
1
+ import type { ExecutionCompleted, ExecutionFailed, ExecutionStarted, Listener } from "../domain/pipeline.js";
2
+ import { type ExecutableClass, type Hearing } from "../services/listening.js";
3
+ /**
4
+ * Who hears every bounded context of this process — the backend's process listeners: an
5
+ * activity indicator, a toast, an audit that must not be wired into each context.
6
+ *
7
+ * const stop = processListeners.onFailure((failed) => toast(failed.execution?.useCase));
8
+ *
9
+ * Context: a bounded context's own listeners (`project.onStart`) hear it alone; these hear all of
10
+ * them, after its own. They live as long as the process — `configureProcess` does not clear
11
+ * them —, so whoever adds one keeps the function that stops it.
12
+ */
13
+ declare const hearing: {
14
+ started: Set<Hearing<Listener<ExecutionStarted>>>;
15
+ completed: Set<Hearing<Listener<ExecutionCompleted>>>;
16
+ failed: Set<Hearing<Listener<ExecutionFailed>>>;
17
+ };
18
+ /** @internal — what the bounded contexts announce to, besides their own listeners. */
19
+ export declare function processHearing(): typeof hearing;
20
+ export declare const processListeners: {
21
+ /** Hears every run of every bounded context begin. Answers how to stop. */
22
+ onStart(listener: Listener<ExecutionStarted>, ...dtos: ExecutableClass[]): () => void;
23
+ /** Hears every service that answered, in every bounded context. */
24
+ onCompletion(listener: Listener<ExecutionCompleted>, ...dtos: ExecutableClass[]): () => void;
25
+ /** Hears every failure that escapes a call, once, where it failed. */
26
+ onFailure(listener: Listener<ExecutionFailed>, ...dtos: ExecutableClass[]): () => void;
27
+ };
28
+ export {};
@@ -0,0 +1,34 @@
1
+ import { listenIn, } from "../services/listening.js";
2
+ /**
3
+ * Who hears every bounded context of this process — the backend's process listeners: an
4
+ * activity indicator, a toast, an audit that must not be wired into each context.
5
+ *
6
+ * const stop = processListeners.onFailure((failed) => toast(failed.execution?.useCase));
7
+ *
8
+ * Context: a bounded context's own listeners (`project.onStart`) hear it alone; these hear all of
9
+ * them, after its own. They live as long as the process — `configureProcess` does not clear
10
+ * them —, so whoever adds one keeps the function that stops it.
11
+ */
12
+ const hearing = {
13
+ started: new Set(),
14
+ completed: new Set(),
15
+ failed: new Set(),
16
+ };
17
+ /** @internal — what the bounded contexts announce to, besides their own listeners. */
18
+ export function processHearing() {
19
+ return hearing;
20
+ }
21
+ export const processListeners = {
22
+ /** Hears every run of every bounded context begin. Answers how to stop. */
23
+ onStart(listener, ...dtos) {
24
+ return listenIn(hearing.started, listener, dtos);
25
+ },
26
+ /** Hears every service that answered, in every bounded context. */
27
+ onCompletion(listener, ...dtos) {
28
+ return listenIn(hearing.completed, listener, dtos);
29
+ },
30
+ /** Hears every failure that escapes a call, once, where it failed. */
31
+ onFailure(listener, ...dtos) {
32
+ return listenIn(hearing.failed, listener, dtos);
33
+ },
34
+ };