@sincpro/client-framework 0.6.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 (132) hide show
  1. package/AGENTS.md +54 -25
  2. package/MANIFESTO.md +59 -39
  3. package/README.md +324 -345
  4. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  5. package/dist/client/{memory.js → adapters/memory.js} +2 -2
  6. package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
  7. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
  8. package/dist/client/{rest.js → adapters/rest.js} +2 -2
  9. package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
  10. package/dist/client/index.d.ts +16 -0
  11. package/dist/client/index.js +16 -0
  12. package/dist/client/services/propagation.d.ts +32 -0
  13. package/dist/{services/execution.js → client/services/propagation.js} +5 -21
  14. package/dist/client/{retry.js → services/retry.js} +1 -1
  15. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
  16. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  17. package/dist/codegen/{names.js → domain/names.js} +3 -3
  18. package/dist/codegen/{cli.js → entrypoint/cli.js} +9 -8
  19. package/dist/codegen/index.d.ts +7 -7
  20. package/dist/codegen/index.js +5 -10
  21. package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
  22. package/dist/codegen/{client.js → services/client.js} +9 -1
  23. package/dist/codegen/services/domain.d.ts +20 -0
  24. package/dist/codegen/{domain.js → services/domain.js} +82 -25
  25. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
  26. package/dist/codegen/{entities.js → services/entities.js} +18 -4
  27. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  28. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
  29. package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
  30. package/dist/common/index.d.ts +10 -0
  31. package/dist/common/index.js +10 -0
  32. package/dist/context/adapters/memory.d.ts +6 -0
  33. package/dist/context/adapters/memory.js +34 -0
  34. package/dist/context/adapters/plain-codec.d.ts +3 -0
  35. package/dist/context/adapters/plain-codec.js +16 -0
  36. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  37. package/dist/context/{codec.js → domain/codec.js} +0 -14
  38. package/dist/context/domain/execution.d.ts +33 -0
  39. package/dist/context/domain/execution.js +11 -0
  40. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  41. package/dist/context/{store.js → domain/store.js} +1 -34
  42. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  43. package/dist/context/index.d.ts +17 -0
  44. package/dist/context/index.js +15 -0
  45. package/dist/context/infrastructure/process.d.ts +13 -0
  46. package/dist/context/infrastructure/process.js +35 -0
  47. package/dist/core/domain/bus.d.ts +18 -0
  48. package/dist/core/domain/bus.js +18 -0
  49. package/dist/core/domain/dto.d.ts +32 -0
  50. package/dist/core/domain/dto.js +20 -0
  51. package/dist/core/domain/errors.d.ts +18 -0
  52. package/dist/core/domain/errors.js +1 -0
  53. package/dist/core/domain/services.d.ts +37 -0
  54. package/dist/core/domain/services.js +28 -0
  55. package/dist/core/entrypoint/bounded-context.d.ts +70 -0
  56. package/dist/core/entrypoint/bounded-context.js +252 -0
  57. package/dist/core/entrypoint/screens.d.ts +6 -0
  58. package/dist/core/entrypoint/screens.js +136 -0
  59. package/dist/core/index.d.ts +17 -0
  60. package/dist/core/index.js +16 -0
  61. package/dist/core/infrastructure/process.d.ts +56 -0
  62. package/dist/core/infrastructure/process.js +35 -0
  63. package/dist/core/services/binding.d.ts +6 -0
  64. package/dist/core/services/binding.js +13 -0
  65. package/dist/core/services/error-handlers.d.ts +11 -0
  66. package/dist/core/services/error-handlers.js +21 -0
  67. package/dist/core/services/execution.d.ts +35 -0
  68. package/dist/core/services/execution.js +137 -0
  69. package/dist/index.d.ts +16 -38
  70. package/dist/index.js +16 -38
  71. package/dist/model/index.d.ts +10 -0
  72. package/dist/model/index.js +10 -0
  73. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  74. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  75. package/dist/model/{schema.js → services/schema.js} +1 -1
  76. package/dist/observability/adapters/console.d.ts +12 -0
  77. package/dist/observability/adapters/console.js +35 -0
  78. package/dist/observability/adapters/memory.d.ts +41 -0
  79. package/dist/observability/adapters/memory.js +49 -0
  80. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  81. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  82. package/dist/observability/domain/errors.d.ts +23 -0
  83. package/dist/observability/domain/errors.js +8 -0
  84. package/dist/observability/domain/logging.d.ts +42 -0
  85. package/dist/observability/domain/logging.js +1 -0
  86. package/dist/observability/domain/tracing.d.ts +49 -0
  87. package/dist/observability/domain/tracing.js +20 -0
  88. package/dist/observability/index.d.ts +16 -0
  89. package/dist/observability/index.js +4 -0
  90. package/dist/observability/infrastructure/registry.d.ts +21 -0
  91. package/dist/observability/infrastructure/registry.js +18 -0
  92. package/dist/observability/services/logging.d.ts +27 -0
  93. package/dist/observability/services/logging.js +105 -0
  94. package/dist/observability/services/outcome.d.ts +12 -0
  95. package/dist/observability/services/outcome.js +20 -0
  96. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  97. package/dist/testing/index.d.ts +3 -33
  98. package/dist/testing/index.js +5 -37
  99. package/package.json +16 -8
  100. package/dist/codegen/domain.d.ts +0 -15
  101. package/dist/observability/ports.d.ts +0 -65
  102. package/dist/observability/ports.js +0 -21
  103. package/dist/services/bounded-context.d.ts +0 -79
  104. package/dist/services/bounded-context.js +0 -261
  105. package/dist/services/execution.d.ts +0 -63
  106. package/dist/services/feature.d.ts +0 -85
  107. package/dist/services/feature.js +0 -86
  108. package/dist/services/process.d.ts +0 -60
  109. package/dist/services/process.js +0 -57
  110. package/dist/services/screens.d.ts +0 -2
  111. package/dist/services/screens.js +0 -76
  112. /package/dist/{adapters → client/adapters}/parquet.js +0 -0
  113. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  114. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  115. /package/dist/client/{entity.js → domain/entity.js} +0 -0
  116. /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
  117. /package/dist/client/{transport.js → domain/transport.js} +0 -0
  118. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  119. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  120. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  121. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  122. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  123. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  124. /package/dist/codegen/{check.js → services/check.js} +0 -0
  125. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  126. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  127. /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
  128. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  129. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  130. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  131. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  132. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
