@sincpro/client-framework 0.6.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/AGENTS.md +95 -25
  2. package/MANIFESTO.md +103 -40
  3. package/README.md +608 -333
  4. package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
  5. package/dist/circuit_breaker/domain/circuit.js +37 -0
  6. package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
  7. package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
  8. package/dist/circuit_breaker/index.d.ts +9 -0
  9. package/dist/circuit_breaker/index.js +9 -0
  10. package/dist/client/adapters/json-rpc.d.ts +40 -0
  11. package/dist/client/adapters/json-rpc.js +97 -0
  12. package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
  13. package/dist/client/{memory.js → adapters/memory.js} +2 -2
  14. package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
  15. package/dist/client/adapters/rest-operations.d.ts +19 -0
  16. package/dist/client/adapters/rest-operations.js +66 -0
  17. package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
  18. package/dist/client/{rest.js → adapters/rest.js} +2 -2
  19. package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
  20. package/dist/client/domain/operation.d.ts +73 -0
  21. package/dist/client/domain/operation.js +19 -0
  22. package/dist/client/index.d.ts +20 -0
  23. package/dist/client/index.js +20 -0
  24. package/dist/client/services/guarded.d.ts +17 -0
  25. package/dist/client/services/guarded.js +19 -0
  26. package/dist/client/services/propagation.d.ts +45 -0
  27. package/dist/{services/execution.js → client/services/propagation.js} +49 -36
  28. package/dist/client/{retry.js → services/retry.js} +1 -1
  29. package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
  30. package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
  31. package/dist/codegen/{names.js → domain/names.js} +3 -3
  32. package/dist/codegen/{cli.js → entrypoint/cli.js} +15 -14
  33. package/dist/codegen/index.d.ts +7 -7
  34. package/dist/codegen/index.js +5 -10
  35. package/dist/codegen/services/client.d.ts +15 -0
  36. package/dist/codegen/services/client.js +111 -0
  37. package/dist/codegen/services/domain.d.ts +20 -0
  38. package/dist/codegen/{domain.js → services/domain.js} +82 -25
  39. package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
  40. package/dist/codegen/{entities.js → services/entities.js} +18 -4
  41. package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
  42. package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +8 -5
  43. package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
  44. package/dist/common/failures.d.ts +83 -0
  45. package/dist/common/failures.js +212 -0
  46. package/dist/common/index.d.ts +11 -0
  47. package/dist/common/index.js +11 -0
  48. package/dist/context/adapters/memory.d.ts +6 -0
  49. package/dist/context/adapters/memory.js +34 -0
  50. package/dist/context/adapters/plain-codec.d.ts +3 -0
  51. package/dist/context/adapters/plain-codec.js +16 -0
  52. package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
  53. package/dist/context/{codec.js → domain/codec.js} +0 -14
  54. package/dist/context/domain/execution.d.ts +45 -0
  55. package/dist/context/domain/execution.js +11 -0
  56. package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
  57. package/dist/context/{store.js → domain/store.js} +1 -34
  58. package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
  59. package/dist/context/index.d.ts +17 -0
  60. package/dist/context/index.js +15 -0
  61. package/dist/context/infrastructure/process.d.ts +13 -0
  62. package/dist/context/infrastructure/process.js +35 -0
  63. package/dist/core/domain/bus.d.ts +27 -0
  64. package/dist/core/domain/bus.js +26 -0
  65. package/dist/core/domain/dto.d.ts +62 -0
  66. package/dist/core/domain/dto.js +43 -0
  67. package/dist/core/domain/errors.d.ts +18 -0
  68. package/dist/core/domain/errors.js +1 -0
  69. package/dist/core/domain/pipeline.d.ts +89 -0
  70. package/dist/core/domain/pipeline.js +1 -0
  71. package/dist/core/domain/services.d.ts +47 -0
  72. package/dist/core/domain/services.js +31 -0
  73. package/dist/core/entrypoint/bounded-context.d.ts +139 -0
  74. package/dist/core/entrypoint/bounded-context.js +426 -0
  75. package/dist/core/entrypoint/screens.d.ts +6 -0
  76. package/dist/core/entrypoint/screens.js +136 -0
  77. package/dist/core/index.d.ts +19 -0
  78. package/dist/core/index.js +17 -0
  79. package/dist/core/infrastructure/listeners.d.ts +28 -0
  80. package/dist/core/infrastructure/listeners.js +34 -0
  81. package/dist/core/infrastructure/process.d.ts +56 -0
  82. package/dist/core/infrastructure/process.js +35 -0
  83. package/dist/core/services/binding.d.ts +6 -0
  84. package/dist/core/services/binding.js +16 -0
  85. package/dist/core/services/error-handlers.d.ts +11 -0
  86. package/dist/core/services/error-handlers.js +21 -0
  87. package/dist/core/services/execution.d.ts +66 -0
  88. package/dist/core/services/execution.js +216 -0
  89. package/dist/core/services/interceptors.d.ts +15 -0
  90. package/dist/core/services/interceptors.js +25 -0
  91. package/dist/core/services/listening.d.ts +17 -0
  92. package/dist/core/services/listening.js +25 -0
  93. package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
  94. package/dist/event_driven/adapters/memory-event-store.js +23 -0
  95. package/dist/event_driven/adapters/stored-queue.d.ts +67 -0
  96. package/dist/event_driven/adapters/stored-queue.js +192 -0
  97. package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
  98. package/dist/event_driven/adapters/sync-queue.js +19 -0
  99. package/dist/event_driven/domain/envelope.d.ts +28 -0
  100. package/dist/event_driven/domain/envelope.js +19 -0
  101. package/dist/event_driven/domain/queue.d.ts +40 -0
  102. package/dist/event_driven/domain/queue.js +1 -0
  103. package/dist/event_driven/domain/store.d.ts +56 -0
  104. package/dist/event_driven/domain/store.js +1 -0
  105. package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
  106. package/dist/event_driven/entrypoint/publisher.js +35 -0
  107. package/dist/event_driven/entrypoint/subscriber.d.ts +34 -0
  108. package/dist/event_driven/entrypoint/subscriber.js +51 -0
  109. package/dist/event_driven/index.d.ts +19 -0
  110. package/dist/event_driven/index.js +17 -0
  111. package/dist/index.d.ts +21 -38
  112. package/dist/index.js +21 -38
  113. package/dist/model/index.d.ts +10 -0
  114. package/dist/model/index.js +10 -0
  115. package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
  116. package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
  117. package/dist/model/{schema.js → services/schema.js} +1 -1
  118. package/dist/observability/adapters/console.d.ts +12 -0
  119. package/dist/observability/adapters/console.js +35 -0
  120. package/dist/observability/adapters/memory.d.ts +41 -0
  121. package/dist/observability/adapters/memory.js +49 -0
  122. package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
  123. package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
  124. package/dist/observability/domain/errors.d.ts +23 -0
  125. package/dist/observability/domain/errors.js +8 -0
  126. package/dist/observability/domain/logging.d.ts +44 -0
  127. package/dist/observability/domain/logging.js +1 -0
  128. package/dist/observability/domain/tracing.d.ts +49 -0
  129. package/dist/observability/domain/tracing.js +20 -0
  130. package/dist/observability/index.d.ts +16 -0
  131. package/dist/observability/index.js +4 -0
  132. package/dist/observability/infrastructure/registry.d.ts +21 -0
  133. package/dist/observability/infrastructure/registry.js +18 -0
  134. package/dist/observability/services/logging.d.ts +27 -0
  135. package/dist/observability/services/logging.js +106 -0
  136. package/dist/observability/services/outcome.d.ts +12 -0
  137. package/dist/observability/services/outcome.js +17 -0
  138. package/dist/{services/report.js → observability/services/reporting.js} +4 -3
  139. package/dist/testing/index.d.ts +3 -33
  140. package/dist/testing/index.js +5 -37
  141. package/package.json +16 -8
  142. package/dist/codegen/client.d.ts +0 -14
  143. package/dist/codegen/client.js +0 -201
  144. package/dist/codegen/domain.d.ts +0 -15
  145. package/dist/observability/ports.d.ts +0 -65
  146. package/dist/observability/ports.js +0 -21
  147. package/dist/services/bounded-context.d.ts +0 -79
  148. package/dist/services/bounded-context.js +0 -261
  149. package/dist/services/execution.d.ts +0 -63
  150. package/dist/services/feature.d.ts +0 -85
  151. package/dist/services/feature.js +0 -86
  152. package/dist/services/process.d.ts +0 -60
  153. package/dist/services/process.js +0 -57
  154. package/dist/services/screens.d.ts +0 -2
  155. package/dist/services/screens.js +0 -76
  156. /package/dist/{adapters → client/adapters}/parquet.js +0 -0
  157. /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
  158. /package/dist/client/{contract.js → domain/contract.js} +0 -0
  159. /package/dist/client/{entity.js → domain/entity.js} +0 -0
  160. /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
  161. /package/dist/client/{transport.js → domain/transport.js} +0 -0
  162. /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
  163. /package/dist/client/{discovery.js → services/discovery.js} +0 -0
  164. /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
  165. /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
  166. /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
  167. /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
  168. /package/dist/codegen/{check.js → services/check.js} +0 -0
  169. /package/dist/codegen/{rest.js → services/rest.js} +0 -0
  170. /package/dist/context/{context.js → entrypoint/context.js} +0 -0
  171. /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
  172. /package/dist/model/{meta.js → domain/meta.js} +0 -0
  173. /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
  174. /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
  175. /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
  176. /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The seam with a server: the `Transport` port and its implementations, entities with typed
