@sincpro/client-framework 1.0.0 → 1.2.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 (84) hide show
  1. package/AGENTS.md +71 -13
  2. package/MANIFESTO.md +59 -14
  3. package/README.md +456 -55
  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/adapters/rest-operations.d.ts +19 -0
  13. package/dist/client/adapters/rest-operations.js +66 -0
  14. package/dist/client/domain/operation.d.ts +73 -0
  15. package/dist/client/domain/operation.js +19 -0
  16. package/dist/client/index.d.ts +5 -1
  17. package/dist/client/index.js +5 -1
  18. package/dist/client/services/guarded.d.ts +17 -0
  19. package/dist/client/services/guarded.js +19 -0
  20. package/dist/client/services/propagation.d.ts +20 -7
  21. package/dist/client/services/propagation.js +44 -15
  22. package/dist/codegen/entrypoint/cli.js +7 -7
  23. package/dist/codegen/services/client.d.ts +7 -8
  24. package/dist/codegen/services/client.js +50 -148
  25. package/dist/codegen/services/scaffold.d.ts +7 -4
  26. package/dist/codegen/services/scaffold.js +23 -27
  27. package/dist/common/failures.d.ts +83 -0
  28. package/dist/common/failures.js +212 -0
  29. package/dist/common/index.d.ts +1 -0
  30. package/dist/common/index.js +1 -0
  31. package/dist/context/domain/execution.d.ts +12 -0
  32. package/dist/context/entrypoint/context.d.ts +5 -4
  33. package/dist/core/domain/bus.d.ts +16 -7
  34. package/dist/core/domain/bus.js +14 -6
  35. package/dist/core/domain/dto.d.ts +33 -3
  36. package/dist/core/domain/dto.js +24 -1
  37. package/dist/core/domain/errors.d.ts +2 -2
  38. package/dist/core/domain/pipeline.d.ts +89 -0
  39. package/dist/core/domain/pipeline.js +1 -0
  40. package/dist/core/domain/services.d.ts +16 -5
  41. package/dist/core/domain/services.js +5 -2
  42. package/dist/core/entrypoint/bounded-context.d.ts +96 -16
  43. package/dist/core/entrypoint/bounded-context.js +244 -39
  44. package/dist/core/index.d.ts +4 -2
  45. package/dist/core/index.js +3 -2
  46. package/dist/core/infrastructure/listeners.d.ts +28 -0
  47. package/dist/core/infrastructure/listeners.js +34 -0
  48. package/dist/core/services/binding.d.ts +2 -2
  49. package/dist/core/services/binding.js +6 -3
  50. package/dist/core/services/execution.d.ts +59 -12
  51. package/dist/core/services/execution.js +114 -19
  52. package/dist/core/services/interceptors.d.ts +15 -0
  53. package/dist/core/services/interceptors.js +25 -0
  54. package/dist/core/services/listening.d.ts +17 -0
  55. package/dist/core/services/listening.js +25 -0
  56. package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
  57. package/dist/event_driven/adapters/memory-event-store.js +23 -0
  58. package/dist/event_driven/adapters/stored-queue.d.ts +68 -0
  59. package/dist/event_driven/adapters/stored-queue.js +193 -0
  60. package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
  61. package/dist/event_driven/adapters/sync-queue.js +19 -0
  62. package/dist/event_driven/domain/envelope.d.ts +28 -0
  63. package/dist/event_driven/domain/envelope.js +19 -0
  64. package/dist/event_driven/domain/queue.d.ts +40 -0
  65. package/dist/event_driven/domain/queue.js +1 -0
  66. package/dist/event_driven/domain/store.d.ts +56 -0
  67. package/dist/event_driven/domain/store.js +1 -0
  68. package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
  69. package/dist/event_driven/entrypoint/publisher.js +35 -0
  70. package/dist/event_driven/entrypoint/subscriber.d.ts +42 -0
  71. package/dist/event_driven/entrypoint/subscriber.js +62 -0
  72. package/dist/event_driven/entrypoint/subscriptions.d.ts +110 -0
  73. package/dist/event_driven/entrypoint/subscriptions.js +184 -0
  74. package/dist/event_driven/index.d.ts +22 -0
  75. package/dist/event_driven/index.js +20 -0
  76. package/dist/index.d.ts +7 -2
  77. package/dist/index.js +7 -2
  78. package/dist/observability/adapters/memory.d.ts +1 -1
  79. package/dist/observability/domain/logging.d.ts +10 -4
  80. package/dist/observability/services/logging.d.ts +7 -4
  81. package/dist/observability/services/logging.js +3 -2
  82. package/dist/observability/services/outcome.d.ts +6 -6
  83. package/dist/observability/services/outcome.js +10 -13
  84. package/package.json +1 -1
