@sincpro/client-framework 0.1.0 → 0.3.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.
package/AGENTS.md CHANGED
@@ -8,23 +8,29 @@ it says what this package is responsible for and what it is not. This file is ho
8
8
  - **Read the manifesto.** A piece that serves nothing there does not go in; a piece it forbids
9
9
  does not either. If you think it should, change the manifesto first and say why.
10
10
  - **Look at the backend.** This package mirrors sincpro_framework (`~/dev/sincpro_framework`):
11
- `Feature`, `ApplicationService`, the context, the span per use case. When in doubt about a
11
+ the bounded context and its dependencies, `Feature`, `ApplicationService`, the context and its
12
+ store, the process, the span per use case. When in doubt about a
12
13
  word or a behaviour, read how the backend does it, not memory.
14
+ - **Use the shared words.** [`docs/UBIQUITOUS_LANGUAGE.md`](docs/UBIQUITOUS_LANGUAGE.md) is the
15
+ vocabulary of every Sincpro framework. A new word, or a word that changes, goes there in the
16
+ same pull request; a word in its «do not use» table never enters code or docs.
13
17
  - **Look at who uses it.** @sincpro/web-framework (`~/dev/sincpro_web_framework`) and
14
18
  @sincpro/mobile (`~/dev/mobile/sincpro_mobile`). A piece only one of them needs, and that
15
19
  needs a platform, belongs there.
16
20
 
17
21
  ## Where things go
18
22
 
19
- | Folder | What |
20
- | -------------------- | --------------------------------------------------------------------------------------- |
21
- | `src/client/` | The seam with a server: `Transport`, REST, memory, `entity()`, the client contract, ids |
22
- | `src/model/` | Model definitions: `defineMeta`, from a JSON Schema, the store of known ones |
23
- | `src/services/` | `Feature`, `ApplicationService`, the runtime, the execution and what binds to it |
24
- | `src/observability/` | The ports a platform plugs: `Tracer`, `ErrorReporter` |
25
- | `src/codegen/` | The generator and its CLI (`sincpro-client generate`) — the only place Node is allowed |
26
- | `src/testing/` | `memoryTracer`, `memoryReporter` — the ports in memory, for tests |
27
- | `tests/` | vitest in Node; `tests/support/` holds the fake domains and forge's real contracts |
23
+ | Folder | What |
24
+ | -------------------- | ------------------------------------------------------------------------------------------- |
25
+ | `src/client/` | The seam with a server: `Transport`, REST, memory, `entity()`, the client contract, ids |
26
+ | `src/model/` | Model definitions: `defineMeta`, from a JSON Schema, the store of known ones |
27
+ | `src/services/` | `UseBoundedContext`, `Feature`, `ApplicationService`, the process, the execution |
28
+ | `src/context/` | The `ContextStore` port, `memoryContexts`, and `Context` — one level of it |
29
+ | `src/observability/` | The ports a platform plugs: `Tracer`, `ErrorReporter` |
30
+ | `src/codegen/` | The generator and its CLI (`sincpro-client generate`) — the only place Node is allowed |
31
+ | `src/adapters/` | Optional entries over a vendor (`./opentelemetry`, `./sentry`): the only place one is named |
32
+ | `src/testing/` | `memoryTracer`, `memoryReporter` — the ports in memory, for tests |
33
+ | `tests/` | vitest in Node; `tests/support/` holds the fake domains and forge's real contracts |
28
34
 
29
35
  ## Hard rules (the build enforces them — `make doctor`)
30
36
 
@@ -32,7 +38,7 @@ it says what this package is responsible for and what it is not. This file is ho
32
38
  no `document`, `window`, `localStorage`, `navigator`. What a platform must give, it gives
33
39
  through a port.
34
40
  - **Node only in the generator.** `node:` imports, `process` and `Buffer` live in `src/codegen/`
35
- and nowhere else; nothing in the runtime imports `codegen/`.
41
+ and nowhere else; nothing an application runs imports `codegen/`.
36
42
  - **Self-imports, no extensions.** Inside `src/`, import `@sincpro/client-framework/<path>` —
37
43
  never a relative path, never `.ts`.
38
44
 
@@ -47,24 +53,42 @@ it says what this package is responsible for and what it is not. This file is ho
47
53
 
48
54
  ## Known traps
49
55
 
