@sincpro/client-framework 0.2.0 → 0.4.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 (39) hide show
  1. package/AGENTS.md +51 -23
  2. package/MANIFESTO.md +44 -26
  3. package/README.md +304 -29
  4. package/dist/adapters/opentelemetry.d.ts +1 -1
  5. package/dist/adapters/opentelemetry.js +1 -1
  6. package/dist/adapters/sentry.d.ts +2 -2
  7. package/dist/adapters/sentry.js +2 -2
  8. package/dist/client/memory.js +14 -6
  9. package/dist/common/decimal.d.ts +49 -0
  10. package/dist/common/decimal.js +146 -0
  11. package/dist/{client → common}/ids.d.ts +7 -0
  12. package/dist/{client → common}/ids.js +12 -0
  13. package/dist/common/money.d.ts +74 -0
  14. package/dist/common/money.js +168 -0
  15. package/dist/common/serialization.d.ts +49 -0
  16. package/dist/common/serialization.js +155 -0
  17. package/dist/common/time.d.ts +40 -0
  18. package/dist/common/time.js +93 -0
  19. package/dist/context/codec.d.ts +24 -0
  20. package/dist/context/codec.js +28 -0
  21. package/dist/context/context.d.ts +45 -0
  22. package/dist/context/context.js +70 -0
  23. package/dist/context/store.d.ts +51 -0
  24. package/dist/context/store.js +59 -0
  25. package/dist/index.d.ts +17 -5
  26. package/dist/index.js +17 -5
  27. package/dist/observability/ports.d.ts +2 -2
  28. package/dist/services/bounded-context.d.ts +79 -0
  29. package/dist/services/bounded-context.js +272 -0
  30. package/dist/services/execution.d.ts +8 -8
  31. package/dist/services/execution.js +8 -6
  32. package/dist/services/feature.d.ts +70 -48
  33. package/dist/services/feature.js +57 -191
  34. package/dist/services/process.d.ts +60 -0
  35. package/dist/services/process.js +57 -0
  36. package/dist/testing/index.d.ts +3 -3
  37. package/package.json +2 -2
  38. package/dist/services/runtime.d.ts +0 -48
  39. package/dist/services/runtime.js +0 -30
package/dist/index.js CHANGED
@@ -6,10 +6,14 @@
6
6
  * (`sideEffects: false`), so an application pays only for what it uses, and a use case, the
7
7
  * client it calls and the entity it reads come from the same place.
8
8
  *
9
+ * common/ the values of the wire, as the backend writes them: ids, a day and an instant,
10
+ * `Decimal`, `Money`, and how any value is written and read (`valuesOf`, `rebuilt`)
9
11
  * client/ the seam with a server: the transport and its REST and in-memory implementations,
10
- * entities with typed commands, the service's client contract, client-side ids
12
+ * entities with typed commands, the service's client contract
11
13
  * model/ model definitions: written by hand, read from a JSON Schema, kept once known
12
- * services/ Feature and ApplicationService, the runtime they run in, what each run carries
14
+ * services/ the bounded context (`UseBoundedContext`), its `Feature` and `ApplicationService`,
15
+ * the process they run in (`configureProcess`), what each run carries
16
+ * context/ the context: one door (`Context`) over the store each platform plugs
13
17
  * observability/ the ports a platform plugs: a tracer and an error reporter
14
18
  *
15
19
  * The generator is `@sincpro/client-framework/codegen`, and the in-memory tracer and reporter
@@ -17,15 +21,23 @@
17
21
  */
18
22
  export { entityKeyOf, kindOf, metasOf, versionsOf, } from "./client/contract.js";
19
23
  export { bodyFor, entity, listRoute, NEW_PARAM, pathFor, PICK_PARAM, QUERY_PARAM, SCOPE_PARAM, scopeOfRef, startingValuesOf, TRAIL_PARAM, WITH_PARAM, writeRoute, } from "./client/entity.js";
20
- export { isEntityId, newEntityId } from "./client/ids.js";
21
24
  export { memoryTransport, } from "./client/memory.js";
22
25
  export { restTransport, urlOf, } from "./client/rest.js";
23
26
  export { retryKeys, unanswered, } from "./client/retry.js";
24
27
  export { readRoute, TransportError, } from "./client/transport.js";