- import { entityKeyOf } from "../client/contract.js";
2
- import { aliasOf, answerOf, claimed, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, isEntity, localOf, methodOf, pluralOf, sorted, } from "./names.js";
1
+ import { entityKeyOf } from "../../client/domain/contract.js";
2
+ import { aliasOf, answerOf, claimed, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, isEntity, localOf, methodOf, pluralOf, sorted, } from "../domain/names.js";
3
3
  import { ownerOf, restOf, WRITES, } from "./rest.js";
4
4
  /** The one operation of `role` for `entity` a transport can call, by `own` when there are many. */
5
5
  function oneOf(candidates, role) {
@@ -90,6 +90,19 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
90
90
  const route = `${write.found.rest.method} ${recordPath(write.found.rest, field)}`;
91
91
  routes.push(` ${role}: ${JSON.stringify(route)},`);
92
92
  }
93
+ // A record's events (sincpro_framework's `DomainEvents`): its history, read by the record's key.
94
+ const events = oneOf(reads("events", isGet), "events");
95
+ if (events !== undefined) {
96
+ if ("missing" in events)
97
+ missing.push(`history: ${events.missing}`);
98
+ else {
99
+ const field = events.found.operation.targets[model];
100
+ if (field !== undefined && events.found.rest.fields.length === 1)
101
+ routes.push(` history: ${JSON.stringify(recordPath(events.found.rest, field))},`);
102
+ else
103
+ missing.push(`history: \`${events.found.local}\` is at ${events.found.rest.path}`);
104
+ }
105
+ }
93
106
  const exported = oneOf(reads("export", isGet), "export");
