@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
@@ -1,37 +1,5 @@
1
- function hex(bytes) {
2
- return Array.from(globalThis.crypto.getRandomValues(new Uint8Array(bytes)), (byte) => byte.toString(16).padStart(2, "0")).join("");
3
- }
4
- export function memoryTracer() {
5
- const spans = [];
6
- return {
7
- spans,
8
- start(name, options) {
9
- const recorded = {
10
- name,
11
- traceId: options.parent?.traceId || hex(16),
12
- spanId: hex(8),
13
- parentSpanId: options.parent?.spanId || undefined,
14
- attributes: { ...options.attributes },
15
- outcome: undefined,
16
- error: undefined,
17
- };
18
- spans.push(recorded);
19
- return {
20
- traceId: recorded.traceId,
21
- spanId: recorded.spanId,
22
- sampled: true,
23
- set: (attributes) => void Object.assign(recorded.attributes, attributes),
24
- end: (outcome, error) => {
25
- if (recorded.outcome !== undefined)
26
- return;
27
- recorded.outcome = outcome;
28
- recorded.error = error;
29
- },
30
- };
31
- },
32
- };
33
- }
34
- export function memoryReporter() {
35
- const reports = [];
36
- return { reports, report: (error, about) => void reports.push({ error, about }) };
37
- }
1
+ /**
2
+ * `@sincpro/client-framework/testing`: the observability ports in memory, so a use case is
3
+ * proven in Node with nothing of a platform. Not part of what an application runs.
4
+ */
5
+ export { memoryLogger, memoryReporter, memoryTracer, } from "../observability/adapters/memory.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/client-framework",
3
- "version": "0.5.0",
3
+ "version": "1.0.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",
@@ -10,6 +10,10 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "default": "./dist/index.js"
12
12
  },
13
+ "./core": {
14
+ "types": "./dist/core/index.d.ts",
15
+ "default": "./dist/core/index.js"
16
+ },
13
17
  "./codegen": {
14
18
  "types": "./dist/codegen/index.d.ts",
15
19
  "default": "./dist/codegen/index.js"
@@ -18,17 +22,25 @@
18
22
  "types": "./dist/testing/index.d.ts",
19
23
  "default": "./dist/testing/index.js"
20
24
  },
25
+ "./console": {
26
+ "types": "./dist/observability/adapters/console.d.ts",
27
+ "default": "./dist/observability/adapters/console.js"
28
+ },
21
29
  "./opentelemetry": {
22
- "types": "./dist/adapters/opentelemetry.d.ts",
23
- "default": "./dist/adapters/opentelemetry.js"
30
+ "types": "./dist/observability/adapters/opentelemetry.d.ts",
31
+ "default": "./dist/observability/adapters/opentelemetry.js"
24
32
  },
25
33
  "./sentry": {
26
- "types": "./dist/adapters/sentry.d.ts",
27
- "default": "./dist/adapters/sentry.js"
34
+ "types": "./dist/observability/adapters/sentry.d.ts",
35
+ "default": "./dist/observability/adapters/sentry.js"
36
+ },
37
+ "./parquet": {
38
+ "types": "./dist/client/adapters/parquet.d.ts",
39
+ "default": "./dist/client/adapters/parquet.js"
28
40
  }
29
41
  },
30
42
  "bin": {
31
- "sincpro-client": "./dist/codegen/cli.js"
43
+ "sincpro-client": "./dist/codegen/entrypoint/cli.js"
32
44
  },
33
45
  "files": [
34
46
  "dist",
@@ -68,13 +80,17 @@
68
80
  },
69
81
  "peerDependencies": {
70
82
  "@opentelemetry/api": "^1.9.0",
71
- "@sincpro/criteria": "^2.0.0",
83
+ "@sincpro/criteria": "^2.1.0",
84
+ "hyparquet": "^1.32.0",
72
85
  "typescript": ">=5.5 <7"
73
86
  },