28
+ export { Decimal } from "./common/decimal.js";
29
+ export { isEntityId, momentOf, newEntityId } from "./common/ids.js";
30
+ export { Money } from "./common/money.js";
31
+ export { BYTES, pack, readValues, rebuilt, unpack, valuesOf, } from "./common/serialization.js";
32
+ export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./common/time.js";
33
+ export { plainCodec, travelling, travels, } from "./context/codec.js";
34
+ export {} from "./context/context.js";
35
+ export { memoryContexts, } from "./context/store.js";
25
36
  export { defineMeta, field, } from "./model/meta.js";
26
37
  export { metaStore } from "./model/metas.js";
27
38
  export { completeMeta, metaFromSchema, } from "./model/schema.js";
28
39
  export { NO_TRACER, } from "./observability/ports.js";
40
+ export { UseBoundedContext, } from "./services/bounded-context.js";
29
41
  export { BIND, bindable, bindDependency, bindTransport, headersOf, } from "./services/execution.js";
30
- export { ApplicationService, bindServices, Feature, } from "./services/feature.js";
31
- export { createRuntime, DETACHED, outcomeOf, } from "./services/runtime.js";
42
+ export { ApplicationService, Feature, } from "./services/feature.js";
43
+ export { configureProcess, outcomeOf, processContext, } from "./services/process.js";
@@ -47,13 +47,13 @@ export interface Tracer {
47
47
  }
48
48
  /** What a report says about where the error happened. */
49
49
  export type ErrorAbout = {
50
- /** `<domain>/<Feature>`, the span's name. */
50
+ /** `<bounded context>.<Service>[.<method>]`: the use case's identity, as the backend's. */
51
51
  useCase: string;
52
52
  /** The trace the error belongs to — the same id the server logged, when it got that far. */
53
53
  traceId: string;
54
54
  /** The execution's own id. */
55
55
  executionId: string;
56
- /** What the use case ran with (`Runtime` `context`). */
56
+ /** What the use case ran with: the process's context, its bounded context's, its scope's. */
57
57
  context: Readonly<Record<string, unknown>>;
58
58
  };
59
59
  /** Reports what nobody expected. One per platform; none when nothing reports. */