94
107
  if (exported !== undefined) {
95
108
  if ("missing" in exported)
@@ -142,8 +155,9 @@ function entitySourceOf(metamodel, model, domain, options, claim) {
142
155
  *
143
156
  * 1. Per model, by name: the one `search` a transport reads for it (`own` when there are
144
157
  * several) — none, and the model gets no entity, said in the file's own doc.
145
- * 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}`, and its
146
- * `export` — every record of a criteria as a file — by its path.
158
+ * 2. Its `get`, `edit` and `preview` the same way, the record's field written `{id}`; its
159
+ * `events` — a record's `DomainEvents` — as `history`, the same way; and its `export` — every
160
+ * record of a criteria as a file — by its path.
147
161
  * 3. Every write of role `command` that concerns it: with `target` when its body names the
148
162
  * record, as it is when it creates one; its body typed without that field.
149
163
  * Final: an `entity<T, C>()` per model listed, keyed `<domain>:<kind>`, and `entities`, all of
@@ -1,4 +1,4 @@
1
- import type { Metamodel, Operation } from "./metamodel.js";
1
+ import type { Metamodel, Operation } from "../domain/metamodel.js";
2
2
  /**
3
3
  * How REST reaches each operation of a contract, as the generated client and entities write it.
4
4
  *
@@ -1,4 +1,4 @@
1
- import type { Metamodel } from "./metamodel.js";
1
+ import type { Metamodel } from "../domain/metamodel.js";
2
2
  /**
3
3
  * A bounded context's skeleton, written ONCE beside what is generated — the files that are the
4
4
  * application's from then on:
@@ -7,7 +7,7 @@ import type { Metamodel } from "./metamodel.js";
7
7
  * generated/ `sincpro-client generate` — regenerated, never edited
8
8
  * infrastructure/dependencies.ts what the services can count on: the generated client
9
9
  * infrastructure/bounded_context.ts its Feature and ApplicationService, and how it is made
10
- * services/index.ts the services, by the name each is reached by
10
+ * services/index.ts registers command classes and their services
11
11
  * index.ts `export const project` — ready to use
12
12
  *
13
13
  * Context: the layout sincpro_framework gives a service (`dependencies.py`, `framework.py`,
@@ -1,9 +1,8 @@
1
- import { aliasOf, FRAMEWORK } from "./names.js";
1
+ import { aliasOf, FRAMEWORK } from "../domain/names.js";
2
2
  /** Names the skeleton's `index.ts` cannot give its constant. */
3
3
  const TAKEN = new Set([
4
4
  "transport",
5
- "features",
6
- "applicationServices",
5
+ "registerServices",
7
6
  ..."break case catch class const continue debugger default delete do else enum export extends false finally for function if import in instanceof new null return super switch this throw true try typeof var void while with yield let static implements interface package private protected public await".split(" "),
8
7
  ]);
9
8
  /** `sales-orders` → `SalesOrders`. */
@@ -31,7 +30,8 @@ export function scaffoldFilesOf(metamodel, options = {}) {
31
30
  const dependencies = [
32
31
  HEADER(`What the services of «${alias}» can count on`),
33
32
  "",
34
- `import type { Transport, UseBoundedContext } from "${FRAMEWORK}";`,
33
+ `import type { Transport } from "${FRAMEWORK}";`,
34
+ `import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
35
35
  ...(client ? ["", `import { clientOf } from "${at("..", "generated", "client")}";`] : []),
36
36
  "",
37
37
  `/** What every service of «${alias}» finds on \`this\`${client ? ": `this.client`, the service's operations" : ""}. */`,
@@ -44,7 +44,7 @@ export function scaffoldFilesOf(metamodel, options = {}) {
44
44
  "}",
45
45
  "",
46
46
  "export function registerDependencies(",
47
- ` bc: UseBoundedContext<${Dependencies}>,`,
47
+ ` bc: BoundedContext<${Dependencies}>,`,
48
48
  ` ${client ? "transport" : "_transport"}: Transport,`,
49
49
  ") {",
50
50
  ...(client ? [` bc.addDependency("client", clientOf(transport));`] : []),
@@ -58,45 +58,42 @@ export function scaffoldFilesOf(metamodel, options = {}) {
58
58
  "import {",
59
59
  " ApplicationService as BaseApplicationService,",
60
60
  " Feature as BaseFeature,",
61
- " type Transport,",
62
- " UseBoundedContext,",
63
- `} from "${FRAMEWORK}";`,
61
+ " BoundedContext,",
62
+ " type Command,",
63
+ `} from "${at(FRAMEWORK, "core")}";`,
64
+ `import type { Transport } from "${FRAMEWORK}";`,
64
65
  "",
65
- `import type { ${Name}Features } from "${at("..", "services")}";`,
66
66
  `import { type ${Dependencies}, registerDependencies } from "${at(".", "dependencies")}";`,
67
67
  "",
68
68
  `/** A Feature of «${alias}»: its dependencies typed on \`this\`, and \`this.context\`. */`,
69
- `export class Feature extends BaseFeature<${Dependencies}> {}`,
69
+ "export abstract class Feature<C extends Command<R>, R> extends BaseFeature<C, R> {",
70
+ ...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
71
+ "}",
70
72
  "",
71
- `/** An ApplicationService of «${alias}»: the same, and \`this.feature.<Name>\` for each Feature. */`,
72
- "export class ApplicationService extends BaseApplicationService<",
73
- ` ${Dependencies},`,
74
- ` ${Name}Features`,
75
- "> {}",
73
+ `/** An ApplicationService of «${alias}»: the same, and \`this.featureBus.execute(dto)\`. */`,
74
+ "export abstract class ApplicationService<C extends Command<R>, R> extends BaseApplicationService<C, R> {",
75
+ ...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
76
+ "}",
76
77
  "",
77
78
  `export function config${Name}BoundedContext(name: string, transport: Transport) {`,
78
- ` return registerDependencies(new UseBoundedContext<${Dependencies}>(name), transport);`,
79
+ ` return registerDependencies(new BoundedContext<${Dependencies}>(name), transport);`,
79
80
  "}",
80
81
  "",
81
82
  ].join("\n");
82
83
  const services = [
83
84
  HEADER(`What «${alias}» offers`),
84
85
  "",
85
- "/**",
86
- ` * Each service by the name it is reached by — \`${name}.Issues.close(…)\`. A Feature groups the`,
87
- " * operations of one part of the domain; an ApplicationService composes the Features:",
88
- " *",
89
- " * // services/issues.ts",
90
- ` * export class Issues extends Feature {`,
91
- " * close(input: dto.CommandCloseIssue) {",
92
- " * return this.client.closeIssue(input);",
93
- " * }",
94
- " * }",
95
- " */",
96
- "export const features = {};",
97
- "export const applicationServices = {};",
86
+ `import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
87
+ `import type { ${Dependencies} } from "${at("..", "infrastructure", "dependencies")}";`,
98
88
  "",
99
- `export type ${Name}Features = typeof features;`,
89
+ "/** Register local Command classes with their Features and ApplicationServices.",
90
+ " * Generated DTO types are remote contracts, not runtime command constructors: import",
91
+ ` * them under their own name — \`import type * as remote from "${at("..", "generated", "domain")}"\` —`,
92
+ " * and let a local Command implement one (`implements remote.CommandCancelIssue`).",
93
+ " * Register with context.feature(CommandClass)(FeatureClass) or",
94
+ " * context.appService(CommandClass)(ApplicationServiceClass).",
95
+ " */",
96
+ `export function registerServices(_context: BoundedContext<${Dependencies}>): void {}`,
100
97
  "",
101
98
  ].join("\n");
102
99
  const index = [
@@ -105,13 +102,11 @@ export function scaffoldFilesOf(metamodel, options = {}) {
105
102
  `import { transport } from "${transport}";`,
106
103
  "",
107
104
  `import { config${Name}BoundedContext } from "${at(".", "infrastructure", "bounded_context")}";`,
108
- `import { applicationServices, features } from "${at(".", "services")}";`,
105
+ `import { registerServices } from "${at(".", "services")}";`,
109
106
  "",
110
- `/** «${alias}»: \`${name}.<Service>.<method>(…)\` — from a screen, a test, another bounded context. */`,
111
- `export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, transport).register({`,
112
- " features,",
113
- " applicationServices,",
114
- "});",
107
+ `/** «${alias}»: \`${name}.execute(new Command(...))\`. */`,
108
+ `export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, transport);`,
109
+ `registerServices(${name});`,
115
110
  "",
116
111
  ].join("\n");
117
112
  return {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The values of the wire, as the backend writes them: ids, a day and an instant, `Decimal`,
3
+ * `Money`, a to-many as a page of its records, and how any value is written and read.
4
+ */
5
+ export { collectionOf, type EntityCollection, type HeldCollection, isCollection, isPartial, relatedCriteria, type RelationKeys, totalOf, } from "./collection.js";
6
+ export { Decimal, type DecimalLike } from "./decimal.js";
7
+ export { isEntityId, momentOf, newEntityId } from "./ids.js";
8
+ export { Money } from "./money.js";
9
+ export { BYTES, type JsonValue, pack, readValues, type Rebuild, rebuilt, unpack, valuesOf, } from "./serialization.js";
10
+ export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./time.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The values of the wire, as the backend writes them: ids, a day and an instant, `Decimal`,
3
+ * `Money`, a to-many as a page of its records, and how any value is written and read.
4
+ */
5
+ export { collectionOf, isCollection, isPartial, relatedCriteria, totalOf, } from "./collection.js";
6
+ export { Decimal } from "./decimal.js";
7
+ export { isEntityId, momentOf, newEntityId } from "./ids.js";
8
+ export { Money } from "./money.js";
9
+ export { BYTES, pack, readValues, rebuilt, unpack, valuesOf, } from "./serialization.js";
10
+ export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./time.js";
@@ -0,0 +1,6 @@
1
+ import type { ContextStore } from "../domain/store.js";
2
+ /**
3
+ * The context in this process's memory: the default, on every platform. A test gets a fresh one
4
+ * from `configureProcess()`.
5
+ */
6
+ export declare function memoryContexts(): ContextStore;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The context in this process's memory: the default, on every platform. A test gets a fresh one
3
+ * from `configureProcess()`.
4
+ */
5
+ export function memoryContexts() {
6
+ const held = new Map();
7
+ const versions = new Map();
8
+ const listeners = new Map();
9
+ const changed = (key) => {
10
+ versions.set(key, (versions.get(key) ?? 0) + 1);
11
+ for (const listener of [...(listeners.get(key) ?? [])])
12
+ listener();
13
+ };
14
+ return {
15
+ load: (key) => held.get(key),
16
+ save(key, values) {
17
+ // A copy: whoever handed the values may keep changing their object.
18
+ held.set(key, { ...values });
19
+ changed(key);
20
+ },
21
+ delete(key) {
22
+ if (!held.delete(key))
23
+ return;
24
+ changed(key);
25
+ },
26
+ version: (key) => versions.get(key) ?? 0,
27
+ watch(key, listener) {
28
+ const own = listeners.get(key) ?? new Set();
29
+ listeners.set(key, own);
30
+ own.add(listener);
31
+ return () => void own.delete(listener);
32
+ },
33
+ };
34
+ }
@@ -0,0 +1,3 @@
1
+ import { type ContextCodec } from "../domain/codec.js";
2
+ /** The codec for anything outside the codebase: only the values that travel, as JSON. */
3
+ export declare const plainCodec: ContextCodec;
@@ -0,0 +1,16 @@
1
+ import { travelling, } from "../domain/codec.js";
2
+ /** The codec for anything outside the codebase: only the values that travel, as JSON. */
3
+ export const plainCodec = {
4
+ dumps: (values) => JSON.stringify(travelling(values)),
5
+ loads(text) {
6
+ try {
7
+ const found = JSON.parse(text);
8
+ return found !== null && typeof found === "object" && !Array.isArray(found)
9
+ ? found
10
+ : {};
11
+ }
12
+ catch {
13
+ return {};
14
+ }
15
+ },
16
+ };
@@ -23,4 +23,3 @@ export interface ContextCodec {
23
23
  export declare function travels(value: unknown): boolean;
24
24
  /** The values of `context` that travel; the rest stays in this process. */
25
25
  export declare function travelling(context: ContextValues): ContextValues;
26
- export declare const plainCodec: ContextCodec;
@@ -28,17 +28,3 @@ export function travels(value) {
28
28
  export function travelling(context) {
29
29
  return Object.fromEntries(Object.entries(context).filter(([, value]) => travels(value)));
30
30
  }
31
- export const plainCodec = {
32
- dumps: (values) => JSON.stringify(travelling(values)),
33
- loads(text) {
34
- try {
35
- const found = JSON.parse(text);
36
- return found !== null && typeof found === "object" && !Array.isArray(found)
37
- ? found
38
- : {};
39
- }
40
- catch {
41
- return {};
42
- }
43
- },
44
- };
@@ -0,0 +1,33 @@
1
+ import type { Span } from "../../observability/domain/tracing.js";
2
+ /**
3
+ * One run of a use case: its span, what it runs with, and who ran it.
4
+ *
5
+ * Context: sincpro_framework keeps the running use case in a context variable; a browser has
6
+ * nothing like it that survives an `await` (AsyncContext is a proposal, Zone.js patches the
7
+ * world), and a global stack mixes the parents of two use cases running at once — which a
8
+ * screen with several windows does all the time. So an execution is a value, handed to what
9
+ * the use case calls: the Features its `featureBus` runs, and its dependencies (`BIND`).
10
+ */
11
+ export type Execution = {
12
+ /** UUID v7, as 32 hex: this run. */
13
+ readonly id: string;
14
+ /** `<bounded context>.<Service>` — the use case's identity, as the backend names its own. */
15
+ readonly useCase: string;
16
+ /** The bounded context the use case belongs to. */
17
+ readonly boundedContext: string;
18
+ /** The run that executed this one: an ApplicationService, for its Features. */
19
+ readonly parent: Execution | undefined;
20
+ readonly span: Span;
21
+ /** What it runs with, read once where the gesture started and inherited by every child. */
22
+ readonly context: Readonly<Record<string, unknown>>;
23
+ };
24
+ /**
25
+ * A dependency that has something to say about the execution it is used in: given that
26
+ * execution, it answers itself bound to it. The transport, every generated client and the
27
+ * predefined loggers are; any other dependency of a bounded context is handed as it is.
28
+ */
29
+ export declare const BIND: unique symbol;
30
+ export type Bindable<T> = {
31
+ [BIND]: (execution: Execution) => T;
32
+ };
33
+ export declare function isBindable<T>(value: T): value is T & Bindable<T>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * A dependency that has something to say about the execution it is used in: given that
3
+ * execution, it answers itself bound to it. The transport, every generated client and the
4
+ * predefined loggers are; any other dependency of a bounded context is handed as it is.
5
+ */
6
+ export const BIND = Symbol.for("sincpro.client-framework.bind");
7
+ export function isBindable(value) {
8
+ return (value !== null &&
9
+ (typeof value === "object" || typeof value === "function") &&
10
+ typeof value[BIND] === "function");
11
+ }
@@ -44,8 +44,3 @@ export interface ContextStore {
44
44
  */
45
45
  watch?(key: string, listener: () => void): () => void;
46
46
  }
47
- /**
48
- * The context in this process's memory: the default, on every platform. A test gets a fresh one
49
- * from `configureProcess()`.
50
- */
51
- export declare function memoryContexts(): ContextStore;
@@ -23,37 +23,4 @@
23
23
  * Reads are synchronous on purpose: a use case reads its context where a gesture starts, and an
24
24
  * asynchronous store would make every use case wait for it.
25
25
  */
26
- /**
27
- * The context in this process's memory: the default, on every platform. A test gets a fresh one
28
- * from `configureProcess()`.
29
- */
30
- export function memoryContexts() {
31
- const held = new Map();
32
- const versions = new Map();
33
- const listeners = new Map();
34
- const changed = (key) => {
35
- versions.set(key, (versions.get(key) ?? 0) + 1);
36
- for (const listener of [...(listeners.get(key) ?? [])])
37
- listener();
38
- };
39
- return {
40
- load: (key) => held.get(key),
41
- save(key, values) {
42
- // A copy: whoever handed the values may keep changing their object.
43
- held.set(key, { ...values });
44
- changed(key);
45
- },
46
- delete(key) {
47
- if (!held.delete(key))
48
- return;
49
- changed(key);
50
- },
51
- version: (key) => versions.get(key) ?? 0,
52
- watch(key, listener) {
53
- const own = listeners.get(key) ?? new Set();
54
- listeners.set(key, own);
55
- own.add(listener);
56
- return () => void own.delete(listener);
57
- },
58
- };
59
- }
26
+ export {};
@@ -1,4 +1,4 @@
1
- import type { ContextStore, ContextValues } from "./store.js";
1
+ import type { ContextStore, ContextValues } from "../domain/store.js";
2
2
  /**
3
3
  * One level of the context: the process's (`processContext`) or a bounded context's
4
4
  * (`project.context`), read and written the same way on every platform.
@@ -0,0 +1,17 @@
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, type ContextCodec, travelling, travels, } from "./domain/codec.js";
14
+ export { BIND, type Bindable, type Execution, } from "./domain/execution.js";
15
+ export type { ContextStore, ContextValues, } from "./domain/store.js";
16
+ export type { Context } from "./entrypoint/context.js";
17
+ export { processContext } from "./infrastructure/process.js";
@@ -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>;