74
87
  "peerDependenciesMeta": {
75
88
  "@opentelemetry/api": {
76
89
  "optional": true
77
90
  },
91
+ "hyparquet": {
92
+ "optional": true
93
+ },
78
94
  "typescript": {
79
95
  "optional": true
80
96
  }
@@ -83,11 +99,12 @@
83
99
  "@eslint/js": "^10.0.1",
84
100
  "@opentelemetry/api": "1.10.0",
85
101
  "@opentelemetry/sdk-trace": "3.1.0",
86
- "@sincpro/criteria": "^2.0.0",
102
+ "@sincpro/criteria": "^2.1.0",
87
103
  "@types/node": "^26.6.6",
88
104
  "eslint": "^10.14.0",
89
105
  "eslint-config-prettier": "^10.1.8",
90
106
  "eslint-plugin-simple-import-sort": "^14.0.0",
107
+ "hyparquet": "^1.32.0",
91
108
  "prettier": "^3.9.10",
92
109
  "tsc-alias": "^1.9.9",
93
110
  "typescript": "~6.0.3",
@@ -1,15 +0,0 @@
1
- import type { Metamodel } from "./metamodel.js";
2
- /**
3
- * The TypeScript of every DTO of `metamodel`, as the text of one file.
4
- *
5
- * 1. The models, by name: an entity's contract, a related model by its own name.
6
- * 2. Each operation, by its local name (its identity without `<context>.`): its input as
7
- * `<Name>` and its answer by the backend's own name for it (`answerOf`: `ResponseX`), with
8
- * `<Name>Answer` kept as a deprecated alias — or as `<Name>Answer` when it has no name.
9
- * 3. The shared definitions those reach — and the ones those reach, until none is new —, but
10
- * never one a package of the stack already types (`Criteria`).
11
- * Final: a header that says where it came from and not to edit it, the imports, then models,
12
- * definitions and operations, each sorted by name; two of them with one TypeScript name are
13
- * refused, naming both.
14
- */
15
- export declare function domainFileOf(metamodel: Metamodel): string;
@@ -1,65 +0,0 @@
1
- /**
2
- * What a platform plugs so every use case is traced and every unexpected error reported.
3
- *
4
- * Context: sincpro_framework opens one span per use case and reports what nobody expected; a
5
- * client has to do the same, and continue the backend's trace. The vendor — OpenTelemetry in a
6
- * browser, a queue on a phone, GlitchTip, memory in a test — is the platform's, so this package
7
- * defines the contract and calls it, and bundles none. Two implementations at least (a
8
- * platform's and a test's) are why these are ports.
9
- *
10
- * Nothing here may break a use case: a tracer or a reporter that throws is ignored, and the use
11
- * case answers as it would have.
12
- */
13
- /** What is said about a span: plain values only, the way OpenTelemetry takes them. */
14
- export type Attributes = Readonly<Record<string, string | number | boolean>>;
15
- /**
16
- * How a use case ended.
17
- *
18
- * - `ok`: it answered.
19
- * - `refused`: the server said no and explained why — a validation, a conflict, a permission.
20
- * Expected; never reported.
21
- * - `aborted`: whoever asked stopped waiting (a screen that closed). Never reported.
22
- * - `failed`: anything else — a bug, a server that broke, a server nobody could reach. Reported.
23
- */
24
- export type Outcome = "ok" | "refused" | "aborted" | "failed";
25
- /** One span: a use case, open while it runs. */
26
- export interface Span {
27
- /** 32 hex — the id the server continues and a person quotes as a support code. Empty when
28
- * nothing traces. */
29
- readonly traceId: string;
30
- /** 16 hex. Empty when nothing traces. */
31
- readonly spanId: string;
32
- /** Whether this trace is kept: the server follows the same decision. */
33
- readonly sampled: boolean;
34
- /** Vendor state that travels beside the trace (`tracestate`), when there is any. */
35
- readonly state?: string;
36
- /** Says more about the use case while it runs. */
37
- set(attributes: Attributes): void;
38
- /** Closes it, once. `error` is what it failed or was refused with. */
39
- end(outcome: Outcome, error?: unknown): void;
40
- }
41
- /** Opens spans. One per platform; `NO_TRACER` when nothing traces. */
42
- export interface Tracer {
43
- start(name: string, options: {
44
- parent?: Span;
45
- attributes?: Attributes;
46
- }): Span;
47
- }
48
- /** What a report says about where the error happened. */
49
- export type ErrorAbout = {
50
- /** `<bounded context>.<Service>[.<method>]`: the use case's identity, as the backend's — or,
51
- * for what failed outside one (`reportError`), where it happened (`view project:issue/acme-3`). */
52
- useCase: string;
53
- /** The trace the error belongs to — the same id the server logged, when it got that far. */
54
- traceId: string;
55
- /** The execution's own id. */
56
- executionId: string;
57
- /** What the use case ran with: the process's context, its bounded context's, its scope's. */
58
- context: Readonly<Record<string, unknown>>;
59
- };
60
- /** Reports what nobody expected. One per platform; none when nothing reports. */
61
- export interface ErrorReporter {
62
- report(error: unknown, about: ErrorAbout): void;
63
- }
64
- /** Traces nothing: what a use case runs with until a platform plugs a tracer. */
65
- export declare const NO_TRACER: Tracer;
@@ -1,21 +0,0 @@
1
- /**
2
- * What a platform plugs so every use case is traced and every unexpected error reported.
3
- *
4
- * Context: sincpro_framework opens one span per use case and reports what nobody expected; a
5
- * client has to do the same, and continue the backend's trace. The vendor — OpenTelemetry in a
6
- * browser, a queue on a phone, GlitchTip, memory in a test — is the platform's, so this package
7
- * defines the contract and calls it, and bundles none. Two implementations at least (a
8
- * platform's and a test's) are why these are ports.
9
- *
10
- * Nothing here may break a use case: a tracer or a reporter that throws is ignored, and the use
11
- * case answers as it would have.
12
- */
13
- const EMPTY_SPAN = {
14
- traceId: "",
15
- spanId: "",
16
- sampled: false,
17
- set: () => undefined,
18
- end: () => undefined,
19
- };
20
- /** Traces nothing: what a use case runs with until a platform plugs a tracer. */
21
- export const NO_TRACER = { start: () => EMPTY_SPAN };
@@ -1,79 +0,0 @@
1
- import { type Context } from "../context/context.js";
2
- import type { ContextValues } from "../context/store.js";
3
- import { type ServiceClasses, type Services } from "./feature.js";
4
- /**
5
- * A bounded context: its dependencies, its services, and the one object everything else uses
6
- * them through — set up the way sincpro_framework sets one up.
7
- *
8
- * // domains/project/infrastructure/dependencies.ts ← what the services can count on
9
- * export interface ProjectDependencies {
10
- * client: ProjectClient;
11
- * session: Session;
12
- * }
13
- * export function registerDependencies(bc: UseBoundedContext<ProjectDependencies>) {
14
- * bc.addDependency("client", clientOf(transport));
15
- * bc.addDependency("session", session);
16
- * return bc;
17
- * }
18
- *
19
- * // domains/project/infrastructure/bounded_context.ts
20
- * export class Feature extends BaseFeature<ProjectDependencies> {}
21
- * export class ApplicationService extends BaseApplicationService<ProjectDependencies, ProjectFeatures> {}
22
- * export function configProjectBoundedContext(name: string) {
23
- * return registerDependencies(new UseBoundedContext<ProjectDependencies>(name));
24
- * }
25
- *
26
- * // domains/project/index.ts
27
- * export const project = configProjectBoundedContext("project").register({
28
- * features, // { Issues, Notices }
29
- * applicationServices, // { CloseAndNotify }
30
- * });
31
- *
32
- * // anywhere — a screen, another bounded context's adapter, a test
33
- * await project.Issues.close(input); // span "project/Issues.close"
34
- * await project.CloseAndNotify.execute(input); // span "project/CloseAndNotify", its Features under it
35
- *
36
- * Context: sincpro_framework's `UseFramework` (being renamed to bounded context) with
37
- * `add_dependency`, and `project(Command(...))` written as a property: a service is reached by
38
- * the name it is registered under, so its span is named by that name — a production build renames
39
- * classes, never the keys of an object — and the type of `project` is the services it holds.
40
- *
41
- * `project` is light and never frozen. It holds references: the classes, the dependencies, and
42
- * one instance of each service, made the first time it is asked for and shared by everything
43
- * derived from it. Each call makes a small view of that instance — a few pointers — that carries
44
- * the call's trace. A screen or a test that needs its own variant derives one, with everything
45
- * else shared:
46
- *
47
- * project.with({ client: fake }) other dependencies — a test, a demo
48
- * project.scoped({ company_id: 2 }) more context for what runs through it — a window
49
- *
50
- * A service is a singleton of its bounded context: what a method writes on `this` stays for the
51
- * next call and for the call running beside it, as in sincpro_framework. What belongs to one
52
- * request goes in its arguments or its context, never on `this`.
53
- */
54
- export type BoundedContext<D extends object, C extends object, F extends ServiceClasses, A extends ServiceClasses> = Services<F, D> & Services<A, D> & {
55
- readonly name: string;
56
- /** This bounded context's level of the context: under the process's, over a scope's. */
57
- readonly context: Context<C>;
58
- /** The same bounded context with other dependencies — the rest, and the services, shared. */
59
- with(dependencies: Partial<D>): BoundedContext<D, C, F, A>;
60
- /** The same bounded context with more context for what runs through it. */
61
- scoped(context: Partial<C>): BoundedContext<D, C, F, A>;
62
- };
63
- export declare class UseBoundedContext<D extends object, C extends object = ContextValues> {
64
- #private;
65
- readonly name: string;
66
- /** This bounded context's level of the context, saved in the process's store. */
67
- readonly context: Context<C>;
68
- constructor(name: string);
69
- /** `value` is what every service of this bounded context finds at `this.<name>`. */
70
- addDependency<K extends keyof D & string>(name: K, value: D[K]): this;
71
- /**
72
- * The object the bounded context is used through: each service under its name. A dependency
73
- * added after this is seen too — the services read them on every call.
74
- */
75
- register<F extends ServiceClasses, A extends ServiceClasses = Record<never, never>>(services: {
76
- features: F;
77
- applicationServices?: A;
78
- }): BoundedContext<D, C, F, A>;
79
- }
@@ -1,261 +0,0 @@
1
- import { newEntityId } from "../common/ids.js";
2
- import { contextAt } from "../context/context.js";
3
- import { bindDependency } from "./execution.js";
4
- import { composes, isApplicationServiceClass, isFeatureClass, methodsOf, } from "./feature.js";
5
- import { processContext, processNow, processStore, } from "./process.js";
6
- import { firstReport } from "./report.js";
7
- /** Names `project` answers itself: no service is registered under them. */
8
- const RESERVED = new Set(["name", "context", "with", "scoped"]);
9
- /** Names a run answers on `this` itself: no dependency is added under them. */
10
- const ANSWERED = new Set(["context", "feature", "span"]);
11
- export class UseBoundedContext {
12
- name;
13
- /** This bounded context's level of the context, saved in the process's store. */
14
- context;
15
- #dependencies = {};
16
- constructor(name) {
17
- if (name === "" || name.includes("/"))
18
- throw new Error(`"${name}" is not a bounded context's name — it names its spans: "${name}/Issues.close"`);
19
- this.name = name;
20
- this.context = contextAt(`bounded_context:${name}`, processStore);
21
- }
22
- /** `value` is what every service of this bounded context finds at `this.<name>`. */
23
- addDependency(name, value) {
24
- if (ANSWERED.has(name))
25
- throw new Error(`${this.name}: a dependency cannot be called "${name}" — every service answers this.${name} itself`);
26
- this.#dependencies[name] = value;
27
- return this;
28
- }
29
- /**
30
- * The object the bounded context is used through: each service under its name. A dependency
31
- * added after this is seen too — the services read them on every call.
32
- */
33
- register(services) {
34
- const classes = new Map();
35
- const add = (key, service) => {
36
- if (RESERVED.has(key))
37
- throw new Error(`${this.name}: no service can be called "${key}" — it is the bounded context's own`);
38
- if (classes.has(key))
39
- throw new Error(`${this.name}: two services are called "${key}"`);
40
- classes.set(key, service);
41
- };
42
- for (const [key, service] of Object.entries(services.features)) {
43
- if (typeof service !== "function" || !isFeatureClass(service))
44
- throw new Error(`${this.name}: "${key}" is not a Feature — an ApplicationService goes in applicationServices`);
45
- add(key, service);
46
- }
47
- for (const [key, service] of Object.entries(services.applicationServices ?? {})) {
48
- if (typeof service !== "function" || !isApplicationServiceClass(service))
49
- throw new Error(`${this.name}: "${key}" is not an ApplicationService — a Feature goes in features`);
50
- add(key, service);
51
- }
52
- const registry = {
53
- name: this.name,
54
- dependencies: this.#dependencies,
55
- classes,
56
- features: new Set(Object.keys(services.features)),
57
- instances: new Map(),
58
- context: this.context,
59
- };
60
- return assemble({ registry, overrides: {}, scope: {} });
61
- }
62
- }
63
- function assemble(binding) {
64
- const { registry } = binding;
65
- const held = {};
66
- Object.defineProperties(held, {
67
- name: { value: registry.name, enumerable: true },
68
- context: { value: registry.context, enumerable: true },
69
- with: {
70
- value: (dependencies) => assemble({ ...binding, overrides: { ...binding.overrides, ...dependencies } }),
71
- },
72
- scoped: {
73
- value: (values) => assemble({ ...binding, scope: { ...binding.scope, ...values } }),
74
- },
75
- });
76
- // Each service is made and wrapped the first time it is asked for, then kept on this object.
77
- for (const key of registry.classes.keys())
78
- Object.defineProperty(held, key, {
79
- configurable: true,
80
- enumerable: true,
81
- get() {
82
- const service = intercepted(key, binding, undefined);
83
- Object.defineProperty(held, key, { value: service, enumerable: true });
84
- return service;
85
- },
86
- });
87
- return held;
88
- }
89
- function instanceOf(registry, key) {
90
- const known = registry.instances.get(key);
91
- if (known !== undefined)
92
- return known;
93
- const Service = registry.classes.get(key);
94
- if (Service === undefined)
95
- throw new Error(`${registry.name}: no service is called "${key}"`);
96
- const made = new Service();
97
- registry.instances.set(key, made);
98
- return made;
99
- }
100
- /**
101
- * A service as its holder sees it: each public method runs in an execution of its own — a child
102
- * of `parent` when an ApplicationService runs it. A method is the same function every time it is
103
- * read, so a screen can keep it (`useService(project.Issues.close)`).
104
- */
105
- function intercepted(key, binding, parent) {
106
- const target = instanceOf(binding.registry, key);
107
- const traced = methodsOf(target);
108
- const methods = new Map();
109
- return new Proxy(target, {
110
- get(held, property) {
111
- const value = Reflect.get(held, property, held);
112
- if (typeof property !== "string" ||
113
- typeof value !== "function" ||
114
- !traced.has(property))
115
- return value;
116
- let method = methods.get(property);
117
- if (method === undefined) {
118
- const body = value;
119
- method = (...args) => run(held, key, property, body, args, binding, parent);
120
- methods.set(property, method);
121
- }
122
- return method;
123
- },
124
- });
125
- }
126
- function dependencyOf(binding, name) {
127
- if (Object.hasOwn(binding.overrides, name))
128
- return { found: true, value: binding.overrides[name] };
129
- if (Object.hasOwn(binding.registry.dependencies, name))
130
- return { found: true, value: binding.registry.dependencies[name] };
131
- return { found: false, value: undefined };
132
- }
133
- /**
134
- * The run's own view of the instance: its context, its dependencies bound to this execution
135
- * (each bound the first time it is read), and — for an ApplicationService — its Features as
136
- * children of this run. Anything else is the instance's.
137
- */
138
- function viewOf(target, binding, execution) {
139
- const bound = new Map();
140
- let features;
141
- const isComposer = composes(target);
142
- return new Proxy(target, {
143
- get(held, property, receiver) {
144
- if (property === "context")
145
- return execution.context;
146
- if (property === "span")
147
- return execution.span;
148
- if (property === "feature" && isComposer)
149
- return (features ??= featuresOf(binding, execution));
150
- if (typeof property === "string") {
151
- if (bound.has(property))
152
- return bound.get(property);
153
- const dependency = dependencyOf(binding, property);
154
- if (dependency.found) {
155
- const value = bindDependency(dependency.value, execution);
156
- bound.set(property, value);
157
- return value;
158
- }
159
- }
160
- return Reflect.get(held, property, receiver);
161
- },
162
- set(held, property, value) {
163
- if (ANSWERED.has(String(property)))
164
- return false;
165
- if (typeof property === "string" && dependencyOf(binding, property).found)
166
- return false;
167
- // What a method writes on `this` lands on the instance — it is the bounded context's.
168
- return Reflect.set(held, property, value);
169
- },
170
- });
171
- }
172
- /** An ApplicationService's `this.feature`: each Feature, asked by name, a child of the run. */
173
- function featuresOf(binding, execution) {
174
- const children = new Map();
175
- return new Proxy({}, {
176
- get(_, property) {
177
- if (typeof property !== "string" || !binding.registry.features.has(property))
178
- return undefined;
179
- let child = children.get(property);
180
- if (child === undefined) {
181
- child = intercepted(property, binding, execution);
182
- children.set(property, child);
183
- }
184
- return child;
185
- },
186
- });
187
- }
188
- const NOTHING = {};
189
- /** What a gesture runs with: the process's context, under it the bounded context's, its scope's. */
190
- function contextOf(binding) {
191
- return {
192
- ...processContext.values,
193
- ...binding.registry.context.values,
194
- ...binding.scope,
195
- };
196
- }
197
- /** What watches a use case never changes what it answers: a tracer or reporter that throws is ignored. */
198
- function quietly(work, otherwise) {
199
- try {
200
- return work();
201
- }
202
- catch {
203
- return otherwise;
204
- }
205
- }
206
- const NO_SPAN = {
207
- traceId: "",
208
- spanId: "",
209
- sampled: false,
210
- set: () => undefined,
211
- end: () => undefined,
212
- };
213
- function run(target, key, method, body, args, binding, parent) {
214
- const settings = processNow();
215
- const boundedContext = binding.registry.name;
216
- // As sincpro_framework names a use case: the span `context/Name`, the identity `context.Name`.
217
- const identity = method === "execute" ? key : `${key}.${method}`;
218
- const useCase = `${boundedContext}.${identity}`;
219
- const context = parent?.context ?? quietly(() => contextOf(binding), NOTHING);
220
- const id = newEntityId();
221
- const attributes = {
222
- "sincpro.use_case": useCase,
223
- "sincpro.layer": composes(target) ? "application_service" : "feature",
224
- "sincpro.context": boundedContext,
225
- "sincpro.execution_id": id,
226
- };
227
- const span = quietly(() => settings.tracer.start(`${boundedContext}/${identity}`, {
228
- ...(parent === undefined ? {} : { parent: parent.span }),
229
- attributes,
230
- }), NO_SPAN);
231
- const execution = { id, useCase, boundedContext, parent, span, context };
232
- const finish = (failed, error) => {
233
- const outcome = failed ? quietly(() => settings.outcomeOf(error), "failed") : "ok";
234
- quietly(() => span.end(outcome, error), undefined);
235
- if (outcome === "failed" && firstReport(error))
236
- quietly(() => settings.reporter?.report(error, {
237
- useCase,
238
- traceId: span.traceId,
239
- executionId: id,
240
- context,
241
- }), undefined);
242
- };
243
- let answer;
244
- try {
245
- answer = body.apply(viewOf(target, binding, execution), args);
246
- }
247
- catch (error) {
248
- finish(true, error);
249
- throw error;
250
- }
251
- if (answer instanceof Promise)
252
- return answer.then((value) => {
253
- finish(false);
254
- return value;
255
- }, (error) => {
256
- finish(true, error);
257
- throw error;
258
- });
259
- finish(false);
260
- return answer;
261
- }
@@ -1,63 +0,0 @@
1
- import type { Transport } from "../client/transport.js";
2
- import type { Span } from "../observability/ports.js";
3
- /**
4
- * One run of a use case: its span, what it runs with, and who ran it.
5
- *
6
- * Context: sincpro_framework keeps the running use case in a context variable; a browser has
7
- * nothing like it that survives an `await` (AsyncContext is a proposal, Zone.js patches the
8
- * world), and a global stack mixes the parents of two use cases running at once — which a
9
- * screen with several windows does all the time. So an execution is a value, handed to what
10
- * the use case calls: its Features (`this.feature`) and its dependencies (`BIND`).
11
- */
12
- export type Execution = {
13
- /** UUID v7, as 32 hex: this run. */
14
- readonly id: string;
15
- /** `<bounded context>.<Service>[.<method>]` — the use case's identity, as the backend names its own. */
16
- readonly useCase: string;
17
- /** The bounded context the use case belongs to. */
18
- readonly boundedContext: string;
19
- /** The run that executed this one: an ApplicationService, for its Features. */
20
- readonly parent: Execution | undefined;
21
- readonly span: Span;
22
- /** What it runs with, read once where the gesture started and inherited by every child. */
23
- readonly context: Readonly<Record<string, unknown>>;
24
- };
25
- /**
26
- * A dependency that has something to say about the execution it is used in: given that
27
- * execution, it answers itself bound to it. The transport and every generated client are;
28
- * any other dependency of a bounded context is handed as it is.
29
- */
30
- export declare const BIND: unique symbol;
31
- export type Bindable<T> = {
32
- [BIND]: (execution: Execution) => T;
33
- };
34
- /**
35
- * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
36
- * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
37
- * Nothing when there is no trace and no context that travels.
38
- */
39
- export declare function headersOf(execution: Execution): Record<string, string>;
40
- /**
41
- * The transport, writing the execution's headers on every call — the caller's own headers of a
42
- * call still win.
43
- *
44
- * Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
45
- * token, the tenant — the same for every call; these are the use case's, different for two use
46
- * cases running at once, so they travel per call (`CallOptions`).
47
- */
48
- export declare function bindTransport(transport: Transport, execution: Execution): Transport;
49
- /**
50
- * A client made over a transport, made again over the transport bound to each execution it is
51
- * used in. What a generated `clientOf(transport)` answers, so a Feature that calls
52
- * `this.client.sendIssue(…)` sends that request with its own trace.
53
- *
54
- * export function clientOf(transport: Transport) {
55
- * return bindable(transport, (transport) => ({ sendIssue: (body) => transport.send(…) }));
56
- * }
57
- *
58
- * Context: it takes the function that makes the client, not the client — rebuilding through
59
- * `clientOf` itself would make TypeScript infer `clientOf` from its own return.
60
- */
61
- export declare function bindable<T extends object>(transport: Transport, make: (transport: Transport) => T): T;
62
- /** A dependency as the execution should see it: bound when it can be, as it is otherwise. */
63
- export declare function bindDependency<T>(value: T, execution: Execution): T;