@sincpro/client-framework 0.5.0 → 1.0.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 (134) hide show
  1. package/AGENTS.md +54 -25
  2. package/MANIFESTO.md +59 -39
  3. package/README.md +329 -328
  4. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  5. package/dist/client/{memory.js → adapters/memory.js} +7 -5
  6. package/dist/client/adapters/parquet.d.ts +50 -0
  7. package/dist/client/adapters/parquet.js +151 -0
  8. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +7 -1
  9. package/dist/client/{rest.js → adapters/rest.js} +48 -3
  10. package/dist/client/{entity.d.ts → domain/entity.d.ts} +22 -11
  11. package/dist/client/{entity.js → domain/entity.js} +16 -1
  12. package/dist/client/{transport.d.ts → domain/transport.d.ts} +37 -1
  13. package/dist/client/{transport.js → domain/transport.js} +6 -0
  14. package/dist/client/index.d.ts +16 -0
  15. package/dist/client/index.js +16 -0
  16. package/dist/client/services/propagation.d.ts +32 -0
  17. package/dist/{services/execution.js → client/services/propagation.js} +8 -21
  18. package/dist/client/{retry.js → services/retry.js} +1 -1
  19. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +21 -2
  20. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  21. package/dist/codegen/{names.js → domain/names.js} +3 -3
  22. package/dist/codegen/{cli.js → entrypoint/cli.js} +9 -8
  23. package/dist/codegen/index.d.ts +7 -7
  24. package/dist/codegen/index.js +5 -10
  25. package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
  26. package/dist/codegen/{client.js → services/client.js} +34 -5
  27. package/dist/codegen/services/domain.d.ts +20 -0
  28. package/dist/codegen/{domain.js → services/domain.js} +121 -23
  29. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -2
  30. package/dist/codegen/{entities.js → services/entities.js} +34 -4
  31. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  32. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
  33. package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
  34. package/dist/common/collection.d.ts +110 -0
  35. package/dist/common/collection.js +169 -0
  36. package/dist/common/index.d.ts +10 -0
  37. package/dist/common/index.js +10 -0
  38. package/dist/context/adapters/memory.d.ts +6 -0
  39. package/dist/context/adapters/memory.js +34 -0
  40. package/dist/context/adapters/plain-codec.d.ts +3 -0
  41. package/dist/context/adapters/plain-codec.js +16 -0
  42. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  43. package/dist/context/{codec.js → domain/codec.js} +0 -14
  44. package/dist/context/domain/execution.d.ts +33 -0
  45. package/dist/context/domain/execution.js +11 -0
  46. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  47. package/dist/context/{store.js → domain/store.js} +1 -34
  48. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  49. package/dist/context/index.d.ts +17 -0
  50. package/dist/context/index.js +15 -0
  51. package/dist/context/infrastructure/process.d.ts +13 -0
  52. package/dist/context/infrastructure/process.js +35 -0
  53. package/dist/core/domain/bus.d.ts +18 -0
  54. package/dist/core/domain/bus.js +18 -0
  55. package/dist/core/domain/dto.d.ts +32 -0
  56. package/dist/core/domain/dto.js +20 -0
  57. package/dist/core/domain/errors.d.ts +18 -0
  58. package/dist/core/domain/errors.js +1 -0
  59. package/dist/core/domain/services.d.ts +37 -0
  60. package/dist/core/domain/services.js +28 -0
  61. package/dist/core/entrypoint/bounded-context.d.ts +70 -0
  62. package/dist/core/entrypoint/bounded-context.js +252 -0
  63. package/dist/core/entrypoint/screens.d.ts +6 -0
  64. package/dist/core/entrypoint/screens.js +136 -0
  65. package/dist/core/index.d.ts +17 -0
  66. package/dist/core/index.js +16 -0
  67. package/dist/core/infrastructure/process.d.ts +56 -0
  68. package/dist/core/infrastructure/process.js +35 -0
  69. package/dist/core/services/binding.d.ts +6 -0
  70. package/dist/core/services/binding.js +13 -0
  71. package/dist/core/services/error-handlers.d.ts +11 -0
  72. package/dist/core/services/error-handlers.js +21 -0
  73. package/dist/core/services/execution.d.ts +35 -0
  74. package/dist/core/services/execution.js +137 -0
  75. package/dist/index.d.ts +16 -36
  76. package/dist/index.js +16 -36
  77. package/dist/model/{meta.d.ts → domain/meta.d.ts} +1 -0
  78. package/dist/model/index.d.ts +10 -0
  79. package/dist/model/index.js +10 -0
  80. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  81. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  82. package/dist/model/{schema.js → services/schema.js} +1 -1
  83. package/dist/observability/adapters/console.d.ts +12 -0
  84. package/dist/observability/adapters/console.js +35 -0
  85. package/dist/observability/adapters/memory.d.ts +41 -0
  86. package/dist/observability/adapters/memory.js +49 -0
  87. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  88. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  89. package/dist/observability/domain/errors.d.ts +23 -0
  90. package/dist/observability/domain/errors.js +8 -0
  91. package/dist/observability/domain/logging.d.ts +42 -0
  92. package/dist/observability/domain/logging.js +1 -0
  93. package/dist/observability/domain/tracing.d.ts +49 -0
  94. package/dist/observability/domain/tracing.js +20 -0
  95. package/dist/observability/index.d.ts +16 -0
  96. package/dist/observability/index.js +4 -0
  97. package/dist/observability/infrastructure/registry.d.ts +21 -0
  98. package/dist/observability/infrastructure/registry.js +18 -0
  99. package/dist/observability/services/logging.d.ts +27 -0
  100. package/dist/observability/services/logging.js +105 -0
  101. package/dist/observability/services/outcome.d.ts +12 -0
  102. package/dist/observability/services/outcome.js +20 -0
  103. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  104. package/dist/testing/index.d.ts +3 -33
  105. package/dist/testing/index.js +5 -37
  106. package/package.json +25 -8
  107. package/dist/codegen/domain.d.ts +0 -15
  108. package/dist/observability/ports.d.ts +0 -65
  109. package/dist/observability/ports.js +0 -21
  110. package/dist/services/bounded-context.d.ts +0 -79
  111. package/dist/services/bounded-context.js +0 -261
  112. package/dist/services/execution.d.ts +0 -63
  113. package/dist/services/feature.d.ts +0 -85
  114. package/dist/services/feature.js +0 -86
  115. package/dist/services/process.d.ts +0 -60
  116. package/dist/services/process.js +0 -57
  117. package/dist/services/screens.d.ts +0 -2
  118. package/dist/services/screens.js +0 -67
  119. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  120. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  121. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  122. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  123. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  124. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  125. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  126. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  127. /package/dist/codegen/{check.js → services/check.js} +0 -0
  128. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  129. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  130. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  131. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  132. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  133. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  134. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The context: the levels a call sees (the process, its bounded context, a scope), where they