@@ -35,42 +35,71 @@ export function headersOf(execution) {
35
35
  *
36
36
  * Context: the transport's own headers (`restTransport({ headers })`) are the session's — the
37
37
  * token, the tenant — the same for every call; these are the use case's, different for two use
38
- * 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.
39
40
  */
40
41
  export function bindTransport(transport, execution) {
41
42
  const own = headersOf(execution);
42
- if (Object.keys(own).length === 0)
43
+ const cancels = execution.signal;
44
+ if (Object.keys(own).length === 0 && cancels === undefined)
43
45
  return transport;
44
46
  const call = (options) => ({ ...options, headers: { ...own, ...options?.headers } });
45
47
  const bound = {
46
- list: (route, criteria, signal, options) => transport.list(route, criteria, signal, call(options)),
47
- one: (route, signal, options) => transport.one(route, signal, call(options)),
48
- 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)),
49
51
  };
50
52
  const read = transport.read;
51
53
  if (read !== undefined)
52
- 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));
53
55
  const download = transport.download;
54
56
  if (download !== undefined)
55
- 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));
56
58
  return bound;
57
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
+ }
58
84
  /**
59
85
  * A client made over a transport, made again over the transport bound to each execution it is
60
- * used in. What a generated `clientOf(transport)` answers, so a Feature that calls
61
- * `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.
62
88
  *
63
- * export function clientOf(transport: Transport) {
64
- * 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
+ * }));
65
93
  * }
66
94
  *
67
95
  * Context: it takes the function that makes the client, not the client — rebuilding through
68
- * `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.
69
98
  */
70
- export function bindable(transport, make) {
71
- const client = make(transport);
99
+ export function bindable(wire, make) {
100
+ const client = make(wire);
72
101
  Object.defineProperty(client, BIND, {
73
- value: (execution) => bindable(bindTransport(transport, execution), make),
102
+ value: (execution) => bindable(bound(wire, execution), make),
74
103
  enumerable: false,
75
104
  });
76
105
  return client;
@@ -14,7 +14,7 @@ import { scaffoldFilesOf } from "../services/scaffold.js";
14
14
  *
15
15
  * - `domain.ts`: the DTOs, only types — and the domain events with `DomainEvent`, their union,
16
16
  * when the contract was exported with them (`export-client-contract --events`);
17
- * - `client.ts`: every operation (and event) as data, and `clientOf(transport)`;
17
+ * - `client.ts`: every operation (and event) as data, and `clientOf(operations)` over any wire;
18
18
  * - `entities.ts`: an `entity<T, C>()` per model a screen can list;
19
19
  * - `contract.json`: the snapshot they were written from, for `metasOf`.
20
20
  *
@@ -26,7 +26,7 @@ import { scaffoldFilesOf } from "../services/scaffold.js";
26
26
  */
27
27
  const USAGE = [
28
28
  "usage: sincpro-client generate <contract.json> --out <dir> [--domain <name>] [--base <path>]",
29
- " sincpro-client scaffold <contract.json> --out <dir> [--transport <module>]",
29
+ " sincpro-client scaffold <contract.json> --out <dir> [--operations <module>]",
30
30
  " sincpro-client check [--root src] [--tsconfig tsconfig.json]",
31
31
  ].join("\n");
32
32
  /** JSON with every key sorted: the snapshot reads the same however the server ordered it. */
@@ -53,7 +53,7 @@ function parsed(args) {
53
53
  flags.set(flag, value);
54
54
  }
55
55
  const out = flags.get("--out");
56
- const known = new Set(command === "generate" ? ["--out", "--domain", "--base"] : ["--out", "--transport"]);
56
+ const known = new Set(command === "generate" ? ["--out", "--domain", "--base"] : ["--out", "--operations"]);
57
57
  if (out === undefined || [...flags.keys()].some((one) => !known.has(one)))
58
58
  return undefined;
59
59
  const domain = flags.get("--domain");
@@ -66,12 +66,12 @@ function parsed(args) {
66
66
  ...(domain === undefined ? {} : { domain }),
67
67
  ...(base === undefined ? {} : { base }),
68
68
  },
69
- transport: flags.get("--transport"),
69
+ operations: flags.get("--operations"),
70
70
  };
71
71
  }