50
- - **A run sees the instance through a view of its own.** Inside a method, `this.deps`,
51
- `this.context` and `this.features` are that run's — bound to its execution. So a Feature keeps
52
- nothing of a request on `this`, and declares no `#private` fields (they refuse the view); use
53
- TypeScript's `private`.
54
- - **A span is named by the key, not the class** (`bindServices(runtime, "project", { sendIssue })`
55
- → `project/sendIssue`; an ApplicationService's Features by their keys too): a production build
56
- renames classes. Only an unbound Feature falls back to its class name.
57
- - **The trace reaches the wire through `deps`.** A dependency is bound to the run when it is a
58
- `Transport` or carries `BIND` (every generated client does, through `bindable`); anything else
59
- is handed as it is. A client built outside `deps` and closed over sends no trace.
56
+ - **A run sees the instance through a view of its own.** Inside a method, each dependency
57
+ (`this.client`), `this.context` and `this.feature` are that run's — bound to its execution. So a
58
+ service declares no `#private` fields (they refuse the view); use TypeScript's `private`.
59
+ - **A service is a singleton of its bounded context.** What a method writes on `this` lands on the
60
+ instance, shared by the next call, the call beside it and every `with`/`scoped` variant — as in
61
+ sincpro_framework. What belongs to one request goes in its arguments or its context.
62
+ - **A span is named by the registered name, not the class** (`register({ features: { Issues } })`
63
+ → `project/Issues.close`, identity `project.Issues.close`): a production build renames classes,
64
+ never the keys of an object.
65
+ - **The dependencies are typed, not declared.** `BaseFeature<ProjectDependencies>` types
66
+ `this.client` from an interface, which does not exist at run time; the bounded context answers
67
+ whatever was added with `addDependency`. A dependency nobody added reads `undefined`.
68
+ - **The trace reaches the wire through the dependencies.** A dependency is bound to the run when it
69
+ is a `Transport` or carries `BIND` (every generated client does, through `bindable`); anything
70
+ else is handed as it is. A client built elsewhere and closed over sends no trace.
71
+ - **The process is read on every call, never at import.** `project` is defined when its module
72
+ loads, before `main` runs `configureProcess`; nothing may capture the tracer, the reporter or the
73
+ store at definition time. `configureProcess` replaces everything at once — a test calls it in
74
+ `beforeEach` and starts with an empty context.
75
+ - **A store's `load` may answer a new object each time** (one that parses JSON does): `Context`
76
+ keeps one object per `version`, so a screen can hand `values` to `useSyncExternalStore`. A store
77
+ that changes without moving the version is a broken store.
60
78
  - **What the generator writes is compared byte for byte** (`tests/support/forge-contract/`, out
61
79
  of prettier and eslint). After changing the generator on purpose, run `make codegen-fixtures`
62
80
  and read the diff.
63
81
  - **A module name the generator writes is built in parts** (`codegen/names.ts`, `FRAMEWORK`): the
64
82
  build's `tsc-alias` rewrites this package's specifiers inside strings too.
83
+ - **An adapter wraps what the application built, never builds it.** `openTelemetryTracer` takes
84
+ the app's tracer, `sentryReporter` the app's client: no `register()`, no `Sentry.init()`, no
85
+ endpoint or DSN here, nothing read from the environment. A vendor's SDK is imported only in
86
+ `src/adapters/`, and nothing else imports an adapter (`make doctor`).
87
+ - **A span nothing records claims no trace.** OpenTelemetry's API with no provider answers an
88
+ all-zeros span: the adapter gives no ids, so no `traceparent` of zeros reaches the server.
65
89
  - **Reported means unexpected.** A `TransportError` the server answered with a 4xx is `refused`,
66
90
  an `AbortError` is `aborted`; neither is reported (`outcomeOf`). Change that with
67
- `RuntimeOptions.outcomeOf`, never by catching in a Feature.
91
+ `ProcessOptions.outcomeOf`, never by catching in a Feature.
68
92
 
69
93
  ## Before you hand it back
70
94
 
package/MANIFESTO.md CHANGED
@@ -25,22 +25,27 @@ server-side render, in a script and in a test, unchanged.**
25
25
 
26
26
  ## What it is responsible for
27
27
 
28
- 1. **Use cases.** `Feature` is one use case, with its dependencies injected; `ApplicationService`
29
- is a Feature that also executes the Features of its own domain — never another
30
- ApplicationService. All of a client's logic lives in them.
28
+ 1. **Services.** A `Feature` is a facade over one part of the domain, each public method a use
29
+ case; an `ApplicationService` composes the Features of its own bounded context — never another
30
+ ApplicationService. All of a client's logic lives in them, with their dependencies typed and
31
+ injected by their bounded context (`this.client`, `this.session`).
31
32
  2. **Interception.** Every public method of a use case passes through interceptors, and that is
32
33
  what makes the defaults free: the trace, the error report, the context. The author of a
33
34
  Feature writes none of it.
34
35
  3. **Observability, as ports.** `Tracer` and `ErrorReporter` are the contract: one span per use
35
- case, named `<domain>/<Feature>` like the backend's; the unexpected reported with its trace id;
36
- what the server already explained (a validation, a conflict) not reported. The platform plugs
37
- the implementation — OpenTelemetry, GlitchTip, a queue on the phone, memory in a test.
38
- 4. **Context.** The levels a call sees — the application, its domain, the call itself — and what
39
- travels to the server with it (`traceparent`, `sincpro-context`). Carried by each call, never
40
- kept in a global: two calls at once never mix their parents.
41
- 5. **Domains and the application.** A bounded context is declared once — its client, its use
42
- cases, what they depend on — and an application is the set of domains it loads. Domains never
43
- import each other; they talk by reference.
36
+ case, named `<bounded context>/<Service>.<method>` like the backend's; the unexpected reported
37
+ with its trace id; what the server already explained (a validation, a conflict) not reported.
38
+ The platform plugs the implementation — OpenTelemetry, GlitchTip, a queue on the phone, memory
39
+ in a test — once, for the process (`configureProcess`).
40
+ 4. **Context.** The levels a call sees — the process, its bounded context, a scope — read once
41
+ where a gesture starts and inherited by everything it runs, and what travels to the server with
42
+ it (`traceparent`, `sincpro-context`). Where the values live is a port (`ContextStore`) each
43
+ platform implements with its own technology; the execution is carried by each call, never kept
44
+ in a global stack: two calls at once never mix their parents.
45
+ 5. **Bounded contexts.** Set up like the backend's — its dependencies (`addDependency`), its
46
+ Features and ApplicationServices, registered once — and used through one light object:
47
+ `project.Issues.close(input)`. An application is the set of bounded contexts it loads; they
48
+ never import each other's insides.
44
49
  6. **The client.** The `Transport` port and its implementations (REST, memory), `entity()` with
45
50
  typed commands and answers, the service's client contract, model definitions read once from
46
51
  the server, client-side ids (UUID v7) and the server's `Problem`.
@@ -55,11 +60,15 @@ server-side render, in a script and in a test, unchanged.**
55
60
  the web and to @sincpro/mobile on a phone.
56
61
  - **Not a shell, a router or a window manager.** Where things open is the platform's.
57
62
  - **Not a cache or a state manager.** Holding server state for screens is the platform's job,
58
- with the libraries that do it best.
63
+ with the libraries that do it best. The context is not one either: a handful of values every
64
+ use case runs with — the tenant, the company — kept wherever the platform says.
59
65
  - **Not offline storage.** SQLite on a phone or IndexedDB in a point of sale are platform
60
66
  adapters.
61
- - **Not a telemetry or error-tracking SDK.** It defines the ports and calls them; it bundles no
62
- vendor.
67
+ - **Not a telemetry or error-tracking SDK.** It defines the ports and calls them. For the
68
+ common vendors it offers optional adapters (`./opentelemetry`, `./sentry`) that wrap what the
69
+ application built — its provider, its client, with its endpoint and its DSN. It initialises
70
+ nothing and registers nothing global; an application that does not import them installs
71
+ nothing and ships nothing, and without a tracer or a reporter everything runs the same.
63
72
  - **Not where business rules live.** The server keeps them; a client reads what it publishes.
64
73
  - **Not an event bus**, and not a dispatcher: a use case is executed by calling it, and the
65
74
  interceptors are what make that call traced and reported.
@@ -69,14 +78,16 @@ server-side render, in a script and in a test, unchanged.**
69
78
  1. **Platform-neutral, or it does not go in.** It runs on Node, in a browser and on React Native.
70
79
  What it needs from a platform it asks through a port, never by reaching for it. The build
71
80
  enforces it.
72
- 2. **The backend's words.** Feature, ApplicationService, domain, context, the span name — the
73
- same as sincpro_framework, so whoever knows the backend already knows the client.
74
- 3. **Nothing escapes the interceptors.** Reusing a use case is executing its Feature. A helper
75
- that wraps one skips the trace and the report — that is how an incident ends up with nothing
76
- to look at.
81
+ 2. **The backend's words.** Bounded context, Feature, ApplicationService, dependencies, context,
82
+ process, the span name — the same as sincpro_framework, so whoever knows the backend already
83
+ knows the client.
84
+ 3. **Nothing escapes the interceptors.** Reusing a use case is calling it through its bounded
85
+ context. A helper that wraps one skips the trace and the report — that is how an incident ends
86
+ up with nothing to look at.
77
87
  4. **Context travels with the call.** Never in a global stack: clients run many things at once.
78
88
  5. **A port where there are two implementations, a class where there is one.** `Transport` has
79
- REST and memory; `Tracer` has a platform's and a test's.
89
+ REST and memory; `Tracer` has a platform's and a test's; `ContextStore` has the browser's, the
90
+ phone's and memory.
80
91
  6. **Generated, never edited.** Applications extend by composition; regenerating never loses
81
92
  work.
82
93
  7. **The server is the source of truth.** A client reads the model, the preview and the refusal,
@@ -92,8 +103,8 @@ server-side render, in a script and in a test, unchanged.**
92
103
  @sincpro/criteria the contract with the server: Criteria, Meta, Page, Problem
93
104
  ▲
94
105
  @sincpro/client-framework this package — platform-neutral
95
- │ Feature · ApplicationService · interceptors · context per call
96
- │ Domain · application · Tracer and ErrorReporter ports
106
+ │ UseBoundedContext · Feature · ApplicationService · interceptors
107
+ │ the process · the context · Tracer, ErrorReporter and ContextStore ports
97
108
  │ Transport · entity() · client contract · model definitions · ids
98
109
  │ the generator · in-memory doubles for tests
99
110
  ▲
@@ -112,8 +123,14 @@ where they already ran without the DOM. `Feature` and `ApplicationService` came
112
123
  ports for tracing and error reporting are in, with the trace and the context written on every
113
124
  request a use case sends.
114
125
 
115
- **Next:** the domains and the application. The plan is written in @sincpro/web-framework
116
- (`docs/prd/PRD_02_marco-de-trabajo.md`, in Spanish like the rest of Sincpro's design records).
126
+ **Then the bounded context**, set up as the backend sets one up: a dependency type that types every
127
+ service's `this.client`, `addDependency`, the services registered by name and used as
128
+ `project.Issues.close(input)` and `this.feature.Issues`; the process configured once in `main`
129
+ (`configureProcess`); and the context as a port, so the browser and the phone keep it each with
130
+ their own technology behind the same door.
131
+
132
+ **Next:** @sincpro/mobile on this package, and the screens' side in @sincpro/web-framework — the
133
+ cache of reads, invalidated by the writes a use case sends.
117
134
 
118
135
  ## How we decide
119
136
 
package/README.md CHANGED
@@ -5,60 +5,317 @@ every platform.** Write a use case once — a `Feature`, or an `ApplicationServi
5
5
  Features — and run it in a browser, on a phone, in a server-side render, in a script or in a test.
6
6
  Every use case is traced and every unexpected error reported, continuing the backend's trace,
7
7
  without its author writing either. A service's client contract becomes typed code for each of
8
- its domains.
8
+ its bounded contexts.
9
9
 
10
- > **Status: 0.x, the first cut.** The API may still change before 1.0. In: the client, the
11
- > generator, `Feature` and `ApplicationService` with their interceptors, and the ports for tracing
12
- > and error reporting. Read the [manifesto](MANIFESTO.md) for what this package is responsible
13
- > for, what it is not and where it is going.
10
+ > **Status: 0.x.** The API may still change before 1.0. In: the client, the generator, the
11
+ > bounded context with its `Feature` and `ApplicationService`, the process (tracing, error
12
+ > reporting, the context) and its ports. Read the [manifesto](MANIFESTO.md) for what this package
13
+ > is responsible for, what it is not and where it is going, and the
14
+ > [ubiquitous language](docs/UBIQUITOUS_LANGUAGE.md) every Sincpro framework shares.
14
15
 
15
- ## A use case, traced and reported by default
16
+ ## What it gives, at a glance
17
+
18
+ ```mermaid
19
+ classDiagram
20
+ direction LR
21
+ class UseBoundedContext {
22
+ +name
23
+ +context : Context
24
+ +addDependency(name, value)
25
+ +register(features, applicationServices) BoundedContext
26
+ }
27
+ class BoundedContext {
28
+ +Issues : the methods of Issues
29
+ +CloseAndNotify : the methods of CloseAndNotify
30
+ +context : Context
31
+ +with(dependencies) BoundedContext
32
+ +scoped(context) BoundedContext
33
+ }
34
+ class Feature {
35
+ #client, session : each dependency
36
+ #context : what this run executes with
37
+ }
38
+ class ApplicationService {
39
+ #feature : Issues, Notices
40
+ }
41
+ class Context {
42
+ +values
43
+ +get(name)
44
+ +set(values)
45
+ +clear(names)
46
+ +watch(listener)
47
+ }
48
+ class ContextStore {
49
+ <<port>>
50
+ +load(key)
51
+ +save(key, values)
52
+ +delete(key)
53
+ +version(key)
54
+ +watch(key, listener)
55
+ }
56
+ class Tracer {
57
+ <<port>>
58
+ +start(name, parent, attributes) Span
59
+ }
60
+ class ErrorReporter {
61
+ <<port>>
62
+ +report(error, about)
63
+ }
64
+ class Transport {
65
+ <<port>>
66
+ +list(route, criteria)
67
+ +one(route)
68
+ +send(method, route, body)
69
+ }
70
+ UseBoundedContext --> BoundedContext : register
71
+ BoundedContext o-- Feature : each by its name
72
+ Feature <|-- ApplicationService
73
+ BoundedContext --> Context : its level
74
+ Context --> ContextStore : kept in
75
+ BoundedContext ..> Tracer : a span per use case
76
+ BoundedContext ..> ErrorReporter : what nobody expected
77
+ Feature ..> Transport : through its generated client
78
+ ```
79
+
80
+ `configureProcess({ tracer, reporter, context })` plugs the ports once; `processContext` is the
81
+ process's level of the context. Beside them: the generated client (`sincpro-client generate`),
82
+ `entity()`, model definitions (`defineMeta`, `metaFromSchema`) and client-side ids.
83
+
84
+ ## A bounded context, set up like the backend's
85
+
86
+ Each bounded context of an application has the layout sincpro_framework gives a service: what its
87
+ services can count on, its bases, its services, and the one object the rest of the application
88
+ uses it through.
89
+
90
+ ```mermaid
91
+ flowchart LR
92
+ subgraph py["sincpro_framework · Python"]
93
+ direction TB
94
+ pd["infrastructure/dependencies.py<br/>DependencyContextType · add_dependency"]
95
+ pf["infrastructure/framework.py<br/>Feature · ApplicationService · UseFramework"]
96
+ ps["services/<br/>@bus.feature · @bus.app_service"]
97
+ pi["__init__.py<br/>the bounded context"]
98
+ pd --> pf --> ps --> pi
99
+ end
100
+ subgraph ts["@sincpro/client-framework · TypeScript"]
101
+ direction TB
102
+ td["infrastructure/dependencies.ts<br/>ProjectDependencies · addDependency"]
103
+ tf["infrastructure/bounded_context.ts<br/>Feature · ApplicationService · UseBoundedContext"]
104
+ tsv["services/<br/>features · applicationServices"]
105
+ ti["index.ts<br/>export const project"]
106
+ td --> tf --> tsv --> ti
107
+ end
108
+ ti --> users["a screen · another bounded context · a test<br/>project.Issues.close(input)"]
109
+ pi -. "the client contract" .-> td
110
+ ```
16
111
 
17
112
  ```ts
113
+ // domains/project/infrastructure/dependencies.ts — what the services can count on
114
+ export interface ProjectDependencies {
115
+ client: ProjectClient; // generated from the service's contract
116
+ session: Session;
117
+ }
118
+
119
+ export function registerDependencies(bc: UseBoundedContext<ProjectDependencies>) {
120
+ bc.addDependency("client", clientOf(transport));
121
+ bc.addDependency("session", session);
122
+ return bc;
123
+ }
124
+ ```
125
+
126
+ ```ts
127
+ // domains/project/infrastructure/bounded_context.ts
18
128
  import {
19
- ApplicationService,
20
- bindServices,
21
- createRuntime,
22
- Feature,
129
+ ApplicationService as BaseApplicationService,
130
+ Feature as BaseFeature,
131
+ UseBoundedContext,
23
132
  } from "@sincpro/client-framework";
24
133
 
25
- class SendIssue extends Feature<{ project: ProjectClient }> {
26
- execute(input: dto.CommandSendIssue) {
27
- return this.deps.project.sendIssue(input); // carries this run's traceparent
134
+ import type { ProjectFeatures } from "../services";
135
+
136
+ export class Feature extends BaseFeature<ProjectDependencies> {}
137
+ export class ApplicationService extends BaseApplicationService<
138
+ ProjectDependencies,
139
+ ProjectFeatures
140
+ > {}
141
+
142
+ export function configProjectBoundedContext(name: string) {
143
+ return registerDependencies(new UseBoundedContext<ProjectDependencies>(name));
144
+ }
145
+ ```
146
+
147
+ ```ts
148
+ // domains/project/services/issues.ts — a Feature is a facade: one part of the domain
149
+ export class Issues extends Feature {
150
+ close(input: dto.CommandCloseIssue) {
151
+ this.session.require(); // each dependency is a typed property
152
+ return this.client.closeIssue(input); // carries this run's traceparent
28
153
  }
29
154
  }
30
155
 
31
- class CloseIssue extends ApplicationService<{ preview: PreviewClose; send: SendIssue }> {
32
- async execute(input: CloseIssueInput) {
33
- const preview = await this.features.preview.execute(input); // a child span
34
- if (preview.warnings.length > 0) return { blocked: preview };
35
- return this.features.send.execute({ ...input, message: "closed" }); // another child
156
+ // domains/project/services/close_and_notify.ts — composes the Features of its bounded context
157
+ export class CloseAndNotify extends ApplicationService {
158
+ async execute(input: dto.CommandCloseIssue) {
159
+ await this.feature.Issues.close(input); // a child span, the same trace
160
+ return this.feature.Notices.tell(input);
36
161
  }
37
162
  }
38
163
 
39
- const runtime = createRuntime({ tracer, reporter, context: () => ({ tenant_id: "acme" }) });
40
- const project = bindServices(runtime, "project", {
41
- closeIssue: new CloseIssue({ preview: new PreviewClose(deps), send: new SendIssue(deps) }),
164
+ // domains/project/services/index.ts
165
+ export const features = { Issues, Notices };
166
+ export const applicationServices = { CloseAndNotify };
167
+ export type ProjectFeatures = typeof features;
168
+ ```
169
+
170
+ ```ts
171
+ // domains/project/index.ts
172
+ export const project = configProjectBoundedContext("project").register({
173
+ features,
174
+ applicationServices,
175
+ });
176
+
177
+ // anywhere: a screen, a script, a test
178
+ await project.Issues.close(input); // span project/Issues.close
179
+ await project.CloseAndNotify.execute(input); // project/CloseAndNotify → its Features under it
180
+ ```
181
+
182
+ Every public method is a use case: a span named as the backend names its own
183
+ (`project/Issues.close`, identity `project.Issues.close`), and a report when it fails in a way
184
+ nobody expected. One gesture is one trace, from the click to the database:
185
+
186
+ ```mermaid
187
+ sequenceDiagram
188
+ participant caller as Caller<br/>(a screen, a test)
189
+ participant app as project.CloseAndNotify<br/>(ApplicationService)
190
+ participant feature as this.feature.Issues<br/>(Feature)
191
+ participant client as this.client<br/>(generated, bound to the run)
192
+ participant process as Process<br/>(Tracer · ErrorReporter)
193
+ participant server as sincpro_framework
194
+
195
+ caller->>app: execute(input)
196
+ app->>process: span project/CloseAndNotify — the context read once
197
+ app->>feature: close(input)
198
+ feature->>process: child span project/Issues.close
199
+ feature->>client: closeIssue(input)
200
+ client->>server: POST /issues/{issue_id}/close · traceparent · sincpro-context
201
+ server-->>client: ResponseCloseIssue — or a Problem
202
+ client-->>caller: the answer, through each run
203
+ Note over process: each run ends ok · refused (shown, never reported) · aborted · failed (reported once, with its trace id)
204
+ ```
205
+
206
+ `project` holds references only — each service is made once, the first time it is
207
+ asked for — and it is never frozen: `project.with({ client: fake })` and
208
+ `project.scoped({ company_id: 2 })` derive a variant that shares everything else.
209
+
210
+ ## The process: tracing, errors and the context
211
+
212
+ What every bounded context runs with is set once, where the application starts — the way the
213
+ backend's process is. All of it is optional:
214
+
215
+ ```ts
216
+ // entrypoints/main.ts
217
+ configureProcess({
218
+ tracer, // the platform's Tracer
219
+ reporter, // the platform's ErrorReporter
220
+ context: memoryContexts(), // where the context lives (this is the default)
42
221
  });
43
- await project.closeIssue.execute(input); // project/closeIssue → project/preview, project/send
222
+
223
+ processContext.set({ tenant_id: "acme" }); // every use case runs with it and sends it
224
+ project.context.set({ warehouse_id: 4 }); // only what «project» runs
225
+ ```
226
+
227
+ A service reads it as `this.context` — the process's values, under them its bounded context's,
228
+ under them a scope's, read once where the gesture started — and every request it sends carries
229
+ it in `sincpro-context`. A screen hears it change with `processContext.watch(…)`.
230
+
231
+ ```mermaid
232
+ flowchart LR
233
+ p["processContext<br/>tenant_id · company_id"]
234
+ b["project.context<br/>warehouse_id"]
235
+ s["project.scoped({ company_id: 2 })<br/>one window"]
236
+ e["this.context<br/>read once where the gesture starts"]
237
+ c["this.context of each Feature it runs<br/>the same values"]
238
+ h["sincpro-context<br/>on every request"]
239
+ store[("ContextStore<br/>the platform's")]
240
+ p --> e
241
+ b --> e
242
+ s --> e
243
+ e --> c
244
+ e --> h
245
+ c --> h
246
+ p -.-> store
247
+ b -.-> store
44
248
  ```
45
249
 
46
- `tracer` and `reporter` are the platform's: OpenTelemetry and GlitchTip in a browser, a queue on
47
- a phone, `memoryTracer()` and `memoryReporter()` (`@sincpro/client-framework/testing`) in a test.
48
- A use case that was never bound runs too, with nothing tracing.
250
+ The context lives in a `ContextStore`, the port each platform implements with its own technology:
251
+ memory by default, `sessionStorage` or the screens' own store in a browser, MMKV on a phone,
252
+ `AsyncLocalStorage` in a server that renders. The services never know which.
253
+
254
+ `tracer` and `reporter` are injected by the application — this package initialises nothing.
255
+ Any object that fulfils the port works; for the common vendors there are optional adapters that
256
+ wrap what the application built:
257
+
258
+ ```ts
259
+ import { openTelemetryTracer } from "@sincpro/client-framework/opentelemetry"; // peer: @opentelemetry/api
260
+ import { sentryReporter } from "@sincpro/client-framework/sentry"; // no dependency
261
+
262
+ const tracer = openTelemetryTracer(provider.getTracer("my-app")); // the app's provider and exporter
263
+ const reporter = sentryReporter(client); // the app's Sentry-compatible client (GlitchTip too)
264
+ ```
265
+
266
+ Nothing is installed or shipped unless it is imported, and without a tracer or a reporter every use
267
+ case runs the same — nothing traces, nothing reports. In a test, `memoryTracer()` and
268
+ `memoryReporter()` (`@sincpro/client-framework/testing`).
49
269
 
50
270
  ## Where it sits
51
271
 
52
- ```text
53
- @sincpro/criteria the contract with the server
54
- ▲
55
- @sincpro/client-framework use cases · interceptors · context · ports · domains · the client
56
- ▲
57
- @sincpro/web-framework the web @sincpro/mobile a phone
272
+ ```mermaid
273
+ flowchart TB
274
+ backend["sincpro_framework<br/>the service: its bounded contexts, Commands, the client contract"]
275
+ criteria["@sincpro/criteria<br/>Criteria · Meta · Page · Problem"]
276
+ cf["@sincpro/client-framework<br/>bounded contexts · services · process · context · ports · the client · the generator"]
277
+ web["@sincpro/web-framework<br/>hooks · screens · the browser's tracer and reporter"]
278
+ mobile["@sincpro/mobile<br/>React Native · SQLite · its queue"]
279
+ rest["a script · a server-side render · a test"]
280
+ web --> cf
281
+ mobile --> cf
282
+ rest --> cf
283
+ cf --> criteria
284
+ backend -. "client contract → sincpro-client generate" .-> cf
285
+ cf -- "HTTP · traceparent · sincpro-context" --> backend
58
286
  ```
59
287
 
60
288
  No React, no DOM, no platform: what a platform must provide — how a span is exported, where an
61
- error is reported — it plugs through a port.
289
+ error is reported, where the context lives — it plugs through a port.
290
+
291
+ ## On each platform
292
+
293
+ This package asks the platform for little: `fetch`, `AbortSignal`, `URL` / `URLSearchParams`,
294
+ `crypto.getRandomValues` (client-side ids, retry keys), `Proxy` / `WeakMap`. Browsers and Node 26
295
+ have them all. **React Native** (Hermes) needs two polyfills, installed once where the app
296
+ starts, before anything imports this package:
297
+
298
+ ```ts
299
+ import "react-native-get-random-values"; // or expo-crypto's getRandomValues polyfill
300
+ import "react-native-url-polyfill/auto"; // RN's own URL resolves relative paths poorly
301
+ ```
302
+
303
+ `memoryTransport` copies with `structuredClone` where the engine has it and a JSON round trip
304
+ where it does not. The OpenTelemetry adapter takes `@opentelemetry/api` from 1.9, the version
305
+ `@sincpro/mobile` runs.
306
+
307
+ The services are the same everywhere; each platform plugs its own technology behind the same
308
+ three ports, in its `main`:
309
+
310
+ | Port | Browser (@sincpro/web-framework) | Phone (@sincpro/mobile) | Test |
311
+ | --------------- | ----------------------------------------------- | ------------------------------------------ | ----------------- |
312
+ | `Tracer` | OpenTelemetry + OTLP/HTTP (`./telemetry`) | OpenTelemetry with its queue and exporter | `memoryTracer` |
313
+ | `ErrorReporter` | GlitchTip / Sentry browser client (`./errors`) | Sentry React Native client | `memoryReporter` |
314
+ | `ContextStore` | memory, `sessionStorage`, or the screens' store | MMKV, or memory hydrated from AsyncStorage | `memoryContexts` |
315
+ | `Transport` | `restTransport` over `fetch` | `restTransport`, or SQLite offline | `memoryTransport` |
316
+
317
+ A store's reads are synchronous — a use case reads its context where a gesture starts and never
318
+ waits for it — so an asynchronous storage is held in memory and written through.
62
319
 
63
320
  ## Development
64
321
 
@@ -0,0 +1,21 @@
1
+ import { type Tracer as OtelTracer } from "@opentelemetry/api";
2
+ import type { Tracer } from "../observability/ports.js";
3
+ /**
4
+ * The `Tracer` port over OpenTelemetry's API: every use case a span of whatever provider the
5
+ * platform registered.
6
+ *
7
+ * // a browser (@sincpro/web-framework/telemetry does this), Node, React Native…
8
+ * const tracer = openTelemetryTracer(provider.getTracer("forge-web"));
9
+ * configureProcess({ tracer });
10
+ *
11
+ * Context: only `@opentelemetry/api`, which runs the same on every platform — the provider, its
12
+ * exporter and where it sends are the platform's (the browser's OTLP over fetch, a phone's
13
+ * queue, a server's SDK). An optional entry with an optional peer: an application that does not
14
+ * import it installs nothing and ships nothing.
15
+ *
16
+ * The parent is given, never read from OpenTelemetry's context: a run carries its own execution
17
+ * (`Execution`), so two use cases at once never take each other's span, and no context manager
18
+ * (Zone.js) is needed. A span nothing records (a sampler said no, no provider registered)
19
+ * answers no ids — so no `traceparent` claims a trace that does not exist.
20
+ */
21
+ export declare function openTelemetryTracer(tracer: OtelTracer): Tracer;