@@ -0,0 +1,79 @@
1
+ import { type Context } from "../context/context.js";
2
+ import type { ContextValues } from "../context/store.js";
3
+ import { type ServiceClasses, type Services } from "./feature.js";
4
+ /**
5
+ * A bounded context: its dependencies, its services, and the one object everything else uses
6
+ * them through — set up the way sincpro_framework sets one up.
7
+ *
8
+ * // domains/project/infrastructure/dependencies.ts ← what the services can count on
9
+ * export interface ProjectDependencies {
10
+ * client: ProjectClient;
11
+ * session: Session;
12
+ * }
13
+ * export function registerDependencies(bc: UseBoundedContext<ProjectDependencies>) {
14
+ * bc.addDependency("client", clientOf(transport));
15
+ * bc.addDependency("session", session);
16
+ * return bc;
17
+ * }
18
+ *
19
+ * // domains/project/infrastructure/bounded_context.ts
20
+ * export class Feature extends BaseFeature<ProjectDependencies> {}
21
+ * export class ApplicationService extends BaseApplicationService<ProjectDependencies, ProjectFeatures> {}
22
+ * export function configProjectBoundedContext(name: string) {
23
+ * return registerDependencies(new UseBoundedContext<ProjectDependencies>(name));
24
+ * }
25
+ *
26
+ * // domains/project/index.ts
27
+ * export const project = configProjectBoundedContext("project").register({
28
+ * features, // { Issues, Notices }
29
+ * applicationServices, // { CloseAndNotify }
30
+ * });
31
+ *
32
+ * // anywhere — a screen, another bounded context's adapter, a test
33
+ * await project.Issues.close(input); // span "project/Issues.close"
34
+ * await project.CloseAndNotify.execute(input); // span "project/CloseAndNotify", its Features under it
35
+ *
36
+ * Context: sincpro_framework's `UseFramework` (being renamed to bounded context) with
37
+ * `add_dependency`, and `project(Command(...))` written as a property: a service is reached by
38
+ * the name it is registered under, so its span is named by that name — a production build renames
39
+ * classes, never the keys of an object — and the type of `project` is the services it holds.
40
+ *
41
+ * `project` is light and never frozen. It holds references: the classes, the dependencies, and
42
+ * one instance of each service, made the first time it is asked for and shared by everything
43
+ * derived from it. Each call makes a small view of that instance — a few pointers — that carries
44
+ * the call's trace. A screen or a test that needs its own variant derives one, with everything
45
+ * else shared:
46
+ *
47
+ * project.with({ client: fake }) other dependencies — a test, a demo
48
+ * project.scoped({ company_id: 2 }) more context for what runs through it — a window
49
+ *
50
+ * A service is a singleton of its bounded context: what a method writes on `this` stays for the
51
+ * next call and for the call running beside it, as in sincpro_framework. What belongs to one
52
+ * request goes in its arguments or its context, never on `this`.
53
+ */
54
+ export type BoundedContext<D extends object, C extends object, F extends ServiceClasses, A extends ServiceClasses> = Services<F, D> & Services<A, D> & {
55
+ readonly name: string;
56
+ /** This bounded context's level of the context: under the process's, over a scope's. */
57
+ readonly context: Context<C>;
58
+ /** The same bounded context with other dependencies — the rest, and the services, shared. */
59
+ with(dependencies: Partial<D>): BoundedContext<D, C, F, A>;
60
+ /** The same bounded context with more context for what runs through it. */
61
+ scoped(context: Partial<C>): BoundedContext<D, C, F, A>;
62
+ };
63
+ export declare class UseBoundedContext<D extends object, C extends object = ContextValues> {
64
+ #private;
65
+ readonly name: string;
66
+ /** This bounded context's level of the context, saved in the process's store. */
67
+ readonly context: Context<C>;
68
+ constructor(name: string);
69
+ /** `value` is what every service of this bounded context finds at `this.<name>`. */
70
+ addDependency<K extends keyof D & string>(name: K, value: D[K]): this;
71
+ /**
72
+ * The object the bounded context is used through: each service under its name. A dependency
73
+ * added after this is seen too — the services read them on every call.
74
+ */
75
+ register<F extends ServiceClasses, A extends ServiceClasses = Record<never, never>>(services: {
76
+ features: F;
77
+ applicationServices?: A;
78
+ }): BoundedContext<D, C, F, A>;
79
+ }
@@ -0,0 +1,272 @@
1
+ import { newEntityId } from "../common/ids.js";
2
+ import { contextAt } from "../context/context.js";
3
+ import { bindDependency } from "./execution.js";
4
+ import { composes, isApplicationServiceClass, isFeatureClass, methodsOf, } from "./feature.js";
5
+ import { processContext, processNow, processStore, } from "./process.js";
6
+ /** Names `project` answers itself: no service is registered under them. */
7
+ const RESERVED = new Set(["name", "context", "with", "scoped"]);
8
+ /** Names a run answers on `this` itself: no dependency is added under them. */
9
+ const ANSWERED = new Set(["context", "feature"]);
10
+ export class UseBoundedContext {
11
+ name;
12
+ /** This bounded context's level of the context, saved in the process's store. */
13
+ context;
14
+ #dependencies = {};
15
+ constructor(name) {
16
+ if (name === "" || name.includes("/"))
17
+ throw new Error(`"${name}" is not a bounded context's name — it names its spans: "${name}/Issues.close"`);
18
+ this.name = name;
19
+ this.context = contextAt(`bounded_context:${name}`, processStore);
20
+ }
21
+ /** `value` is what every service of this bounded context finds at `this.<name>`. */
22
+ addDependency(name, value) {
23
+ if (ANSWERED.has(name))
24
+ throw new Error(`${this.name}: a dependency cannot be called "${name}" — every service answers this.${name} itself`);
25
+ this.#dependencies[name] = value;
26
+ return this;
27
+ }
28
+ /**
29
+ * The object the bounded context is used through: each service under its name. A dependency
30
+ * added after this is seen too — the services read them on every call.
31
+ */
32
+ register(services) {
33
+ const classes = new Map();
34
+ const add = (key, service) => {
35
+ if (RESERVED.has(key))
36
+ throw new Error(`${this.name}: no service can be called "${key}" — it is the bounded context's own`);
37
+ if (classes.has(key))
38
+ throw new Error(`${this.name}: two services are called "${key}"`);
39
+ classes.set(key, service);
40
+ };
41
+ for (const [key, service] of Object.entries(services.features)) {
42
+ if (typeof service !== "function" || !isFeatureClass(service))
43
+ throw new Error(`${this.name}: "${key}" is not a Feature — an ApplicationService goes in applicationServices`);
44
+ add(key, service);
45
+ }
46
+ for (const [key, service] of Object.entries(services.applicationServices ?? {})) {
47
+ if (typeof service !== "function" || !isApplicationServiceClass(service))
48
+ throw new Error(`${this.name}: "${key}" is not an ApplicationService — a Feature goes in features`);
49
+ add(key, service);
50
+ }
51
+ const registry = {
52
+ name: this.name,
53
+ dependencies: this.#dependencies,
54
+ classes,
55
+ features: new Set(Object.keys(services.features)),
56
+ instances: new Map(),
57
+ context: this.context,
58
+ };
59
+ return assemble({ registry, overrides: {}, scope: {} });
60
+ }
61
+ }
62
+ function assemble(binding) {
63
+ const { registry } = binding;
64
+ const held = {};
65
+ Object.defineProperties(held, {
66
+ name: { value: registry.name, enumerable: true },
67
+ context: { value: registry.context, enumerable: true },
68
+ with: {
69
+ value: (dependencies) => assemble({ ...binding, overrides: { ...binding.overrides, ...dependencies } }),
70
+ },
71
+ scoped: {
72
+ value: (values) => assemble({ ...binding, scope: { ...binding.scope, ...values } }),
73
+ },
74
+ });
75
+ // Each service is made and wrapped the first time it is asked for, then kept on this object.
76
+ for (const key of registry.classes.keys())
77
+ Object.defineProperty(held, key, {
78
+ configurable: true,
79
+ enumerable: true,
80
+ get() {
81
+ const service = intercepted(key, binding, undefined);
82
+ Object.defineProperty(held, key, { value: service, enumerable: true });
83
+ return service;
84
+ },
85
+ });
86
+ return held;
87
+ }
88
+ function instanceOf(registry, key) {
89
+ const known = registry.instances.get(key);
90
+ if (known !== undefined)
91
+ return known;
92
+ const Service = registry.classes.get(key);
93
+ if (Service === undefined)
94
+ throw new Error(`${registry.name}: no service is called "${key}"`);
95
+ const made = new Service();
96
+ registry.instances.set(key, made);
97
+ return made;
98
+ }
99
+ /**
100
+ * A service as its holder sees it: each public method runs in an execution of its own — a child
101
+ * of `parent` when an ApplicationService runs it. A method is the same function every time it is
102
+ * read, so a screen can keep it (`useService(project.Issues.close)`).
103
+ */
104
+ function intercepted(key, binding, parent) {
105
+ const target = instanceOf(binding.registry, key);
106
+ const traced = methodsOf(target);
107
+ const methods = new Map();
108
+ return new Proxy(target, {
109
+ get(held, property) {
110
+ const value = Reflect.get(held, property, held);
111
+ if (typeof property !== "string" ||
112
+ typeof value !== "function" ||
113
+ !traced.has(property))
114
+ return value;
115
+ let method = methods.get(property);
116
+ if (method === undefined) {
117
+ const body = value;
118
+ method = (...args) => run(held, key, property, body, args, binding, parent);
119
+ methods.set(property, method);
120
+ }
121
+ return method;
122
+ },
123
+ });
124
+ }
125
+ function dependencyOf(binding, name) {
126
+ if (Object.hasOwn(binding.overrides, name))
127
+ return { found: true, value: binding.overrides[name] };
128
+ if (Object.hasOwn(binding.registry.dependencies, name))
129
+ return { found: true, value: binding.registry.dependencies[name] };
130
+ return { found: false, value: undefined };
131
+ }
132
+ /**
133
+ * The run's own view of the instance: its context, its dependencies bound to this execution
134
+ * (each bound the first time it is read), and — for an ApplicationService — its Features as
135
+ * children of this run. Anything else is the instance's.
136
+ */
137
+ function viewOf(target, binding, execution) {
138
+ const bound = new Map();
139
+ let features;
140
+ const isComposer = composes(target);
141
+ return new Proxy(target, {
142
+ get(held, property, receiver) {
143
+ if (property === "context")
144
+ return execution.context;
145
+ if (property === "feature" && isComposer)
146
+ return (features ??= featuresOf(binding, execution));
147
+ if (typeof property === "string") {
148
+ if (bound.has(property))
149
+ return bound.get(property);
150
+ const dependency = dependencyOf(binding, property);
151
+ if (dependency.found) {
152
+ const value = bindDependency(dependency.value, execution);
153
+ bound.set(property, value);
154
+ return value;
155
+ }
156
+ }
157
+ return Reflect.get(held, property, receiver);
158
+ },
159
+ set(held, property, value) {
160
+ if (ANSWERED.has(String(property)))
161
+ return false;
162
+ if (typeof property === "string" && dependencyOf(binding, property).found)
163
+ return false;
164
+ // What a method writes on `this` lands on the instance — it is the bounded context's.
165
+ return Reflect.set(held, property, value);
166
+ },
167
+ });
168
+ }
169
+ /** An ApplicationService's `this.feature`: each Feature, asked by name, a child of the run. */
170
+ function featuresOf(binding, execution) {
171
+ const children = new Map();
172
+ return new Proxy({}, {
173
+ get(_, property) {
174
+ if (typeof property !== "string" || !binding.registry.features.has(property))
175
+ return undefined;
176
+ let child = children.get(property);
177
+ if (child === undefined) {
178
+ child = intercepted(property, binding, execution);
179
+ children.set(property, child);
180
+ }
181
+ return child;
182
+ },
183
+ });
184
+ }
185
+ const NOTHING = {};
186
+ /** What a gesture runs with: the process's context, under it the bounded context's, its scope's. */
187
+ function contextOf(binding) {
188
+ return {
189
+ ...processContext.values,
190
+ ...binding.registry.context.values,
191
+ ...binding.scope,
192
+ };
193
+ }
194
+ /**
195
+ * Whether this is the first run to report this error. A Feature that fails inside an
196
+ * ApplicationService throws through it: the error is reported once, by the run where it
197
+ * happened — its span is the one that says where — and every run around it still ends `failed`.
198
+ */
199
+ const REPORTED = new WeakSet();
200
+ function firstReport(error) {
201
+ if (error === null || (typeof error !== "object" && typeof error !== "function"))
202
+ return true;
203
+ if (REPORTED.has(error))
204
+ return false;
205
+ REPORTED.add(error);
206
+ return true;
207
+ }
208
+ /** What watches a use case never changes what it answers: a tracer or reporter that throws is ignored. */
209
+ function quietly(work, otherwise) {
210
+ try {
211
+ return work();
212
+ }
213
+ catch {
214
+ return otherwise;
215
+ }
216
+ }
217
+ const NO_SPAN = {
218
+ traceId: "",
219
+ spanId: "",
220
+ sampled: false,
221
+ set: () => undefined,
222
+ end: () => undefined,
223
+ };
224
+ function run(target, key, method, body, args, binding, parent) {
225
+ const settings = processNow();
226
+ const boundedContext = binding.registry.name;
227
+ // As sincpro_framework names a use case: the span `context/Name`, the identity `context.Name`.
228
+ const identity = method === "execute" ? key : `${key}.${method}`;
229
+ const useCase = `${boundedContext}.${identity}`;
230
+ const context = parent?.context ?? quietly(() => contextOf(binding), NOTHING);
231
+ const id = newEntityId();
232
+ const attributes = {
233
+ "sincpro.use_case": useCase,
234
+ "sincpro.layer": composes(target) ? "application_service" : "feature",
235
+ "sincpro.context": boundedContext,
236
+ "sincpro.execution_id": id,
237
+ };
238
+ const span = quietly(() => settings.tracer.start(`${boundedContext}/${identity}`, {
239
+ ...(parent === undefined ? {} : { parent: parent.span }),
240
+ attributes,
241
+ }), NO_SPAN);
242
+ const execution = { id, useCase, boundedContext, parent, span, context };
243
+ const finish = (failed, error) => {
244
+ const outcome = failed ? quietly(() => settings.outcomeOf(error), "failed") : "ok";
245
+ quietly(() => span.end(outcome, error), undefined);
246
+ if (outcome === "failed" && firstReport(error))
247
+ quietly(() => settings.reporter?.report(error, {
248
+ useCase,
249
+ traceId: span.traceId,
250
+ executionId: id,
251
+ context,
252
+ }), undefined);
253
+ };
254
+ let answer;
255
+ try {
256
+ answer = body.apply(viewOf(target, binding, execution), args);
257
+ }
258
+ catch (error) {
259
+ finish(true, error);
260
+ throw error;
261
+ }
262
+ if (answer instanceof Promise)
263
+ return answer.then((value) => {
264
+ finish(false);
265
+ return value;
266
+ }, (error) => {
267
+ finish(true, error);
268
+ throw error;
269
+ });
270
+ finish(false);
271
+ return answer;
272
+ }
@@ -7,15 +7,15 @@ import type { Span } from "../observability/ports.js";
7
7
  * nothing like it that survives an `await` (AsyncContext is a proposal, Zone.js patches the
8
8
  * world), and a global stack mixes the parents of two use cases running at once — which a