3
+ * commands, the service's client contract, and how a run's trace and context reach the wire.
4
+ *
5
+ * domain/ the port and the server's words: `Transport`, `TransportError`, `entity()`, the
6
+ * client contract
7
+ * adapters/ REST, memory, and Parquet (optional entry `./parquet`)
8
+ * services/ discovery, retry keys, propagation (`traceparent`, `sincpro-context`)
9
+ */
10
+ export { JsonRpcError, jsonRpcOperations, type JsonRpcOptions, } from "./adapters/json-rpc.js";
11
+ export { type MemoryOptions, type MemoryTable, memoryTransport, } from "./adapters/memory.js";
12
+ export { type RestOptions, restTransport, urlOf, type VersionDrift, } from "./adapters/rest.js";
13
+ export { restOperations } from "./adapters/rest-operations.js";
14
+ export { type ClientContract, entityKeyOf, kindOf, metasOf, versionsOf, } from "./domain/contract.js";
15
+ export { bodyFor, type CommandAnswers, type CommandBodies, type CommandRoute, type Entity, entity, type EntityRef, type EntityRoutes, type ExportFormat, exportRoute, listRoute, NEW_PARAM, pathFor, PICK_PARAM, QUERY_PARAM, SCOPE_PARAM, scopeOfRef, startingValuesOf, type SummaryFields, type SummaryPart, TRAIL_PARAM, WITH_PARAM, type WriteRoute, writeRoute, } from "./domain/entity.js";
16
+ export { OperationNotPublished, type OperationOptions, type OperationTransport, type RemoteOperation, } from "./domain/operation.js";
17
+ export { type CallOptions, type Decoder, type Download, downloadRoute, type Draft, type Method, type One, readRoute, type SendOptions, type Transport, TransportError, } from "./domain/transport.js";
18
+ export { guardedTransport } from "./services/guarded.js";
19
+ export { bindable, bindOperations, bindTransport, headersOf, } from "./services/propagation.js";
20
+ export { type RetryKeys, retryKeys, unanswered, } from "./services/retry.js";
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The seam with a server: the `Transport` port and its implementations, entities with typed
3
+ * commands, the service's client contract, and how a run's trace and context reach the wire.
4
+ *
5
+ * domain/ the port and the server's words: `Transport`, `TransportError`, `entity()`, the
6
+ * client contract
7
+ * adapters/ REST, memory, and Parquet (optional entry `./parquet`)
8
+ * services/ discovery, retry keys, propagation (`traceparent`, `sincpro-context`)
9
+ */
10
+ export { JsonRpcError, jsonRpcOperations, } from "./adapters/json-rpc.js";
11
+ export { memoryTransport, } from "./adapters/memory.js";
12
+ export { restTransport, urlOf, } from "./adapters/rest.js";
13
+ export { restOperations } from "./adapters/rest-operations.js";
14
+ export { entityKeyOf, kindOf, metasOf, versionsOf, } from "./domain/contract.js";
15
+ export { bodyFor, entity, exportRoute, listRoute, NEW_PARAM, pathFor, PICK_PARAM, QUERY_PARAM, SCOPE_PARAM, scopeOfRef, startingValuesOf, TRAIL_PARAM, WITH_PARAM, writeRoute, } from "./domain/entity.js";
16
+ export { OperationNotPublished, } from "./domain/operation.js";
17
+ export { downloadRoute, readRoute, TransportError, } from "./domain/transport.js";
18
+ export { guardedTransport } from "./services/guarded.js";
19
+ export { bindable, bindOperations, bindTransport, headersOf, } from "./services/propagation.js";
20
+ export { retryKeys, unanswered, } from "./services/retry.js";
@@ -0,0 +1,17 @@
1
+ import type { CircuitBreaker } from "../../circuit_breaker/entrypoint/circuit-breaker.js";
2
+ import { type OperationTransport } from "../domain/operation.js";
3
+ import type { Transport } from "../domain/transport.js";
4
+ /**
5
+ * `transport`, every call through `breaker` — an `OperationTransport` of any wire, or REST's
6
+ * `Transport`:
7
+ *
8
+ * const operations = guardedTransport(jsonRpcOperations({ endpoint }), new CircuitBreaker("forge"));
9
+ * project.addDependency("client", clientOf(operations));
10
+ *
11
+ * Context: the circuit belongs where the dependency is — the transport —, not in each Feature. A
12
+ * run binds it as it binds any transport, so the trace and the context still travel; an open
13
+ * circuit fails at once with `CircuitOpen` (`unavailable`, with its wait), which the outcome, a
14
+ * stored queue and a screen already read.
15
+ */
16
+ export declare function guardedTransport(transport: OperationTransport, breaker: CircuitBreaker): OperationTransport;
17
+ export declare function guardedTransport(transport: Transport, breaker: CircuitBreaker): Transport;
@@ -0,0 +1,19 @@
1
+ import { isOperationTransport, } from "../domain/operation.js";
2
+ export function guardedTransport(transport, breaker) {
3
+ if (isOperationTransport(transport))
4
+ return {
5
+ call: (operation, input, options) => breaker.call(() => transport.call(operation, input, options)),
6
+ };
7
+ const guarded = {
8
+ list: (route, criteria, signal, options) => breaker.call(() => transport.list(route, criteria, signal, options)),
9
+ one: (route, signal, options) => breaker.call(() => transport.one(route, signal, options)),
10
+ send: (method, route, body, signal, options) => breaker.call(() => transport.send(method, route, body, signal, options)),
11
+ };
12
+ const read = transport.read;
13
+ if (read !== undefined)
14
+ guarded.read = (route, signal, options) => breaker.call(() => read.call(transport, route, signal, options));
15
+ const download = transport.download;
16
+ if (download !== undefined)
17
+ guarded.download = (route, signal, options) => breaker.call(() => download.call(transport, route, signal, options));
18
+ return guarded;
19
+ }
@@ -0,0 +1,45 @@
1
+ import type { OperationTransport } from "../domain/operation.js";
2
+ import type { Transport } from "../domain/transport.js";
3
+ import { type Execution } from "../../context/domain/execution.js";
4
+ /** Whether a value is a `Transport`: what a run binds to itself even without `BIND`. */
5
+ export declare function isTransport(value: unknown): value is Transport;
6
+ /**
7
+ * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
8
+ * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
9
+ * Nothing when there is no trace and no context that travels.
10
+ */
11
+ export declare function headersOf(execution: Execution): Record<string, string>;
12
+ /**
13
+ * The transport, writing the execution's headers on every call — the caller's own headers of a
14
+ * call still win.
15
+ *
16
+ * Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
17
+ * token, the tenant — the same for every call; these are the use case's, different for two use
18
+ * cases running at once, so they travel per call (`CallOptions`). A call that gives no signal of
19
+ * its own is cancelled with the run.
20
+ */
21
+ export declare function bindTransport(transport: Transport, execution: Execution): Transport;
22
+ /**
23
+ * The operation transport, calling as `execution`: every call carries the run's trace and
24
+ * context as headers and is cancelled with the run — the caller's own headers and signal still
25
+ * win.
26
+ */
27
+ export declare function bindOperations(transport: OperationTransport, execution: Execution): OperationTransport;
28
+ /** A transport of either kind, bound to `execution`. */
29
+ export declare function bound<W extends Transport | OperationTransport>(wire: W, execution: Execution): W;
30
+ /**
31
+ * A client made over a transport, made again over the transport bound to each execution it is
32
+ * used in. What a generated `clientOf(operations)` answers, so a Feature that calls
33
+ * `this.client.sendIssue(…)` sends that request with its own trace, context and signal.
34
+ *
35
+ * export function clientOf(operations: OperationTransport) {
36
+ * return bindable(operations, (operations) => ({
37
+ * sendIssue: (input) => operations.call(operations_.CommandSendIssue, input),
38
+ * }));
39
+ * }
40
+ *
41
+ * Context: it takes the function that makes the client, not the client — rebuilding through
42
+ * `clientOf` itself would make TypeScript infer `clientOf` from its own return. A REST
43
+ * `Transport` binds the same way, for a client written over it by hand.
44
+ */
45
+ export declare function bindable<W extends Transport | OperationTransport, T extends object>(wire: W, make: (wire: W) => T): T;
@@ -1,16 +1,8 @@
1
- import { CONTEXT_HEADER, plainCodec, travels } from "../context/codec.js";
2
- /**
3
- * A dependency that has something to say about the execution it is used in: given that
4
- * execution, it answers itself bound to it. The transport and every generated client are;
5
- * any other dependency of a bounded context is handed as it is.
6
- */
7
- export const BIND = Symbol.for("sincpro.client-framework.bind");
8
- function isBindable(value) {
9
- return (value !== null &&
10
- (typeof value === "object" || typeof value === "function") &&
11
- typeof value[BIND] === "function");
12
- }
13
- function isTransport(value) {
1
+ import { plainCodec } from "../../context/adapters/plain-codec.js";
2
+ import { CONTEXT_HEADER, travels } from "../../context/domain/codec.js";
3
+ import { BIND } from "../../context/domain/execution.js";
4
+ /** Whether a value is a `Transport`: what a run binds to itself even without `BIND`. */
5
+ export function isTransport(value) {
14
6
  if (value === null || typeof value !== "object")
15
7
  return false;
16
8
  const one = value;
@@ -43,51 +35,72 @@ export function headersOf(execution) {
43
35
  *
44
36
  * Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
45
37
  * 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`).
38
+ * cases running at once, so they travel per call (`CallOptions`). A call that gives no signal of
39
+ * its own is cancelled with the run.
47
40
  */
48
41
  export function bindTransport(transport, execution) {
49
42
  const own = headersOf(execution);
50
- if (Object.keys(own).length === 0)
43
+ const cancels = execution.signal;
44
+ if (Object.keys(own).length === 0 && cancels === undefined)
51
45
  return transport;
52
46
  const call = (options) => ({ ...options, headers: { ...own, ...options?.headers } });
53
47
  const bound = {
54
- list: (route, criteria, signal, options) => transport.list(route, criteria, signal, call(options)),
55
- one: (route, signal, options) => transport.one(route, signal, call(options)),
56
- send: (method, route, body, signal, options) => transport.send(method, route, body, signal, call(options)),
48
+ list: (route, criteria, signal, options) => transport.list(route, criteria, signal ?? cancels, call(options)),
49
+ one: (route, signal, options) => transport.one(route, signal ?? cancels, call(options)),
50
+ send: (method, route, body, signal, options) => transport.send(method, route, body, signal ?? cancels, call(options)),
57
51
  };
58
52
  const read = transport.read;
59
53
  if (read !== undefined)
60
- bound.read = (route, signal, options) => read.call(transport, route, signal, call(options));
54
+ bound.read = (route, signal, options) => read.call(transport, route, signal ?? cancels, call(options));
61
55
  const download = transport.download;
62
56
  if (download !== undefined)
63
- bound.download = (route, signal, options) => download.call(transport, route, signal, call(options));
57
+ bound.download = (route, signal, options) => download.call(transport, route, signal ?? cancels, call(options));
64
58
  return bound;
65
59
  }
60
+ /**
61
+ * The operation transport, calling as `execution`: every call carries the run's trace and
62
+ * context as headers and is cancelled with the run — the caller's own headers and signal still
63
+ * win.
64
+ */
65
+ export function bindOperations(transport, execution) {
66
+ const own = headersOf(execution);
67
+ const cancels = execution.signal;
68
+ if (Object.keys(own).length === 0 && cancels === undefined)
69
+ return transport;
70
+ return {
71
+ call: (operation, input, options) => transport.call(operation, input, {
72
+ ...options,
73
+ headers: { ...own, ...options?.headers },
74
+ ...((options?.signal ?? cancels) ? { signal: options?.signal ?? cancels } : {}),
75
+ }),
76
+ };
77
+ }
78
+ /** A transport of either kind, bound to `execution`. */
79
+ export function bound(wire, execution) {
80
+ return (isTransport(wire)
81
+ ? bindTransport(wire, execution)
82
+ : bindOperations(wire, execution));
83
+ }
66
84
  /**
67
85
  * A client made over a transport, made again over the transport bound to each execution it is
68
- * used in. What a generated `clientOf(transport)` answers, so a Feature that calls
69
- * `this.client.sendIssue(…)` sends that request with its own trace.
86
+ * used in. What a generated `clientOf(operations)` answers, so a Feature that calls
87
+ * `this.client.sendIssue(…)` sends that request with its own trace, context and signal.
70
88
  *
71
- * export function clientOf(transport: Transport) {
72
- * return bindable(transport, (transport) => ({ sendIssue: (body) => transport.send(…) }));
89
+ * export function clientOf(operations: OperationTransport) {
90
+ * return bindable(operations, (operations) => ({
91
+ * sendIssue: (input) => operations.call(operations_.CommandSendIssue, input),
92
+ * }));
73
93
  * }
74
94
  *
75
95
  * Context: it takes the function that makes the client, not the client — rebuilding through
76
- * `clientOf` itself would make TypeScript infer `clientOf` from its own return.
96
+ * `clientOf` itself would make TypeScript infer `clientOf` from its own return. A REST
97
+ * `Transport` binds the same way, for a client written over it by hand.
77
98
  */
78
- export function bindable(transport, make) {
79
- const client = make(transport);
99
+ export function bindable(wire, make) {
100
+ const client = make(wire);
80
101
  Object.defineProperty(client, BIND, {
81
- value: (execution) => bindable(bindTransport(transport, execution), make),
102
+ value: (execution) => bindable(bound(wire, execution), make),
82
103
  enumerable: false,
83
104
  });
84
105
  return client;
85
106
  }
86
- /** A dependency as the execution should see it: bound when it can be, as it is otherwise. */
87
- export function bindDependency(value, execution) {
88
- if (isBindable(value))
89
- return value[BIND](execution);
90
- if (isTransport(value))
91
- return bindTransport(value, execution);
92
- return value;
93
- }
@@ -1,4 +1,4 @@
1
- import { TransportError } from "./transport.js";
1
+ import { TransportError } from "../domain/transport.js";
2
2
  /** Whether a failure leaves it unknown if the Command ran — so a retry must reuse its key. */
3
3
  export function unanswered(error) {
4
4
  if (!(error instanceof TransportError))
@@ -53,6 +53,21 @@ export type Operation = {
53
53
  /** The operation that replaces it, by identity. */
54
54
  replacement?: string | null;
55
55
  };
56
+ /**
57
+ * A domain event of the bounded context — in a contract exported with its events
58
+ * (`export-client-contract --events`), keyed by its wire name (`DomainEvent.name`).
59
+ */
60
+ export type EventDescription = {
61
+ /** Its class's name on the server: the type it is written as. */
62
+ title: string;
63
+ description?: string | null;
64
+ /** One of the bounded context's own events: its aggregates record it. */
65
+ recorded: boolean;
66
+ /** A Feature of the bounded context is registered for it: it subscribes. */
67
+ handled: boolean;
68
+ /** Every field it carries — the envelope and what happened —, `name` a constant. */
69
+ schema: Schema;
70
+ };
56
71
  /** How REST reaches an operation: its method and its whole path, `{field}` where a field goes. */
57
72
  export type RestBinding = {
58
73
  method: string;
@@ -80,14 +95,14 @@ export type Metamodel = {
80
95
  /** The name the gateways publish it under (`FastApiGateway({"project": …})`): what a client
81
96
  * names the context by. */
82
97
  alias?: string;
83
- /** The bounded context's name, as sincpro_framework 3.32 wrote it before `bounded_context`. */
84
- context?: string;
85
98
  version: string;
86
99
  /** Every shape the contract names: an entity (it has an `identity`) or a value it holds. */
87
100
  models: Readonly<Record<string, Schema>>;
88
101
  /** By the Command's identity, `"<bounded_context>.<Class>"`. */
89
102
  operations: Readonly<Record<string, Operation>>;
90
103
  $defs?: Readonly<Record<string, Schema>>;
104
+ /** Its domain events, by wire name — only when the contract was exported with them. */
105
+ events?: Readonly<Record<string, EventDescription>>;
91
106
  /** By wire, then by the Command's identity. */
92
107
  bindings?: Bindings;
93
108
  };
@@ -13,7 +13,7 @@ export declare function keyOf(name: string): string;
13
13
  export declare function sorted<V>(record: Readonly<Record<string, V>>): [string, V][];
14
14
  /** `{ Issue: "issue_id" }`: a record of texts as TypeScript writes it, keys sorted. */
15
15
  export declare function recordLiteral(record: Readonly<Record<string, string>>): string;
16
- /** The bounded context's own name: `bounded_context`, or `context` in a 3.32 contract. */
16
+ /** The bounded context's own name: what every operation's identity starts with. */
17
17
  export declare function contextOf(metamodel: Metamodel): string;
18
18
  /** What a client names the context by: its `alias`, else its own name. */
19
19
  export declare function aliasOf(metamodel: Metamodel): string;
@@ -30,7 +30,7 @@ export declare function localOf(metamodel: Metamodel, identity: string): string;
30
30
  *
31
31
  * Context: the ubiquitous language. The backend writes `CommandCancelIssue` → `ResponseCancelIssue`,
32
32
  * and a client that calls it `CommandCancelIssueAnswer` makes a person learn two words for one
33
- * thing. The `<Name>Answer` it used to write stays as a deprecated alias of the backend's name.
33
+ * thing.
34
34
  */
35
35
  export declare function answerOf(metamodel: Metamodel, identity: string): string;
36
36
  /** The method an operation is called by: `CommandSendIssue` → `sendIssue`. */
@@ -32,9 +32,9 @@ export function recordLiteral(record) {
32
32
  const entries = sorted(record).map(([key, value]) => `${keyOf(key)}: ${JSON.stringify(value)}`);
33
33
  return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
34
34
  }
35
- /** The bounded context's own name: `bounded_context`, or `context` in a 3.32 contract. */
35
+ /** The bounded context's own name: what every operation's identity starts with. */
36
36
  export function contextOf(metamodel) {
37
- const name = metamodel.bounded_context ?? metamodel.context;
37
+ const name = metamodel.bounded_context;
38
38
  if (name === undefined)
39
39
  throw new Error("the contract names no bounded context");
40
40
  return name;
@@ -100,7 +100,7 @@ function answersOf(metamodel) {
100
100
  *
101
101
  * Context: the ubiquitous language. The backend writes `CommandCancelIssue` → `ResponseCancelIssue`,
102
102
  * and a client that calls it `CommandCancelIssueAnswer` makes a person learn two words for one
103
- * thing. The `<Name>Answer` it used to write stays as a deprecated alias of the backend's name.
103
+ * thing.
104
104
  */
105
105
  export function answerOf(metamodel, identity) {
106
106
  let known = ANSWERS.get(metamodel);
@@ -1,19 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { CheckError, checkImports } from "./check.js";
5
- import { clientFileOf } from "./client.js";
6
- import { domainFileOf } from "./domain.js";
7
- import { entitiesFileOf } from "./entities.js";
8
- import { aliasOf } from "./names.js";
9
- import { scaffoldFilesOf } from "./scaffold.js";
4
+ import { aliasOf } from "../domain/names.js";
5
+ import { CheckError, checkImports } from "../services/check.js";
6
+ import { clientFileOf } from "../services/client.js";
7
+ import { domainFileOf } from "../services/domain.js";
8
+ import { entitiesFileOf } from "../services/entities.js";
9
+ import { scaffoldFilesOf } from "../services/scaffold.js";
10
10
  /**
11
11
  * `sincpro-client generate <contract.json> --out <dir> [--domain <name>] [--base <path>]` — what an
12
12
  * application's `make client` runs, once per bounded context, over the client contract
13
13
  * sincpro_framework exported (`export-client-contract`):
14
14
  *
15
- * - `domain.ts`: the DTOs, only types;
16
- * - `client.ts`: every operation as data, and `clientOf(transport)`;
15
+ * - `domain.ts`: the DTOs, only types — and the domain events with `DomainEvent`, their union,
16
+ * when the contract was exported with them (`export-client-contract --events`);
17
+ * - `client.ts`: every operation (and event) as data, and `clientOf(operations)` over any wire;
17
18
  * - `entities.ts`: an `entity<T, C>()` per model a screen can list;
18
19
  * - `contract.json`: the snapshot they were written from, for `metasOf`.
19
20
  *
@@ -25,7 +26,7 @@ import { scaffoldFilesOf } from "./scaffold.js";
25
26
  */
26
27
  const USAGE = [
27
28
  "usage: sincpro-client generate <contract.json> --out <dir> [--domain <name>] [--base <path>]",
28
- " sincpro-client scaffold <contract.json> --out <dir> [--transport <module>]",
29
+ " sincpro-client scaffold <contract.json> --out <dir> [--operations <module>]",
29
30
  " sincpro-client check [--root src] [--tsconfig tsconfig.json]",
30
31
  ].join("\n");
31
32
  /** JSON with every key sorted: the snapshot reads the same however the server ordered it. */
@@ -52,7 +53,7 @@ function parsed(args) {
52
53
  flags.set(flag, value);
53
54
  }
54
55
  const out = flags.get("--out");
55
- const known = new Set(command === "generate" ? ["--out", "--domain", "--base"] : ["--out", "--transport"]);
56
+ const known = new Set(command === "generate" ? ["--out", "--domain", "--base"] : ["--out", "--operations"]);
56
57
  if (out === undefined || [...flags.keys()].some((one) => !known.has(one)))
57
58
  return undefined;
58
59
  const domain = flags.get("--domain");
@@ -65,12 +66,12 @@ function parsed(args) {
65
66
  ...(domain === undefined ? {} : { domain }),
66
67
  ...(base === undefined ? {} : { base }),
67
68
  },
68
- transport: flags.get("--transport"),
69
+ operations: flags.get("--operations"),
69
70
  };
70
71
  }
71
72
  /** The skeleton, each file only when it is not there: what is there is the application's. */
72
- function scaffold(metamodel, out, transport) {
73
- const files = scaffoldFilesOf(metamodel, transport === undefined ? {} : { transport });
73
+ function scaffold(metamodel, out, operations) {
74
+ const files = scaffoldFilesOf(metamodel, operations === undefined ? {} : { operations });
74
75
  const written = [];
75
76
  const kept = [];
76
77
  for (const [name, source] of Object.entries(files)) {
@@ -127,7 +128,7 @@ function main(args) {
127
128
  }
128
129
  const metamodel = JSON.parse(readFileSync(asked.source, "utf8"));
129
130
  if (asked.command === "scaffold")
130
- return scaffold(metamodel, asked.out, asked.transport);
131
+ return scaffold(metamodel, asked.out, asked.operations);
131
132
  const files = { "domain.ts": domainFileOf(metamodel) };
132
133
  if (metamodel.bindings !== undefined) {
133
134
  files["client.ts"] = clientFileOf(metamodel, asked.options);
@@ -3,10 +3,10 @@
3
3
  * sincpro_framework exports (PRD_01 phase B). A build step that runs in Node — nothing in
4
4
  * the runtime imports it (`make doctor`), and the package's root entry does not re-export it. `sincpro-client generate` (`cli.ts`) is the same, from a terminal.
5
5
  */
6
- export { CheckError, checkImports, type Place, placeOf, type Violation, violationOf, } from "./check.js";
7
- export { clientFileOf } from "./client.js";
8
- export { domainFileOf } from "./domain.js";
9
- export { entitiesFileOf } from "./entities.js";
10
- export type { Bindings, Metamodel, Operation, RestBinding, Schema, } from "./metamodel.js";
11
- export type { GenerateOptions } from "./rest.js";
12
- export { scaffoldFilesOf, type ScaffoldOptions, } from "./scaffold.js";
6
+ export type { Bindings, Metamodel, Operation, RestBinding, Schema, } from "./domain/metamodel.js";
7
+ export { CheckError, checkImports, type Place, placeOf, type Violation, violationOf, } from "./services/check.js";
8
+ export { clientFileOf } from "./services/client.js";
9
+ export { domainFileOf } from "./services/domain.js";
10
+ export { entitiesFileOf } from "./services/entities.js";
11
+ export type { GenerateOptions } from "./services/rest.js";
12
+ export { scaffoldFilesOf, type ScaffoldOptions, } from "./services/scaffold.js";
@@ -1,10 +1,5 @@
1
- /**
2
- * The generator: a bounded context's DTOs, client and entities out of the client contract
3
- * sincpro_framework exports (PRD_01 phase B). A build step that runs in Node — nothing in
4
- * the runtime imports it (`make doctor`), and the package's root entry does not re-export it. `sincpro-client generate` (`cli.ts`) is the same, from a terminal.
5
- */
6
- export { CheckError, checkImports, placeOf, violationOf, } from "./check.js";
7
- export { clientFileOf } from "./client.js";
8
- export { domainFileOf } from "./domain.js";
9
- export { entitiesFileOf } from "./entities.js";
10
- export { scaffoldFilesOf, } from "./scaffold.js";
1
+ export { CheckError, checkImports, placeOf, violationOf, } from "./services/check.js";
2
+ export { clientFileOf } from "./services/client.js";
3
+ export { domainFileOf } from "./services/domain.js";
4
+ export { entitiesFileOf } from "./services/entities.js";
5
+ export { scaffoldFilesOf, } from "./services/scaffold.js";
@@ -0,0 +1,15 @@
1
+ import type { Metamodel } from "../domain/metamodel.js";
2
+ import { type GenerateOptions } from "./rest.js";
3
+ /**
4
+ * The TypeScript of the client of `metamodel`, as the text of one file next to its `domain.ts`.
5
+ *
6
+ * 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets,
7
+ * whether it answers bytes, and the binding of each wire that publishes it.
8
+ * 2. A method per operation, named by `methodOf`: its input — optional when nothing in it is
9
+ * required — and the call's options (headers, signal, idempotency key), answering its answer.
10
+ * 3. When the contract carries them, every domain event as data, by its type's name: its wire
11
+ * name, and whether the bounded context records it or handles it.
12
+ * Final: `clientOf(transport)` over an `OperationTransport`, bound to each run that uses it; two
13
+ * methods with one name are refused, naming both.
14
+ */
15
+ export declare function clientFileOf(metamodel: Metamodel, options?: GenerateOptions): string;
@@ -0,0 +1,111 @@
1
+ import { aliasOf, answerOf, claimed, deprecationOf, docOf, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, localOf, methodOf, recordLiteral, sorted, } from "../domain/names.js";
2
+ import { restOf, } from "./rest.js";
3
+ /**
4
+ * The client of a bounded context as TypeScript: every operation as data, and a method for each
5
+ * one, called by who it is through an `OperationTransport` — so a script, a test, a Feature or a
6
+ * hook calls the service typed, over whatever wire the application injects.
7
+ *
8
+ * Context: the generated client names an operation and its input, never a route, a verb or an
9
+ * RPC method: those are the contract's bindings, in `operations`, and the adapter reads the one of
10
+ * its wire (`restOperations`, `jsonRpcOperations`, a platform's gRPC). So the same client, and the
11
+ * Features that use it, run over REST today and JSON-RPC or gRPC tomorrow without regenerating.
12
+ * Each method takes the operation's input and answers its answer, as the contract types them
13
+ * (`domain.ts`).
14
+ */
15
+ const WIRES = [
16
+ ["rest", "REST"],
17
+ ["rpc", "JSON-RPC"],
18
+ ["grpc", "gRPC"],
19
+ ["mcp", "MCP"],
20
+ ];
21
+ /** The wires that publish an operation, by name. */
22
+ function wiresOf(metamodel, identity) {
23
+ return WIRES.filter(([wire]) => metamodel.bindings?.[wire]?.[identity] !== undefined).map(([, name]) => name);
24
+ }
25
+ /** Whether an operation answers bytes as they are produced — an export —, not JSON. */
26
+ function isStreamed(operation) {
27
+ return (operation.output !== null &&
28
+ operation.output["x-sincpro-streamed"] !== undefined);
29
+ }
30
+ /** The data of one operation: what it is, and how each wire reaches it. */
31
+ function dataOf(metamodel, identity, operation, rest) {
32
+ const lines = [
33
+ ` identity: ${JSON.stringify(identity)},`,
34
+ ` role: ${JSON.stringify(operation.role)},`,
35
+ ` query: ${operation.query},`,
36
+ ` entities: ${JSON.stringify(operation.entities)},`,
37
+ ` targets: ${recordLiteral(operation.targets)},`,
38
+ ];
39
+ if (isStreamed(operation))
40
+ lines.push(" streamed: true,");
41
+ if (rest !== undefined)
42
+ lines.push(` rest: { method: ${JSON.stringify(rest.method)}, path: ${JSON.stringify(rest.path)} },`);
43
+ const rpc = metamodel.bindings?.rpc?.[identity];
44
+ if (rpc !== undefined)
45
+ lines.push(` rpc: { method: ${JSON.stringify(rpc.method)} },`);
46
+ const grpc = metamodel.bindings?.grpc?.[identity];
47
+ if (grpc !== undefined)
48
+ lines.push(` grpc: { method: ${JSON.stringify(grpc.method)} },`);
49
+ const mcp = metamodel.bindings?.mcp?.[identity];
50
+ if (mcp !== undefined)
51
+ lines.push(` mcp: { tool: ${JSON.stringify(mcp.tool)} },`);
52
+ return ` ${localOf(metamodel, identity)}: {\n${lines.join("\n")}\n },`;
53
+ }
54
+ /**
55
+ * The TypeScript of the client of `metamodel`, as the text of one file next to its `domain.ts`.
56
+ *
57
+ * 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets,
58
+ * whether it answers bytes, and the binding of each wire that publishes it.
59
+ * 2. A method per operation, named by `methodOf`: its input — optional when nothing in it is
60
+ * required — and the call's options (headers, signal, idempotency key), answering its answer.
61
+ * 3. When the contract carries them, every domain event as data, by its type's name: its wire
62
+ * name, and whether the bounded context records it or handles it.
63
+ * Final: `clientOf(transport)` over an `OperationTransport`, bound to each run that uses it; two
64
+ * methods with one name are refused, naming both.
65
+ */
66
+ export function clientFileOf(metamodel, options = {}) {
67
+ const claim = claimed();
68
+ const data = [];
69
+ const methods = [];
70
+ for (const [identity, operation] of sorted(metamodel.operations)) {
71
+ const local = localOf(metamodel, identity);
72
+ data.push(dataOf(metamodel, identity, operation, restOf(metamodel, identity, options)));
73
+ const method = methodOf(local);
74
+ claim(method, `operation ${identity}`);
75
+ const wires = wiresOf(metamodel, identity);
76
+ const doc = docOf([
77
+ operation.description,
78
+ deprecationOf(metamodel, operation),
79
+ wires.length === 0 ? undefined : `Published over ${wires.join(", ")}.`,
80
+ ], " ");
81
+ const optional = (operation.input.required ?? []).length === 0;
82
+ methods.push([
83
+ `${doc} ${method}: (input${optional ? "?" : ""}: dto.${local}, options?: OperationOptions) =>`,
84
+ ` transport.call<dto.${answerOf(metamodel, identity)}>(operations.${local}, input${optional ? " ?? {}" : ""}, options),`,
85
+ ].join("\n"));
86
+ }
87
+ const events = sorted(metamodel.events ?? {}).map(([name, event]) => ` ${identifierOf(event.title)}: { name: ${JSON.stringify(name)}, recorded: ${event.recorded}, handled: ${event.handled} },`);
88
+ const imports = [
89
+ `import { bindable, type OperationOptions, type OperationTransport } from "${FRAMEWORK}";`,
90
+ "",
91
+ `import type * as dto from "${DOMAIN_MODULE}";`,
92
+ ].join("\n");
93
+ const client = [
94
+ "/**",
95
+ ` * «${aliasOf(metamodel)}» over any wire: a method per operation, called by who it is. The transport`,
96
+ " * decides how it travels — `restOperations(restTransport(…))`, `jsonRpcOperations(…)`, a",
97
+ " * platform's —; used by a Feature, each call carries the run's trace, context and signal.",
98
+ " */",
99
+ ].join("\n");
100
+ return `${[
101
+ headerOf("The client", metamodel),
102
+ imports,
103
+ `/** Every operation of «${aliasOf(metamodel)}», as data: what it is, and how each wire reaches it. */\nexport const operations = {\n${data.join("\n")}\n} as const;`,
104
+ ...(events.length === 0
105
+ ? []
106
+ : [
107
+ `/** Every domain event of «${aliasOf(metamodel)}» in the contract, as data: its wire name, and whether it records or handles it. */\nexport const events = {\n${events.join("\n")}\n} as const;`,
108
+ ]),
109
+ `${client}\nexport function clientOf(transport: OperationTransport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
110
+ ].join("\n\n")}\n`;
111
+ }
@@ -0,0 +1,20 @@
1
+ import type { Metamodel } from "../domain/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`) — or as
8
+ * `<Name>Answer` when it has no name.
9
+ * 3. A model an input carries — and the ones it carries, until none is new — as `<Name>Input`:
10
+ * what a caller sends follows the contract's `required`; the server fills the rest.
11
+ * 4. Each domain event, when the contract carries them, by its class's name (`title`): a record
12
+ * as it is received — every field present, `name` its wire name as a literal —, and
13
+ * `DomainEvent`, the union of them all, which a `switch` on `name` narrows.
14
+ * 5. The shared definitions those reach — and the ones those reach, until none is new —, but
15
+ * never one a package of the stack already types (`Criteria`).
16
+ * Final: a header that says where it came from and not to edit it, the imports, then models,
17
+ * definitions, operations and events, each sorted by name; two of them with one TypeScript
18
+ * name are refused, naming both.
19
+ */
20
+ export declare function domainFileOf(metamodel: Metamodel): string;