3
+ * live (`ContextStore`, a port each platform implements), what travels with a request, and the
4
+ * execution a run carries.
5
+ *
6
+ * domain/ the ports and rules: `ContextStore`, `ContextCodec`, what travels, `Execution`
7
+ * adapters/ the implementations this package gives: memory, the plain codec
8
+ * entrypoint/ `Context`: one door over a level
9
+ * infrastructure/ the process's store and its level (`processContext`)
10
+ */
11
+ export { memoryContexts } from "./adapters/memory.js";
12
+ export { plainCodec } from "./adapters/plain-codec.js";
13
+ export { CONTEXT_HEADER, travelling, travels, } from "./domain/codec.js";
14
+ export { BIND, } from "./domain/execution.js";
15
+ export { processContext } from "./infrastructure/process.js";
@@ -0,0 +1,13 @@
1
+ import type { ContextStore } from "../domain/store.js";
2
+ import { type Context, type StoreSource } from "../entrypoint/context.js";
3
+ /** @internal — `configureProcess` is the way in. */
4
+ export declare function replaceProcessStore(store: ContextStore): void;
5
+ /** The process's store as every level of the context reads it — the process's and each bounded context's. */
6
+ export declare const processStore: StoreSource;
7
+ /**
8
+ * The process's level of the context: what every use case of every bounded context runs with —
9
+ * the tenant, the company, the language.
10
+ *
11
+ * processContext.set({ tenant_id: "acme" });
12
+ */
13
+ export declare const processContext: Context;
@@ -0,0 +1,35 @@
1
+ import { memoryContexts } from "../adapters/memory.js";
2
+ import { contextAt, } from "../entrypoint/context.js";
3
+ /**
4
+ * Where this process keeps its context: the store `configureProcess` chose (`memoryContexts()`
5
+ * until it chooses), read on every call, never at import.
6
+ *
7
+ * Context: a level made before `main` configures the process (`project` is defined when its
8
+ * module loads) still reads the store chosen later; whoever listens moves to the new store and
9
+ * hears the change.
10
+ */
11
+ let current = memoryContexts();
12
+ const replacing = new Set();
13
+ /** @internal — `configureProcess` is the way in. */
14
+ export function replaceProcessStore(store) {
15
+ const before = current;
16
+ current = store;
17
+ if (current !== before)
18
+ for (const listener of [...replacing])
19
+ listener();
20
+ }
21
+ /** The process's store as every level of the context reads it — the process's and each bounded context's. */
22
+ export const processStore = {
23
+ current: () => current,
24
+ replaced(listener) {
25
+ replacing.add(listener);
26
+ return () => void replacing.delete(listener);
27
+ },
28
+ };
29
+ /**
30
+ * The process's level of the context: what every use case of every bounded context runs with —
31
+ * the tenant, the company, the language.
32
+ *
33
+ * processContext.set({ tenant_id: "acme" });
34
+ */
35
+ export const processContext = contextAt("process", processStore);
@@ -0,0 +1,18 @@
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. */
3
+ export interface CommandBus {
4
+ execute<R>(command: Command<R>, returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
5
+ }
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>;
10
+ /**
11
+ * What an ApplicationService composes its context's Features through (`this.featureBus`): the
12
+ * same pipeline, as a child of the running execution, and Features only.
13
+ */
14
+ export declare class FeatureBus implements CommandBus {
15
+ private readonly dispatch;
16
+ constructor(dispatch: Dispatch);
17
+ execute<R>(command: Command<R>, _returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
18
+ }
@@ -0,0 +1,18 @@
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));
5
+ }
6
+ /**
7
+ * What an ApplicationService composes its context's Features through (`this.featureBus`): the
8
+ * same pipeline, as a child of the running execution, and Features only.
9
+ */
10
+ export class FeatureBus {
11
+ dispatch;
12
+ constructor(dispatch) {
13
+ this.dispatch = dispatch;
14
+ }
15
+ execute(command, _returnType) {
16
+ return executeTyped(this.dispatch, command);
17
+ }
18
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The DTOs of the application layer, as sincpro_framework's `DataTransferObject`: a Command asks
3
+ * for work and names, by its type, the Response it answers.
4
+ *
5
+ * class CommandCancelIssue extends Command<ResponseCancelIssue> {
6
+ * constructor(readonly issue_id: string) { super(); }
7
+ * }
8
+ * const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
9
+ *
10
+ * Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
11
+ * `execute` infers it with no second argument. The association is type-only: absent from a
12
+ * serialized DTO, not a runtime validator. A generated DTO is a remote contract (a type), not one
13
+ * of these: a local Command may implement it.
14
+ */
15
+ export declare abstract class DataTransferObject {
16
+ protected readonly dtoBrand: void;
17
+ }
18
+ export declare abstract class Response extends DataTransferObject {
19
+ }
20
+ declare const responseType: unique symbol;
21
+ export declare abstract class Command<R> extends DataTransferObject {
22
+ /** Type-only association: absent from serialized DTOs and not a runtime validator. */
23
+ readonly [responseType]: R;
24
+ }
25
+ export type AnyCommand = Command<unknown>;
26
+ /** What a Command answers. */
27
+ export type Answer<C extends AnyCommand> = C[typeof responseType];
28
+ /** A Command's class: what a service is registered for, by exact constructor identity. */
29
+ export type CommandClass<C extends AnyCommand> = abstract new (...args: never[]) => C;
30
+ /** A Response's class: the backend's `return_type`, accepted for the same reading. */
31
+ export type ResponseClass<R> = abstract new (...args: never[]) => R;
32
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The DTOs of the application layer, as sincpro_framework's `DataTransferObject`: a Command asks
3
+ * for work and names, by its type, the Response it answers.
4
+ *
5
+ * class CommandCancelIssue extends Command<ResponseCancelIssue> {
6
+ * constructor(readonly issue_id: string) { super(); }
7
+ * }
8
+ * const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
9
+ *
10
+ * Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
11
+ * `execute` infers it with no second argument. The association is type-only: absent from a
12
+ * serialized DTO, not a runtime validator. A generated DTO is a remote contract (a type), not one
13
+ * of these: a local Command may implement it.
14
+ */
15
+ export class DataTransferObject {
16
+ }
17
+ export class Response extends DataTransferObject {
18
+ }
19
+ export class Command extends DataTransferObject {
20
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What answers a use case's failure instead of raising it — sincpro_framework's error handlers.
3
+ *
4
+ * project.addFeatureErrorHandler((error) => {
5
+ * if (error instanceof IssueNotFound) return new ResponseEmpty(); // the answer
6
+ * throw error; // the next handler
7
+ * });
8
+ *
9
+ * Context: three chains per bounded context — global (around `project.execute`), feature and
10
+ * application service —, each run first-registered-first. A handler that throws hands what it
11
+ * threw to the next; the last one's throw reaches the caller. The failure was already recorded
12
+ * where it happened (its span, its log line and, when unexpected, its report): a handler decides
13
+ * the answer, not whether it is observed. A handler that only watches must end with `throw`:
14
+ * what it returns is the answer.
15
+ */
16
+ export type ErrorHandler = (error: unknown) => unknown;
17
+ /** An error's class, as `expectErrors` names it. */
18
+ export type ErrorClass = abstract new (...args: never[]) => unknown;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,37 @@
1
+ import type { ContextValues } from "../../context/domain/store.js";
2
+ import type { FeatureBus } from "./bus.js";
3
+ import type { Command } from "./dto.js";
4
+ import type { LayerLogger } from "../../observability/domain/logging.js";
5
+ import type { Span } from "../../observability/domain/tracing.js";
6
+ /**
7
+ * The services of the application layer, as sincpro_framework's: a Feature executes one
8
+ * registered Command; an ApplicationService composes its context's Features.
9
+ *
10
+ * @project.feature(CommandCancelIssue)
11
+ * class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
12
+ * static readonly serviceName = "CancelIssue";
13
+ * async execute(command: CommandCancelIssue) {
14
+ * this.logger.info("Cancelling"); // bound to this run
15
+ * return new ResponseCancelIssue(await this.client.cancelIssue(command));
16
+ * }
17
+ * }
18
+ *
19
+ * Context: while a method runs, `this.context`, `this.span`, `this.logger`, `this.featureBus` and
20
+ * every dependency are that run's (`executeService`). `serviceName` is stable metadata, never
21
+ * `constructor.name`, which a minifier renames.
22
+ */
23
+ export declare abstract class Feature<C extends Command<R>, R, Context extends object = ContextValues> {
24
+ /** What this run reads: the process's context, its bounded context's, its scope's. */
25
+ readonly context: Readonly<Partial<Context>>;
26
+ /** This run's span: `this.span.set({...})` says more about the use case. */
27
+ readonly span: Span;
28
+ /** This run's logger: every line carries its use case, execution and trace. */
29
+ readonly logger: LayerLogger;
30
+ abstract execute(command: C): R | Promise<R>;
31
+ }
32
+ export declare abstract class ApplicationService<C extends Command<R>, R, Context extends object = ContextValues> extends Feature<C, R, Context> {
33
+ /** The context's Features, run as children of this execution. */
34
+ protected readonly featureBus: FeatureBus;
35
+ }
36
+ /** @internal — what a run finds on `this` that no dependency may take. */
37
+ export declare const RESERVED_MEMBERS: ReadonlySet<string>;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The services of the application layer, as sincpro_framework's: a Feature executes one
3
+ * registered Command; an ApplicationService composes its context's Features.
4
+ *
5
+ * @project.feature(CommandCancelIssue)
6
+ * class CancelIssue extends Feature<CommandCancelIssue, ResponseCancelIssue> {
7
+ * static readonly serviceName = "CancelIssue";
8
+ * async execute(command: CommandCancelIssue) {
9
+ * this.logger.info("Cancelling"); // bound to this run
10
+ * return new ResponseCancelIssue(await this.client.cancelIssue(command));
11
+ * }
12
+ * }
13
+ *
14
+ * Context: while a method runs, `this.context`, `this.span`, `this.logger`, `this.featureBus` and
15
+ * every dependency are that run's (`executeService`). `serviceName` is stable metadata, never
16
+ * `constructor.name`, which a minifier renames.
17
+ */
18
+ export class Feature {
19
+ }
20
+ export class ApplicationService extends Feature {
21
+ }
22
+ /** @internal — what a run finds on `this` that no dependency may take. */
23
+ export const RESERVED_MEMBERS = new Set([
24
+ "featureBus",
25
+ "context",
26
+ "span",
27
+ "logger",
28
+ ]);
@@ -0,0 +1,70 @@
1
+ import type { ContextValues } from "../../context/domain/store.js";
2
+ import { type Context } from "../../context/entrypoint/context.js";
3
+ import { type CommandBus } from "../domain/bus.js";
4
+ import type { Answer, AnyCommand, Command, CommandClass, ResponseClass } from "../domain/dto.js";
5
+ import type { ErrorClass, ErrorHandler } from "../domain/errors.js";
6
+ import type { LayerLogger } from "../../observability/domain/logging.js";
7
+ interface Executor<C extends AnyCommand, R> {
8
+ execute(command: C): R | Promise<R>;
9
+ }
10
+ type ServiceClass<C extends AnyCommand, R> = {
11
+ new (): Executor<C, R>;
12
+ readonly serviceName: string;
13
+ };
14
+ /**
15
+ * A bounded context: the bus of its Commands, as sincpro_framework's `UseFramework`.
16
+ *
17
+ * const project = new BoundedContext<ProjectDependencies>("project");
18
+ * project.addDependency("client", clientOf(transport));
19
+ * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
20
+ * project.addFeatureErrorHandler(toEmptyWhenMissing);
21
+ * project.expectErrors(IssueClosed); // refused, never reported
22
+ * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
23
+ * project.logger.info("Catalog preloaded");
24
+ *
25
+ * Context: registrations, dependencies, handlers and expected errors are snapshotted per
26
+ * generation; a call keeps the generation it started with, and what is added later answers from
27
+ * the next call — as the backend's `build_root_bus`.
28
+ */
29
+ export declare class BoundedContext<D extends object = Record<never, never>, C extends object = ContextValues> {
30
+ readonly name: string;
31
+ /** Its level of the context: what every use case of it runs with, under the process's. */
32
+ readonly context: Context<C>;
33
+ /** Its own logger, named after it: for what it does outside a use case. */
34
+ readonly logger: LayerLogger;
35
+ private readonly registrations;
36
+ private readonly dependencyValues;
37
+ private readonly handlers;
38
+ private readonly expected;
39
+ private revision;
40
+ private builtRevision;
41
+ private built;
42
+ constructor(name: string);
43
+ get deps(): Readonly<D>;
44
+ addDependency<K extends keyof D & string>(name: K, dependency: D[K], options?: {
45
+ replaces: D[K];
46
+ }): this;
47
+ feature<T extends AnyCommand>(command: CommandClass<T>): <S extends ServiceClass<NoInfer<T>, Answer<NoInfer<T>>>>(Service: S, decorator?: ClassDecoratorContext<S> | undefined) => S;
48
+ appService<T extends AnyCommand>(command: CommandClass<T>): <S extends ServiceClass<NoInfer<T>, Answer<NoInfer<T>>>>(Service: S, decorator?: ClassDecoratorContext<S> | undefined) => S;
49
+ /** Answers a failure that reaches `execute` (or a scope's): after every layer's handlers. */
50
+ addGlobalErrorHandler(handler: ErrorHandler): this;
51
+ /** Answers a Feature's failure, wherever it was executed from. */
52
+ addFeatureErrorHandler(handler: ErrorHandler): this;
53
+ /** Answers an ApplicationService's failure. */
54
+ addAppServiceErrorHandler(handler: ErrorHandler): this;
55
+ /**
56
+ * Errors of these classes are the business answering, not a fault: the run ends `refused`,
57
+ * its span is no error and nothing is reported. They are still thrown — or handled.
58
+ */
59
+ expectErrors(...types: ErrorClass[]): this;
60
+ execute<R>(command: Command<R>, _returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
61
+ scoped(context: Partial<C>): CommandBus;
62
+ buildRootBus(): void;
63
+ fresh(): BoundedContext<D, C>;
64
+ private addErrorHandler;
65
+ private registrar;
66
+ private registerService;
67
+ private generation;
68
+ private buildGeneration;
69
+ }
70
+ export {};
@@ -0,0 +1,252 @@
1
+ import { contextAt, } from "../../context/entrypoint/context.js";
2
+ import { processContext, processStore, } from "../../context/infrastructure/process.js";
3
+ import { executeTyped, FeatureBus, } from "../domain/bus.js";
4
+ import { ApplicationService, Feature, RESERVED_MEMBERS, } from "../domain/services.js";
5
+ import { handled } from "../services/error-handlers.js";
6
+ import { executeService } from "../services/execution.js";
7
+ import { boundedContextLogger, logHandled, } from "../../observability/services/logging.js";
8
+ function isExecutor(value) {
9
+ return (typeof value === "object" &&
10
+ value !== null &&
11
+ "execute" in value &&
12
+ typeof value.execute === "function");
13
+ }
14
+ /**
15
+ * A bounded context: the bus of its Commands, as sincpro_framework's `UseFramework`.
16
+ *
17
+ * const project = new BoundedContext<ProjectDependencies>("project");
18
+ * project.addDependency("client", clientOf(transport));
19
+ * project.feature(CommandCancelIssue)(CancelIssue); // or @project.feature(...)
20
+ * project.addFeatureErrorHandler(toEmptyWhenMissing);
21
+ * project.expectErrors(IssueClosed); // refused, never reported
22
+ * await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
23
+ * project.logger.info("Catalog preloaded");
24
+ *
25
+ * Context: registrations, dependencies, handlers and expected errors are snapshotted per
26
+ * generation; a call keeps the generation it started with, and what is added later answers from
27
+ * the next call — as the backend's `build_root_bus`.
28
+ */
29
+ export class BoundedContext {
30
+ name;
31
+ /** Its level of the context: what every use case of it runs with, under the process's. */
32
+ context;
33
+ /** Its own logger, named after it: for what it does outside a use case. */
34
+ logger;
35
+ registrations = new Map();
36
+ dependencyValues = new Map();
37
+ handlers = {
38
+ global: [],
39
+ feature: [],
40
+ application_service: [],
41
+ };
42
+ expected = [];
43
+ revision = 0;
44
+ builtRevision = -1;
45
+ built;
46
+ constructor(name) {
47
+ this.name = name;
48
+ if (name.trim() === "" || name.includes("/")) {
49
+ throw new TypeError("A bounded context needs a nonempty name without '/'");
50
+ }
51
+ this.context = contextAt(`bounded_context:${name}`, processStore);
52
+ this.logger = boundedContextLogger(name);
53
+ }
54
+ get deps() {
55
+ // The dependency interface is erased; the locator checks real registrations.
56
+ return new Proxy({}, {
57
+ get: (_target, key) => {
58
+ if (typeof key !== "string" || !this.dependencyValues.has(key)) {
59
+ throw new Error(`Dependency is not registered: ${String(key)}`);
60
+ }
61
+ return this.dependencyValues.get(key);
62
+ },
63
+ set: () => {
64
+ throw new Error("Dependencies are read-only; use addDependency");
65
+ },
66
+ deleteProperty: () => {
67
+ throw new Error("Dependencies cannot be deleted");
68
+ },
69
+ defineProperty: () => {
70
+ throw new Error("Dependencies are read-only");
71
+ },
72
+ setPrototypeOf: () => {
73
+ throw new Error("Dependencies are read-only");
74
+ },
75
+ });
76
+ }
77
+ addDependency(name, dependency, options) {
78
+ if (RESERVED_MEMBERS.has(name)) {
79
+ throw new Error(`Reserved dependency name: ${name}`);
80
+ }
81
+ const registered = this.dependencyValues.has(name);
82
+ if (registered && options === undefined) {
83
+ throw new Error(`Dependency already registered: ${name}`);
84
+ }
85
+ if (options && (!registered || this.dependencyValues.get(name) !== options.replaces)) {
86
+ throw new Error(`Dependency replacement does not match the registration: ${name}`);
87
+ }
88
+ this.dependencyValues.set(name, dependency);
89
+ this.revision++;
90
+ return this;
91
+ }
92
+ feature(command) {
93
+ return this.registrar(command, "feature");
94
+ }
95
+ appService(command) {
96
+ return this.registrar(command, "application_service");
97
+ }
98
+ /** Answers a failure that reaches `execute` (or a scope's): after every layer's handlers. */
99
+ addGlobalErrorHandler(handler) {
100
+ return this.addErrorHandler("global", handler);
101
+ }
102
+ /** Answers a Feature's failure, wherever it was executed from. */
103
+ addFeatureErrorHandler(handler) {
104
+ return this.addErrorHandler("feature", handler);
105
+ }
106
+ /** Answers an ApplicationService's failure. */
107
+ addAppServiceErrorHandler(handler) {
108
+ return this.addErrorHandler("application_service", handler);
109
+ }
110
+ /**
111
+ * Errors of these classes are the business answering, not a fault: the run ends `refused`,
112
+ * its span is no error and nothing is reported. They are still thrown — or handled.
113
+ */
114
+ expectErrors(...types) {
115
+ for (const type of types) {
116
+ if (typeof type !== "function")
117
+ throw new TypeError("An expected error is a class");
118
+ if (!this.expected.includes(type))
119
+ this.expected.push(type);
120
+ }
121
+ this.revision++;
122
+ return this;
123
+ }
124
+ execute(command, _returnType) {
125
+ return executeTyped((one) => this.generation().root(one, {}), command);
126
+ }
127
+ scoped(context) {
128
+ const scope = { ...context };
129
+ return new FeatureBus((command) => this.generation().root(command, scope));
130
+ }
131
+ buildRootBus() {
132
+ this.generation();
133
+ }
134
+ fresh() {
135
+ const fresh = new BoundedContext(this.name);
136
+ for (const [command, registration] of this.registrations) {
137
+ fresh.registrations.set(command, registration);
138
+ }
139
+ for (const [name, dependency] of this.dependencyValues) {
140
+ fresh.dependencyValues.set(name, dependency);
141
+ }
142
+ for (const where of ["global", "feature", "application_service"]) {
143
+ fresh.handlers[where].push(...this.handlers[where]);
144
+ }
145
+ fresh.expected.push(...this.expected);
146
+ fresh.revision = this.revision;
147
+ return fresh;
148
+ }
149
+ addErrorHandler(where, handler) {
150
+ if (typeof handler !== "function")
151
+ throw new TypeError("An error handler is a function");
152
+ this.handlers[where].push(handler);
153
+ this.revision++;
154
+ return this;
155
+ }
156
+ registrar(command, layer) {
157
+ return (Service, decorator) => {
158
+ const register = () => this.registerService(command, Service, layer);
159
+ // Standard decorators run before static fields have initialized.
160
+ if (decorator)
161
+ decorator.addInitializer(register);
162
+ else
163
+ register();
164
+ return Service;
165
+ };
166
+ }
167
+ registerService(command, Service, layer) {
168
+ const Base = layer === "feature" ? Feature : ApplicationService;
169
+ if (!(Service.prototype instanceof Base) ||
170
+ (layer === "feature" && Service.prototype instanceof ApplicationService)) {
171
+ throw new TypeError(`The service is not an ${layer}`);
172
+ }
173
+ if (this.registrations.has(command)) {
174
+ throw new Error("Command already registered: one executor across both layers");
175
+ }
176
+ const name = Service.serviceName;
177
+ if (typeof name !== "string" || name.trim() === "") {
178
+ throw new Error("A service needs a nonempty registered name");
179
+ }
180
+ if ([...this.registrations.values()].some((registration) => registration.name === name)) {
181
+ throw new Error(`Service name already registered: ${name}`);
182
+ }
183
+ this.registrations.set(command, { name, layer, Service });
184
+ this.revision++;
185
+ }
186
+ generation() {
187
+ if (this.built && this.builtRevision === this.revision)
188
+ return this.built;
189
+ const revision = this.revision;
190
+ const built = this.buildGeneration();
191
+ this.built = built;
192
+ this.builtRevision = revision;
193
+ return built;
194
+ }
195
+ buildGeneration() {
196
+ const registrations = new Map(this.registrations);
197
+ const dependencies = new Map(this.dependencyValues);
198
+ const handlers = {
199
+ global: [...this.handlers.global],
200
+ feature: [...this.handlers.feature],
201
+ application_service: [...this.handlers.application_service],
202
+ };
203
+ const expectedTypes = [...this.expected];
204
+ const expected = (error) => expectedTypes.some((type) => error instanceof type);
205
+ const instances = new Map();
206
+ for (const registration of registrations.values()) {
207
+ const instance = new registration.Service();
208
+ if (!isExecutor(instance)) {
209
+ throw new Error(`Registered service is unavailable: ${registration.name}`);
210
+ }
211
+ instances.set(registration, instance);
212
+ }
213
+ const dispatch = async (command, parent, featureOnly = false, scope = {}) => {
214
+ // Exact constructor identity: a subclass of a registered Command is another Command.
215
+ const target = registrations.get(Object.getPrototypeOf(command)?.constructor);
216
+ if (!target)
217
+ throw new Error("No executor registered for this command class");
218
+ if (featureOnly && target.layer !== "feature") {
219
+ throw new Error("An ApplicationService may compose only Features");
220
+ }
221
+ const service = instances.get(target);
222
+ if (!service) {
223
+ throw new Error(`Registered service is unavailable: ${target.name}`);
224
+ }
225
+ return executeService(service, command, {
226
+ boundedContext: this.name,
227
+ name: target.name,
228
+ layer: target.layer,
229
+ dependencies,
230
+ context: () => ({ ...processContext.values, ...this.context.values, ...scope }),
231
+ ...(parent ? { parent } : {}),
232
+ featureBus: (execution) => new FeatureBus((child) => dispatch(child, execution, true)),
233
+ expected,
234
+ handlers: handlers[target.layer],
235
+ });
236
+ };
237
+ return {
238
+ root: async (command, scope) => {
239
+ try {
240
+ return await dispatch(command, undefined, false, scope);
241
+ }
242
+ catch (error) {
243
+ if (handlers.global.length === 0)
244
+ throw error;
245
+ const answer = await handled(handlers.global, error);
246
+ logHandled("bounded_context", { boundedContext: this.name });
247
+ return answer;
248
+ }
249
+ },
250
+ };
251
+ }
252
+ }
@@ -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;