72
72
  /** The skeleton, each file only when it is not there: what is there is the application's. */
73
- function scaffold(metamodel, out, transport) {
74
- const files = scaffoldFilesOf(metamodel, transport === undefined ? {} : { transport });
73
+ function scaffold(metamodel, out, operations) {
74
+ const files = scaffoldFilesOf(metamodel, operations === undefined ? {} : { operations });
75
75
  const written = [];
76
76
  const kept = [];
77
77
  for (const [name, source] of Object.entries(files)) {
@@ -128,7 +128,7 @@ function main(args) {
128
128
  }
129
129
  const metamodel = JSON.parse(readFileSync(asked.source, "utf8"));
130
130
  if (asked.command === "scaffold")
131
- return scaffold(metamodel, asked.out, asked.transport);
131
+ return scaffold(metamodel, asked.out, asked.operations);
132
132
  const files = { "domain.ts": domainFileOf(metamodel) };
133
133
  if (metamodel.bindings !== undefined) {
134
134
  files["client.ts"] = clientFileOf(metamodel, asked.options);
@@ -3,14 +3,13 @@ import { type GenerateOptions } from "./rest.js";
3
3
  /**
4
4
  * The TypeScript of the client of `metamodel`, as the text of one file next to its `domain.ts`.
5
5
  *
6
- * 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets
7
- * and the binding of each wire that publishes it.
8
- * 2. Per operation REST reaches, how a transport calls it (`callOf`): a page, a record, a write
9
- * — or none, said in the client's own doc.
10
- * 3. The routes of the writes, read once (`writeRoute`).
11
- * 4. When the contract carries them, every domain event as data, by its type's name: its wire
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
12
11
  * name, and whether the bounded context records it or handles it.
13
- * Final: `clientOf(transport)`, a method per operation reached, named by `methodOf`; two with
14
- * one name are refused, naming both.
12
+ * Final: `clientOf(transport)` over an `OperationTransport`, bound to each run that uses it; two
13
+ * methods with one name are refused, naming both.
15
14
  */
16
15
  export declare function clientFileOf(metamodel: Metamodel, options?: GenerateOptions): string;
@@ -1,59 +1,31 @@
1
1
  import { aliasOf, answerOf, claimed, deprecationOf, docOf, DOMAIN_MODULE, FRAMEWORK, headerOf, identifierOf, localOf, methodOf, recordLiteral, sorted, } from "../domain/names.js";
2
- import { ownerOf, restOf, WRITES, } from "./rest.js";
2
+ import { restOf, } from "./rest.js";
3
3
  /**
4
4
  * The client of a bounded context as TypeScript: every operation as data, and a method for each
5
- * one REST reaches the way a `Transport` sends — over `@sincpro/client-framework`, so a script,
6
- * a test or a hook of its own calls the service typed, with no screen.
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
7
  *
8
- * Context: a method only writes down the route the contract published and hands the call to the
9
- * transport, which keeps the rules — the `Idempotency-Key`, the `Problem`, the model's version.
10
- * A GET is a page (`list`), one record (`one`), a file (`export`: `downloadRoute`, its bytes as
11
- * they arrive), or — a Query of the context's own, `GET /me` — a read of its answer as it came
12
- * (`readRoute`, over a transport that has `read`). A GET's
13
- * keys in its path are required parameters; every other field is an optional one.
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
14
  */
15
- /** A search's fields a page asks with, not the screen: the criteria, and its cursor inside it. */
16
- const PAGED = new Set(["criteria", "cursor"]);
17
- /** An export's fields its method takes apart: the criteria and the format. */
18
- const EXPORTED = new Set(["criteria", "cursor", "format"]);
19
- /** A GET's parameters: the path's keys, and the rest of its input but what a page asks with. */
20
- function askedOf(operation, rest, paged, apart = PAGED) {
21
- const path = [...rest.fields].sort();
22
- const query = Object.keys(operation.input.properties ?? {})
23
- .filter((one) => !path.includes(one) && !(paged && apart.has(one)))
24
- .sort();
25
- return { path, query };
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);
26
24
  }
27
- function callOf(metamodel, operation, rest) {
28
- if (rest === undefined)
29
- return { kind: "none", why: "REST does not publish it" };
30
- if (WRITES.has(rest.method))
31
- return { kind: "send" };
32
- const owner = ownerOf(metamodel, operation);
33
- if (operation.role === "search" && owner !== undefined && owner in metamodel.models)
34
- return { kind: "list", entity: owner, ...askedOf(operation, rest, true) };
35
- // An export answers bytes, never JSON: it is downloaded.
36
- if (operation.role === "export" && owner !== undefined && owner in metamodel.models)
37
- return { kind: "download", entity: owner, ...askedOf(operation, rest, true, EXPORTED) };
38
- const field = owner === undefined ? undefined : operation.targets[owner];
39
- const record = rest.fields.length === 1 && rest.fields[0] === field;
40
- if (operation.role === "get" && owner !== undefined && field !== undefined && record)
41
- return { kind: "one", entity: owner, field };
42
- // Any other GET — a Query of the context's own, `GET /me` — is read as it answers.
43
- return { kind: "read", ...askedOf(operation, rest, false) };
44
- }
45
- /** A parameter list's TypeScript: the path's keys required, the rest optional, all as text. */
46
- function paramsOf(asked) {
47
- const union = (names) => names.map((one) => JSON.stringify(one)).join(" | ");
48
- if (asked.path.length === 0 && asked.query.length === 0)
49
- return { signature: "", route: (path) => JSON.stringify(path) };
50
- const required = asked.path.length === 0 ? "" : `Readonly<Record<${union(asked.path)}, string>>`;
51
- const optional = asked.query.length === 0 ? "" : `Partial<Record<${union(asked.query)}, string>>`;
52
- const type = [required, optional].filter((one) => one !== "").join(" & ");
53
- return {
54
- signature: `params${asked.path.length === 0 ? "?" : ""}: ${type}`,
55
- route: (path) => `listRoute(${JSON.stringify(path)}, params)`,
56
- };
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);
57
29
  }
58
30
  /** The data of one operation: what it is, and how each wire reaches it. */
59
31
  function dataOf(metamodel, identity, operation, rest) {
@@ -64,6 +36,8 @@ function dataOf(metamodel, identity, operation, rest) {
64
36
  ` entities: ${JSON.stringify(operation.entities)},`,
65
37
  ` targets: ${recordLiteral(operation.targets)},`,
66
38
  ];
39
+ if (isStreamed(operation))
40
+ lines.push(" streamed: true,");
67
41
  if (rest !== undefined)
68
42
  lines.push(` rest: { method: ${JSON.stringify(rest.method)}, path: ${JSON.stringify(rest.path)} },`);
69
43
  const rpc = metamodel.bindings?.rpc?.[identity];
@@ -77,121 +51,50 @@ function dataOf(metamodel, identity, operation, rest) {
77
51
  lines.push(` mcp: { tool: ${JSON.stringify(mcp.tool)} },`);
78
52
  return ` ${localOf(metamodel, identity)}: {\n${lines.join("\n")}\n },`;
79
53
  }
80
- function methodSourceOf(local, answer, method, call, rest, doc) {
81
- if (call.kind === "list") {
82
- const entity = `dto.${identifierOf(call.entity)}`;
83
- const params = paramsOf(call);
84
- const signature = params.signature === "" ? "" : `, ${params.signature}`;
85
- return `${doc} ${method}: (criteria: Criteria<${entity}>${signature}) =>\n transport.list<${entity}>(${params.route(rest.path)}, criteria),`;
86
- }
87
- if (call.kind === "download") {
88
- const entity = `dto.${identifierOf(call.entity)}`;
89
- const params = paramsOf(call);
90
- // The path's keys are required, so they come first: a required parameter never follows an
91
- // optional one.
92
- const keys = params.signature === "" ? "" : `${params.signature.replace("params?:", "params:")}, `;
93
- const given = params.signature === "" ? "" : ", params";
94
- return `${doc} ${method}: (${keys}criteria?: Criteria<${entity}>, format?: ExportFormat) =>\n downloadRoute(transport, exportRoute(${JSON.stringify(rest.path)}, criteria, format${given})),`;
95
- }
96
- if (call.kind === "read") {
97
- const params = paramsOf(call);
98
- return `${doc} ${method}: (${params.signature}) =>\n readRoute<dto.${answer}>(transport, ${params.route(rest.path)}),`;
99
- }
100
- if (call.kind === "one") {
101
- const path = rest.path.replace(`{${call.field}}`, "${encodeURIComponent(id)}");
102
- return `${doc} ${method}: (id: string) =>\n transport.one<dto.${identifierOf(call.entity)}>(\`${path}\`),`;
103
- }
104
- const id = rest.fields.includes("id")
105
- ? "body.id === undefined ? undefined : String(body.id)"
106
- : "undefined";
107
- return [
108
- `${doc} ${method}: (body: dto.${local}, options?: SendOptions) =>`,
109
- ` transport.send<dto.${answer}>(`,
110
- ` ${JSON.stringify(rest.method)},`,
111
- ` pathFor(routes.${method}, ${id}, body),`,
112
- ` body,`,
113
- ` undefined,`,
114
- ` options,`,
115
- ` ),`,
116
- ].join("\n");
117
- }
118
54
  /**
119
55
  * The TypeScript of the client of `metamodel`, as the text of one file next to its `domain.ts`.
120
56
  *
121
- * 1. Every operation, by its local name, as data: identity, role, `query`, entities, targets
122
- * and the binding of each wire that publishes it.
123
- * 2. Per operation REST reaches, how a transport calls it (`callOf`): a page, a record, a write
124
- * — or none, said in the client's own doc.
125
- * 3. The routes of the writes, read once (`writeRoute`).
126
- * 4. When the contract carries them, every domain event as data, by its type's name: its wire
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
127
62
  * name, and whether the bounded context records it or handles it.
128
- * Final: `clientOf(transport)`, a method per operation reached, named by `methodOf`; two with
129
- * one name are refused, naming both.
63
+ * Final: `clientOf(transport)` over an `OperationTransport`, bound to each run that uses it; two
64
+ * methods with one name are refused, naming both.
130
65
  */
131
66
  export function clientFileOf(metamodel, options = {}) {
132
67
  const claim = claimed();
133
- const operations = sorted(metamodel.operations);
134
68
  const data = [];
135
- const routes = [];
136
69
  const methods = [];
137
- const unreached = [];
138
- let lists = false;
139
- let paged = false;
140
- let reads = false;
141
- let sends = false;
142
- let downloads = false;
143
- for (const [identity, operation] of operations) {
70
+ for (const [identity, operation] of sorted(metamodel.operations)) {
144
71
  const local = localOf(metamodel, identity);
145
- const rest = restOf(metamodel, identity, options);
146
- data.push(dataOf(metamodel, identity, operation, rest));
147
- const call = callOf(metamodel, operation, rest);
148
- if (call.kind === "none" || rest === undefined) {
149
- unreached.push(`\`${local}\`: ${call.kind === "none" ? call.why : ""}.`);
150
- continue;
151
- }
72
+ data.push(dataOf(metamodel, identity, operation, restOf(metamodel, identity, options)));
152
73
  const method = methodOf(local);
153
74
  claim(method, `operation ${identity}`);
154
- const doc = docOf([operation.description, deprecationOf(metamodel, operation)], " ");
155
- methods.push(methodSourceOf(local, answerOf(metamodel, identity), method, call, rest, doc));
156
- if (call.kind === "download") {
157
- downloads = true;
158
- paged = true;
159
- }
160
- if (call.kind === "list" || call.kind === "read") {
161
- paged ||= call.kind === "list";
162
- reads ||= call.kind === "read";
163
- lists ||= call.path.length + call.query.length > 0;
164
- }
165
- if (call.kind === "send") {
166
- sends = true;
167
- routes.push(` ${method}: writeRoute(${JSON.stringify(`${rest.method} ${rest.path}`)}),`);
168
- }
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"));
169
86
  }
170
- const core = [
171
- "bindable",
172
- ...(downloads ? ["downloadRoute", "type ExportFormat", "exportRoute"] : []),
173
- ...(lists ? ["listRoute"] : []),
174
- ...(sends ? ["pathFor"] : []),
175
- ...(reads ? ["readRoute"] : []),
176
- ...(sends ? ["type SendOptions"] : []),
177
- "type Transport",
178
- ...(sends ? ["writeRoute"] : []),
179
- ];
87
+ const events = sorted(metamodel.events ?? {}).map(([name, event]) => ` ${identifierOf(event.title)}: { name: ${JSON.stringify(name)}, recorded: ${event.recorded}, handled: ${event.handled} },`);
180
88
  const imports = [
181
- ...(paged ? ['import type { Criteria } from "@sincpro/criteria";'] : []),
182
- `import { ${core.join(", ")} } from "${FRAMEWORK}";`,
89
+ `import { bindable, type OperationOptions, type OperationTransport } from "${FRAMEWORK}";`,
183
90
  "",
184
91
  `import type * as dto from "${DOMAIN_MODULE}";`,
185
92
  ].join("\n");
186
- const events = sorted(metamodel.events ?? {}).map(([name, event]) => ` ${identifierOf(event.title)}: { name: ${JSON.stringify(name)}, recorded: ${event.recorded}, handled: ${event.handled} },`);
187
93
  const client = [
188
94
  "/**",
189
- ` * «${aliasOf(metamodel)}» over a transport: a method for each operation REST reaches as a`,
190
- " * page, a record, a read of its own or a write. Used by a Feature, each request carries the",
191
- " * trace and the context of the use case that sent it (`bindable`).",
192
- ...(unreached.length === 0
193
- ? []
194
- : [" *", " * Without a method here:", ...unreached.map((one) => ` * - ${one}`)]),
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.",
195
98
  " */",
196
99
  ].join("\n");
197
100
  return `${[
@@ -203,7 +106,6 @@ export function clientFileOf(metamodel, options = {}) {
203
106
  : [
204
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;`,
205
108
  ]),
206
- ...(routes.length === 0 ? [] : [`const routes = {\n${routes.join("\n")}\n};`]),
207
- `${client}\nexport function clientOf(transport: Transport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
109
+ `${client}\nexport function clientOf(transport: OperationTransport) {\n return bindable(transport, (transport) => ({\n${methods.join("\n")}\n }));\n}`,
208
110
  ].join("\n\n")}\n`;
209
111
  }
@@ -8,7 +8,7 @@ import type { Metamodel } from "../domain/metamodel.js";
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
10
  * services/index.ts registers command classes and their services
11
- * index.ts `export const project` — ready to use
11
+ * index.ts `export const projectBoundedContext` — ready to use
12
12
  *
13
13
  * Context: the layout sincpro_framework gives a service (`dependencies.py`, `framework.py`,
14
14
  * `services/`, `__init__.py`), so a bounded context starts the way every other does and nobody
@@ -17,8 +17,11 @@ import type { Metamodel } from "../domain/metamodel.js";
17
17
  * whole every time.
18
18
  */
19
19
  export type ScaffoldOptions = {
20
- /** Where the application's transport is exported (`export const transport`), from the
21
- * bounded context's folder. `../common/infrastructure/transport` by default. */
22
- transport?: string;
20
+ /**
21
+ * Where the application exports how it reaches its services (`export const operations`, an
22
+ * `OperationTransport`: REST, JSON-RPC, a platform's), from the bounded context's folder.
23
+ * `../common/infrastructure/operations` by default.
24
+ */
25
+ operations?: string;
23
26
  };
24
27
  export declare function scaffoldFilesOf(metamodel: Metamodel, options?: ScaffoldOptions): Record<string, string>;
@@ -1,10 +1,4 @@
1
1
  import { aliasOf, FRAMEWORK } from "../domain/names.js";
2
- /** Names the skeleton's `index.ts` cannot give its constant. */
3
- const TAKEN = new Set([
4
- "transport",
5
- "registerServices",
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(" "),
7
- ]);
8
2
  /** `sales-orders` → `SalesOrders`. */
9
3
  function pascalOf(alias) {
10
4
  const joined = alias
@@ -20,17 +14,16 @@ const HEADER = (what) => `// ${what} — written once by \`sincpro-client scaffo
20
14
  export function scaffoldFilesOf(metamodel, options = {}) {
21
15
  const alias = aliasOf(metamodel);
22
16
  const Name = pascalOf(alias);
23
- const bare = `${Name.charAt(0).toLowerCase()}${Name.slice(1)}`;
24
- // A word the language keeps (`import`, `default`) or a name `index.ts` already imports cannot
25
- // be the constant's: it takes the bounded context's word instead.
26
- const name = TAKEN.has(bare) ? `${bare}BoundedContext` : bare;
17
+ // Named for what it is — `projectBoundedContext`, never `project` (an entity?) or `ctx` —:
18
+ // nobody misreads it, and no word of the language or name `index.ts` imports can take it.
19
+ const name = `${Name.charAt(0).toLowerCase()}${Name.slice(1)}BoundedContext`;
27
20
  const Dependencies = `${Name}Dependencies`;
28
21
  const client = metamodel.bindings !== undefined;
29
- const transport = options.transport ?? at("..", "common", "infrastructure", "transport");
22
+ const operations = options.operations ?? at("..", "common", "infrastructure", "operations");
30
23
  const dependencies = [
31
24
  HEADER(`What the services of «${alias}» can count on`),
32
25
  "",
33
- `import type { Transport } from "${FRAMEWORK}";`,
26
+ `import type { OperationTransport } from "${FRAMEWORK}";`,
34
27
  `import type { BoundedContext } from "${at(FRAMEWORK, "core")}";`,
35
28
  ...(client ? ["", `import { clientOf } from "${at("..", "generated", "client")}";`] : []),
36
29
  "",
@@ -44,11 +37,11 @@ export function scaffoldFilesOf(metamodel, options = {}) {
44
37
  "}",
45
38
  "",
46
39
  "export function registerDependencies(",
47
- ` bc: BoundedContext<${Dependencies}>,`,
48
- ` ${client ? "transport" : "_transport"}: Transport,`,
40
+ ` boundedContext: BoundedContext<${Dependencies}>,`,
41
+ ` ${client ? "operations" : "_operations"}: OperationTransport,`,
49
42
  ") {",
50
- ...(client ? [` bc.addDependency("client", clientOf(transport));`] : []),
51
- " return bc;",
43
+ ...(client ? [` boundedContext.addDependency("client", clientOf(operations));`] : []),
44
+ " return boundedContext;",
52
45
  "}",
53
46
  "",
54
47
  ].join("\n");
@@ -60,23 +53,24 @@ export function scaffoldFilesOf(metamodel, options = {}) {
60
53
  " Feature as BaseFeature,",
61
54
  " BoundedContext,",
62
55
  " type Command,",
56
+ " type DomainEvent,",
63
57
  `} from "${at(FRAMEWORK, "core")}";`,
64
- `import type { Transport } from "${FRAMEWORK}";`,
58
+ `import type { OperationTransport } from "${FRAMEWORK}";`,
65
59
  "",
66
60
  `import { type ${Dependencies}, registerDependencies } from "${at(".", "dependencies")}";`,
67
61
  "",
68
- `/** A Feature of «${alias}»: its dependencies typed on \`this\`, and \`this.context\`. */`,
69
- "export abstract class Feature<C extends Command<R>, R> extends BaseFeature<C, R> {",
62
+ `/** A Feature of «${alias}» — of a Command, or a use case of an event: its dependencies typed on \`this\`, and \`this.context\`. */`,
63
+ "export abstract class Feature<C extends Command<R> | DomainEvent, R> extends BaseFeature<C, R> {",
70
64
  ...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
71
65
  "}",
72
66
  "",
73
67
  `/** 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> {",
68
+ "export abstract class ApplicationService<C extends Command<R> | DomainEvent, R> extends BaseApplicationService<C, R> {",
75
69
  ...(client ? [` declare readonly client: ${Dependencies}["client"];`] : []),
76
70
  "}",
77
71
  "",
78
- `export function config${Name}BoundedContext(name: string, transport: Transport) {`,
79
- ` return registerDependencies(new BoundedContext<${Dependencies}>(name), transport);`,
72
+ `export function config${Name}BoundedContext(name: string, operations: OperationTransport) {`,
73
+ ` return registerDependencies(new BoundedContext<${Dependencies}>(name), operations);`,
80
74
  "}",
81
75
  "",
82
76
  ].join("\n");
@@ -90,22 +84,24 @@ export function scaffoldFilesOf(metamodel, options = {}) {
90
84
  " * Generated DTO types are remote contracts, not runtime command constructors: import",
91
85
  ` * them under their own name — \`import type * as remote from "${at("..", "generated", "domain")}"\` —`,
92
86
  " * and let a local Command implement one (`implements remote.CommandCancelIssue`).",
93
- " * Register with context.feature(CommandClass)(FeatureClass) or",
94
- " * context.appService(CommandClass)(ApplicationServiceClass).",
87
+ " * Register with boundedContext.feature(CommandClass)(FeatureClass) or",
88
+ " * boundedContext.appService(CommandClass)(ApplicationServiceClass); the services live in",
89
+ " * one folder per aggregate (`services/<aggregate>/`), and an entity of that aggregate with",
90
+ " * use cases of its own in a folder inside it (`services/<aggregate>/<entity>/`).",
95
91
  " */",
96
- `export function registerServices(_context: BoundedContext<${Dependencies}>): void {}`,
92
+ `export function registerServices(_boundedContext: BoundedContext<${Dependencies}>): void {}`,
97
93
  "",
98
94
  ].join("\n");
99
95
  const index = [
100
96
  HEADER(`«${alias}», ready to use`),
101
97
  "",
102
- `import { transport } from "${transport}";`,
98
+ `import { operations } from "${operations}";`,
103
99
  "",
104
100
  `import { config${Name}BoundedContext } from "${at(".", "infrastructure", "bounded_context")}";`,
105
101
  `import { registerServices } from "${at(".", "services")}";`,
106
102
  "",
107
103
  `/** «${alias}»: \`${name}.execute(new Command(...))\`. */`,
108
- `export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, transport);`,
104
+ `export const ${name} = config${Name}BoundedContext(${JSON.stringify(alias)}, operations);`,
109
105
  `registerServices(${name});`,
110
106
  "",
111
107
  ].join("\n");