9
9
  * screen with several windows does all the time. So an execution is a value, handed to what
10
- * the use case calls: its Features (`ApplicationService`) and its dependencies (`BIND`).
10
+ * the use case calls: its Features (`this.feature`) and its dependencies (`BIND`).
11
11
  */
12
12
  export type Execution = {
13
13
  /** UUID v7, as 32 hex: this run. */
14
14
  readonly id: string;
15
- /** `<domain>/<Feature>` — the span's name, as the backend names its own. */
15
+ /** `<bounded context>.<Service>[.<method>]` — the use case's identity, as the backend names its own. */
16
16
  readonly useCase: string;
17
- /** The bounded context the use case belongs to, when it was bound to one. */
18
- readonly domain: string | undefined;
17
+ /** The bounded context the use case belongs to. */
18
+ readonly boundedContext: string;
19
19
  /** The run that executed this one: an ApplicationService, for its Features. */
20
20
  readonly parent: Execution | undefined;
21
21
  readonly span: Span;
@@ -25,7 +25,7 @@ export type Execution = {
25
25
  /**
26
26
  * A dependency that has something to say about the execution it is used in: given that
27
27
  * execution, it answers itself bound to it. The transport and every generated client are;
28
- * anything else in a Feature's `deps` is handed as it is.
28
+ * any other dependency of a bounded context is handed as it is.
29
29
  */
30
30
  export declare const BIND: unique symbol;
31
31
  export type Bindable<T> = {
@@ -33,8 +33,8 @@ export type Bindable<T> = {
33
33
  };
34
34
  /**
35
35
  * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
36
- * (`sincpro-context`, the header sincpro_framework's entrances read). Nothing when there is no
37
- * trace and no context.
36
+ * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
37
+ * Nothing when there is no trace and no context that travels.
38
38
  */
39
39
  export declare function headersOf(execution: Execution): Record<string, string>;
40
40
  /**
@@ -49,7 +49,7 @@ export declare function bindTransport(transport: Transport, execution: Execution
49
49
  /**
50
50
  * A client made over a transport, made again over the transport bound to each execution it is
51
51
  * used in. What a generated `clientOf(transport)` answers, so a Feature that calls
52
- * `this.deps.project.sendIssue(…)` sends that request with its own trace.
52
+ * `this.client.sendIssue(…)` sends that request with its own trace.
53
53
  *
54
54
  * export function clientOf(transport: Transport) {
55
55
  * return bindable(transport, (transport) => ({ sendIssue: (body) => transport.send(…) }));
@@ -1,7 +1,8 @@
1
+ import { plainCodec, travels } from "../context/codec.js";
1
2
  /**
2
3
  * A dependency that has something to say about the execution it is used in: given that
3
4
  * execution, it answers itself bound to it. The transport and every generated client are;
4
- * anything else in a Feature's `deps` is handed as it is.
5
+ * any other dependency of a bounded context is handed as it is.
5
6
  */
6
7
  export const BIND = Symbol.for("sincpro.client-framework.bind");
7
8
  function isBindable(value) {
@@ -19,8 +20,8 @@ function isTransport(value) {
19
20
  }
20
21
  /**
21
22
  * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
22
- * (`sincpro-context`, the header sincpro_framework's entrances read). Nothing when there is no
23
- * trace and no context.
23
+ * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
24
+ * Nothing when there is no trace and no context that travels.
24
25
  */
25
26
  export function headersOf(execution) {
26
27
  const headers = {};
@@ -31,8 +32,9 @@ export function headersOf(execution) {
31
32
  if (span.state)
32
33
  headers["tracestate"] = span.state;
33
34
  }
34
- if (Object.keys(context).length > 0)
35
- headers["sincpro-context"] = JSON.stringify(context);
35
+ // Only what travels: an object or a function held in the context stays in this process.
36
+ if (Object.values(context).some(travels))
37
+ headers["sincpro-context"] = plainCodec.dumps(context);
36
38
  return headers;
37
39
  }
38
40
  /**
@@ -61,7 +63,7 @@ export function bindTransport(transport, execution) {
61
63
  /**
62
64
  * A client made over a transport, made again over the transport bound to each execution it is
63
65
  * used in. What a generated `clientOf(transport)` answers, so a Feature that calls
64
- * `this.deps.project.sendIssue(…)` sends that request with its own trace.
66
+ * `this.client.sendIssue(…)` sends that request with its own trace.
65
67
  *
66
68
  * export function clientOf(transport: Transport) {
67
69
  * return bindable(transport, (transport) => ({ sendIssue: (body) => transport.send(…) }));
@@ -1,60 +1,82 @@
1
- import { type Runtime } from "./runtime.js";
1
+ import type { ContextValues } from "../context/store.js";
2
2
  /**
3
- * One use case. What it needs from outside — the generated client of its domain, another
4
- * adapter — comes in `deps`, typed by the subclass.
3
+ * `Feature` and `ApplicationService`: the two kinds of service a bounded context holds, as in
4
+ * sincpro_framework. Each bounded context gives them its dependencies once, in its own base:
5
5
  *
6
- * export class SendIssue extends Feature<{ project: ProjectClient }> {
7
- * execute(input: dto.CommandSendIssue) {
8
- * return this.deps.project.sendIssue(input);
6
+ * // domains/project/infrastructure/bounded_context.ts
7
+ * export class Feature extends BaseFeature<ProjectDependencies> {}
8
+ * export class ApplicationService extends BaseApplicationService<ProjectDependencies, ProjectFeatures> {}
9
+ *
10
+ * // domains/project/services/issues.ts
11
+ * export class Issues extends Feature {
12
+ * close(input: dto.CommandCloseIssue) {
13
+ * this.session.require(); // each dependency is a property — typed
14
+ * return this.client.closeIssue(input);
9
15
  * }
10
16
  * }
11
17
  *
12
- * Context: all of a client's logic lives in Features and ApplicationServices, as in
13
- * sincpro_framework and @sincpro/mobile, and none of them writes a span or catches an error to
14
- * report it: `new` answers the instance behind interceptors, and every public method runs
15
- * inside its own execution — a span named `<domain>/<name>`, the context it runs with, its
16
- * report if nobody expected how it failed. `execute` is a use case's entry; another public
17
- * method is traced as `<name>.<method>`, and a name that starts with `_` is not traced at all.
18
- *
19
- * Inside a method, `this.deps` and `this.context` are this run's own: each dependency that can
20
- * be (the transport, a generated client) is bound to the execution, so its requests carry this
21
- * run's trace. So a Feature keeps nothing of a request on `this`, and declares no `#private`
22
- * fields — the run sees the instance through a view of its own, and `#private` refuses views;
23
- * TypeScript's `private` is the same promise without that.
24
- */
25
- export declare class Feature<Deps extends object = Record<string, never>> {
26
- protected readonly deps: Deps;
27
- /** What this run executes with (`RuntimeOptions.context`): empty outside a run. */
28
- protected readonly context: Readonly<Record<string, unknown>>;
29
- constructor(deps?: Deps);
30
- }
31
- /**
32
- * A use case that composes the Features of its own domain — the only difference with a
33
- * Feature. Each call to one of them is a child of this run: its span under this span, the same
34
- * context, the same trace on the wire.
35
- *
36
- * export class CloseIssue extends ApplicationService<{ preview: PreviewClose; send: SendIssue }> {
37
- * async execute(input: CloseIssueInput) {
38
- * const preview = await this.features.preview.execute(input);
39
- * if (preview.warnings.length > 0) return { blocked: preview };
40
- * return this.features.send.execute({ ...input, message: "cerrado" });
18
+ * // domains/project/services/close_and_notify.ts
19
+ * export class CloseAndNotify extends ApplicationService {
20
+ * async execute(input: dto.CommandCloseIssue) {
21
+ * await this.feature.Issues.close(input); // a child of this run, on the same trace
22
+ * return this.feature.Notices.tell(input);
41
23
  * }
42
24
  * }
43
25
  *
44
- * Context: it never executes another ApplicationService — sincpro_framework's rule, refused here
45
- * when it is constructed rather than found in a trace later.
26
+ * Context: a Feature is a facade — the operations of one part of the domain, each a public
27
+ * method; an ApplicationService composes the Features of its own bounded context and never
28
+ * another ApplicationService (sincpro_framework's rule). Neither writes a span nor catches an
29
+ * error to report it: the bounded context runs every public method inside its own execution
30
+ * (`UseBoundedContext`).
31
+ *
32
+ * The dependencies are typed, not declared: `BaseFeature<ProjectDependencies>` types
33
+ * `this.client` and `this.session` from the bounded context's dependency type — sincpro_framework's
34
+ * `DependencyContextType` — and the bounded context hands them in on each run. So a service
35
+ * constructs with nothing, keeps no dependency on `this`, and declares no `#private` field: a run
36
+ * sees the instance through a view of its own (its dependencies bound to its trace), and
37
+ * `#private` refuses views. TypeScript's `private` is the same promise without that.
46
38
  */
47
- export declare class ApplicationService<Features extends Readonly<Record<string, Feature<object>>>, Deps extends object = Record<string, never>> extends Feature<Deps> {
48
- protected readonly features: Features;
49
- constructor(features: Features, deps?: Deps);
39
+ declare class FeatureCore {
40
+ }
41
+ declare class ApplicationServiceCore extends FeatureCore {
50
42
  }
43
+ /** What every service sees on `this` inside a method. */
44
+ type Within<D extends object, C extends object> = Readonly<D> & {
45
+ /**
46
+ * What this run executes with: the process's context, its bounded context's, its scope's —
47
+ * read once where the gesture started and the same for every Feature it runs.
48
+ */
49
+ readonly context: Readonly<Partial<C>>;
50
+ };
51
+ /** Service classes by the name a bounded context gives them: `{ Issues, Notices }`. */
52
+ export type ServiceClasses = Readonly<Record<string, new () => object>>;
51
53
  /**
52
- * The services of a bounded context, each running in this runtime under that domain: traced as
53
- * `<domain>/<key>`, reported by its reporter, with its context.
54
- *
55
- * const project = bindServices(runtime, "project", { sendIssue, closeIssue });
56
- *
57
- * Context: a span is named by the key it is bound with, not by the class — a production build
58
- * renames classes, never the keys of an object.
54
+ * What a service offers to whoever holds it — `project.Issues`, `this.feature.Issues`: its public
55
+ * methods, without its dependencies, its context or its `_` helpers.
56
+ */
57
+ export type ServiceMethods<I, D> = {
58
+ readonly [M in keyof I as M extends keyof D | "context" | "feature" | `_${string}` ? never : I[M] extends (...args: never[]) => unknown ? M : never]: I[M];
59
+ };
60
+ /** The services of a set of classes, by their names. */
61
+ export type Services<S extends ServiceClasses, D> = {
62
+ readonly [K in keyof S]: ServiceMethods<InstanceType<S[K]>, D>;
63
+ };
64
+ type FeatureClass = abstract new <D extends object = Record<never, never>, C extends object = ContextValues>() => FeatureCore & Within<D, C>;
65
+ type ApplicationServiceClass = abstract new <D extends object = Record<never, never>, F extends ServiceClasses = Record<never, never>, C extends object = ContextValues>() => ApplicationServiceCore & Within<D, C> & {
66
+ /** The Features of this bounded context, each run a child of this one. */
67
+ readonly feature: Services<F, D>;
68
+ };
69
+ /** The base of a bounded context's Features: `class Feature extends BaseFeature<Deps> {}`. */
70
+ export declare const Feature: FeatureClass;
71
+ /**
72
+ * The base of a bounded context's ApplicationServices:
73
+ * `class ApplicationService extends BaseApplicationService<Deps, typeof features> {}`.
59
74
  */
60
- export declare function bindServices<Services extends Readonly<Record<string, Feature<object>>>>(runtime: Runtime, domain: string, services: Services): Services;
75
+ export declare const ApplicationService: ApplicationServiceClass;
76
+ /** Whether a class is a Feature — and not an ApplicationService. */
77
+ export declare function isFeatureClass(service: new () => object): boolean;
78
+ export declare function isApplicationServiceClass(service: new () => object): boolean;
79
+ /** Whether a service composes Features — an ApplicationService. */
80
+ export declare function composes(service: object): boolean;
81
+ export declare function methodsOf(service: object): ReadonlySet<string>;
82
+ export {};