@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.
- package/AGENTS.md +51 -23
- package/MANIFESTO.md +44 -26
- package/README.md +304 -29
- package/dist/adapters/opentelemetry.d.ts +1 -1
- package/dist/adapters/opentelemetry.js +1 -1
- package/dist/adapters/sentry.d.ts +2 -2
- package/dist/adapters/sentry.js +2 -2
- package/dist/client/memory.js +14 -6
- package/dist/common/decimal.d.ts +49 -0
- package/dist/common/decimal.js +146 -0
- package/dist/{client → common}/ids.d.ts +7 -0
- package/dist/{client → common}/ids.js +12 -0
- package/dist/common/money.d.ts +74 -0
- package/dist/common/money.js +168 -0
- package/dist/common/serialization.d.ts +49 -0
- package/dist/common/serialization.js +155 -0
- package/dist/common/time.d.ts +40 -0
- package/dist/common/time.js +93 -0
- package/dist/context/codec.d.ts +24 -0
- package/dist/context/codec.js +28 -0
- package/dist/context/context.d.ts +45 -0
- package/dist/context/context.js +70 -0
- package/dist/context/store.d.ts +51 -0
- package/dist/context/store.js +59 -0
- package/dist/index.d.ts +17 -5
- package/dist/index.js +17 -5
- package/dist/observability/ports.d.ts +2 -2
- package/dist/services/bounded-context.d.ts +79 -0
- package/dist/services/bounded-context.js +272 -0
- package/dist/services/execution.d.ts +8 -8
- package/dist/services/execution.js +8 -6
- package/dist/services/feature.d.ts +70 -48
- package/dist/services/feature.js +57 -191
- package/dist/services/process.d.ts +60 -0
- package/dist/services/process.js +57 -0
- package/dist/testing/index.d.ts +3 -3
- package/package.json +2 -2
- package/dist/services/runtime.d.ts +0 -48
- package/dist/services/runtime.js +0 -30
package/dist/services/feature.js
CHANGED
|
@@ -1,95 +1,79 @@
|
|
|
1
|
-
import { newEntityId } from "../client/ids.js";
|
|
2
|
-
import { bindDependency } from "./execution.js";
|
|
3
|
-
import { DETACHED } from "./runtime.js";
|
|
4
1
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* `Feature` and `ApplicationService`: the two kinds of service a bounded context holds, as in
|
|
3
|
+
* sincpro_framework. Each bounded context gives them its dependencies once, in its own base:
|
|
7
4
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* // domains/project/infrastructure/bounded_context.ts
|
|
6
|
+
* export class Feature extends BaseFeature<ProjectDependencies> {}
|
|
7
|
+
* export class ApplicationService extends BaseApplicationService<ProjectDependencies, ProjectFeatures> {}
|
|
8
|
+
*
|
|
9
|
+
* // domains/project/services/issues.ts
|
|
10
|
+
* export class Issues extends Feature {
|
|
11
|
+
* close(input: dto.CommandCloseIssue) {
|
|
12
|
+
* this.session.require(); // each dependency is a property — typed
|
|
13
|
+
* return this.client.closeIssue(input);
|
|
11
14
|
* }
|
|
12
15
|
* }
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* method is traced as `<name>.<method>`, and a name that starts with `_` is not traced at all.
|
|
20
|
-
*
|
|
21
|
-
* Inside a method, `this.deps` and `this.context` are this run's own: each dependency that can
|
|
22
|
-
* be (the transport, a generated client) is bound to the execution, so its requests carry this
|
|
23
|
-
* run's trace. So a Feature keeps nothing of a request on `this`, and declares no `#private`
|
|
24
|
-
* fields — the run sees the instance through a view of its own, and `#private` refuses views;
|
|
25
|
-
* TypeScript's `private` is the same promise without that.
|
|
26
|
-
*/
|
|
27
|
-
export class Feature {
|
|
28
|
-
deps;
|
|
29
|
-
/** What this run executes with (`RuntimeOptions.context`): empty outside a run. */
|
|
30
|
-
context = NO_CONTEXT;
|
|
31
|
-
constructor(deps) {
|
|
32
|
-
this.deps = deps ?? {};
|
|
33
|
-
// A class constructor that answers another object hands THAT to `new` and to every subclass
|
|
34
|
-
// constructor after it: their fields land on the instance through the proxy.
|
|
35
|
-
return intercepted(this, { runtime: DETACHED, domain: undefined, name: undefined });
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* A use case that composes the Features of its own domain — the only difference with a
|
|
40
|
-
* Feature. Each call to one of them is a child of this run: its span under this span, the same
|
|
41
|
-
* context, the same trace on the wire.
|
|
42
|
-
*
|
|
43
|
-
* export class CloseIssue extends ApplicationService<{ preview: PreviewClose; send: SendIssue }> {
|
|
44
|
-
* async execute(input: CloseIssueInput) {
|
|
45
|
-
* const preview = await this.features.preview.execute(input);
|
|
46
|
-
* if (preview.warnings.length > 0) return { blocked: preview };
|
|
47
|
-
* return this.features.send.execute({ ...input, message: "cerrado" });
|
|
17
|
+
* // domains/project/services/close_and_notify.ts
|
|
18
|
+
* export class CloseAndNotify extends ApplicationService {
|
|
19
|
+
* async execute(input: dto.CommandCloseIssue) {
|
|
20
|
+
* await this.feature.Issues.close(input); // a child of this run, on the same trace
|
|
21
|
+
* return this.feature.Notices.tell(input);
|
|
48
22
|
* }
|
|
49
23
|
* }
|
|
50
24
|
*
|
|
51
|
-
* Context:
|
|
52
|
-
*
|
|
25
|
+
* Context: a Feature is a facade — the operations of one part of the domain, each a public
|
|
26
|
+
* method; an ApplicationService composes the Features of its own bounded context and never
|
|
27
|
+
* another ApplicationService (sincpro_framework's rule). Neither writes a span nor catches an
|
|
28
|
+
* error to report it: the bounded context runs every public method inside its own execution
|
|
29
|
+
* (`UseBoundedContext`).
|
|
30
|
+
*
|
|
31
|
+
* The dependencies are typed, not declared: `BaseFeature<ProjectDependencies>` types
|
|
32
|
+
* `this.client` and `this.session` from the bounded context's dependency type — sincpro_framework's
|
|
33
|
+
* `DependencyContextType` — and the bounded context hands them in on each run. So a service
|
|
34
|
+
* constructs with nothing, keeps no dependency on `this`, and declares no `#private` field: a run
|
|
35
|
+
* sees the instance through a view of its own (its dependencies bound to its trace), and
|
|
36
|
+
* `#private` refuses views. TypeScript's `private` is the same promise without that.
|
|
53
37
|
*/
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
super(deps);
|
|
58
|
-
for (const [name, feature] of Object.entries(features))
|
|
59
|
-
if (targetOf(feature) instanceof ApplicationService)
|
|
60
|
-
throw new Error(`${this.constructor.name}: "${name}" is an ApplicationService — an ApplicationService executes Features, never another ApplicationService`);
|
|
61
|
-
this.features = features;
|
|
62
|
-
}
|
|
38
|
+
class FeatureCore {
|
|
39
|
+
}
|
|
40
|
+
class ApplicationServiceCore extends FeatureCore {
|
|
63
41
|
}
|
|
42
|
+
/** The base of a bounded context's Features: `class Feature extends BaseFeature<Deps> {}`. */
|
|
43
|
+
export const Feature = FeatureCore;
|
|
64
44
|
/**
|
|
65
|
-
* The
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* const project = bindServices(runtime, "project", { sendIssue, closeIssue });
|
|
69
|
-
*
|
|
70
|
-
* Context: a span is named by the key it is bound with, not by the class — a production build
|
|
71
|
-
* renames classes, never the keys of an object.
|
|
45
|
+
* The base of a bounded context's ApplicationServices:
|
|
46
|
+
* `class ApplicationService extends BaseApplicationService<Deps, typeof features> {}`.
|
|
72
47
|
*/
|
|
73
|
-
export
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
48
|
+
export const ApplicationService = ApplicationServiceCore;
|
|
49
|
+
/** Whether a class is a Feature — and not an ApplicationService. */
|
|
50
|
+
export function isFeatureClass(service) {
|
|
51
|
+
return (service.prototype instanceof FeatureCore &&
|
|
52
|
+
!(service.prototype instanceof ApplicationServiceCore));
|
|
78
53
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function targetOf(feature) {
|
|
82
|
-
return TARGETS.get(feature) ?? feature;
|
|
54
|
+
export function isApplicationServiceClass(service) {
|
|
55
|
+
return service.prototype instanceof ApplicationServiceCore;
|
|
83
56
|
}
|
|
84
|
-
/**
|
|
57
|
+
/** Whether a service composes Features — an ApplicationService. */
|
|
58
|
+
export function composes(service) {
|
|
59
|
+
return service instanceof ApplicationServiceCore;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The methods a class adds over its base — what is traced — read once per class: every public
|
|
63
|
+
* method of the class and its parents up to `Feature`, but `constructor` and a name that starts
|
|
64
|
+
* with `_`.
|
|
65
|
+
*/
|
|
85
66
|
const METHODS = new WeakMap();
|
|
86
|
-
function methodsOf(
|
|
87
|
-
const prototype = Object.getPrototypeOf(
|
|
67
|
+
export function methodsOf(service) {
|
|
68
|
+
const prototype = Object.getPrototypeOf(service);
|
|
88
69
|
const known = METHODS.get(prototype);
|
|
89
70
|
if (known !== undefined)
|
|
90
71
|
return known;
|
|
91
72
|
const names = new Set();
|
|
92
|
-
for (let level = prototype; level !== null &&
|
|
73
|
+
for (let level = prototype; level !== null &&
|
|
74
|
+
level !== ApplicationServiceCore.prototype &&
|
|
75
|
+
level !== FeatureCore.prototype &&
|
|
76
|
+
level !== Object.prototype; level = Object.getPrototypeOf(level))
|
|
93
77
|
for (const name of Object.getOwnPropertyNames(level)) {
|
|
94
78
|
const descriptor = Object.getOwnPropertyDescriptor(level, name);
|
|
95
79
|
if (name !== "constructor" &&
|
|
@@ -100,121 +84,3 @@ function methodsOf(target) {
|
|
|
100
84
|
METHODS.set(prototype, names);
|
|
101
85
|
return names;
|
|
102
86
|
}
|
|
103
|
-
function intercepted(target, binding) {
|
|
104
|
-
const proxy = new Proxy(target, {
|
|
105
|
-
get(held, property) {
|
|
106
|
-
const value = Reflect.get(held, property, held);
|
|
107
|
-
if (typeof property !== "string" || typeof value !== "function")
|
|
108
|
-
return value;
|
|
109
|
-
if (!methodsOf(held).has(property))
|
|
110
|
-
return value;
|
|
111
|
-
return (...args) => run(held, property, value, args, binding);
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
TARGETS.set(proxy, target);
|
|
115
|
-
return proxy;
|
|
116
|
-
}
|
|
117
|
-
/**
|
|
118
|
-
* Whether this is the first run to report this error. A Feature that fails inside an
|
|
119
|
-
* ApplicationService throws through it: the error is reported once, by the run where it
|
|
120
|
-
* happened — its span is the one that says where — and every run around it still ends `failed`.
|
|
121
|
-
*/
|
|
122
|
-
const REPORTED = new WeakSet();
|
|
123
|
-
function firstReport(error) {
|
|
124
|
-
if (error === null || (typeof error !== "object" && typeof error !== "function"))
|
|
125
|
-
return true;
|
|
126
|
-
if (REPORTED.has(error))
|
|
127
|
-
return false;
|
|
128
|
-
REPORTED.add(error);
|
|
129
|
-
return true;
|
|
130
|
-
}
|
|
131
|
-
function quietly(work, otherwise) {
|
|
132
|
-
try {
|
|
133
|
-
return work();
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return otherwise;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
const NO_SPAN = {
|
|
140
|
-
traceId: "",
|
|
141
|
-
spanId: "",
|
|
142
|
-
sampled: false,
|
|
143
|
-
set: () => undefined,
|
|
144
|
-
end: () => undefined,
|
|
145
|
-
};
|
|
146
|
-
function run(target, method, body, args, binding) {
|
|
147
|
-
const { runtime, domain, parent } = binding;
|
|
148
|
-
const name = binding.name ?? target.constructor.name;
|
|
149
|
-
const useCase = `${domain === undefined ? "" : `${domain}/`}${name}${method === "execute" ? "" : `.${method}`}`;
|
|
150
|
-
const context = parent?.context ?? quietly(() => runtime.context(), NO_CONTEXT);
|
|
151
|
-
const composes = target instanceof ApplicationService;
|
|
152
|
-
const id = newEntityId();
|
|
153
|
-
const attributes = {
|
|
154
|
-
"sincpro.use_case": useCase,
|
|
155
|
-
"sincpro.layer": composes ? "application_service" : "feature",
|
|
156
|
-
"sincpro.execution_id": id,
|
|
157
|
-
...(domain === undefined ? {} : { "sincpro.domain": domain }),
|
|
158
|
-
};
|
|
159
|
-
const span = quietly(() => runtime.tracer.start(useCase, {
|
|
160
|
-
...(parent === undefined ? {} : { parent: parent.span }),
|
|
161
|
-
attributes,
|
|
162
|
-
}), NO_SPAN);
|
|
163
|
-
const execution = { id, useCase, domain, parent, span, context };
|
|
164
|
-
// The run's own view of the instance: its dependencies bound to this execution, its context,
|
|
165
|
-
// and — for an ApplicationService — its Features as children of this run.
|
|
166
|
-
const own = target;
|
|
167
|
-
const view = Object.create(target, {
|
|
168
|
-
deps: {
|
|
169
|
-
value: Object.fromEntries(Object.entries(own.deps ?? {}).map(([key, dependency]) => [
|
|
170
|
-
key,
|
|
171
|
-
bindDependency(dependency, execution),
|
|
172
|
-
])),
|
|
173
|
-
},
|
|
174
|
-
context: { value: context },
|
|
175
|
-
...(composes
|
|
176
|
-
? {
|
|
177
|
-
features: {
|
|
178
|
-
value: Object.fromEntries(Object.entries(own.features ?? {}).map(([key, feature]) => [
|
|
179
|
-
key,
|
|
180
|
-
intercepted(targetOf(feature), {
|
|
181
|
-
runtime,
|
|
182
|
-
domain,
|
|
183
|
-
name: key,
|
|
184
|
-
parent: execution,
|
|
185
|
-
}),
|
|
186
|
-
])),
|
|
187
|
-
},
|
|
188
|
-
}
|
|
189
|
-
: {}),
|
|
190
|
-
});
|
|
191
|
-
const finish = (failed, error) => {
|
|
192
|
-
const outcome = failed ? quietly(() => runtime.outcomeOf(error), "failed") : "ok";
|
|
193
|
-
quietly(() => span.end(outcome, error), undefined);
|
|
194
|
-
if (outcome === "failed" && firstReport(error))
|
|
195
|
-
quietly(() => runtime.reporter?.report(error, {
|
|
196
|
-
useCase,
|
|
197
|
-
traceId: span.traceId,
|
|
198
|
-
executionId: id,
|
|
199
|
-
context,
|
|
200
|
-
}), undefined);
|
|
201
|
-
};
|
|
202
|
-
let answer;
|
|
203
|
-
try {
|
|
204
|
-
answer = body.apply(view, args);
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
finish(true, error);
|
|
208
|
-
throw error;
|
|
209
|
-
}
|
|
210
|
-
if (answer instanceof Promise)
|
|
211
|
-
return answer.then((value) => {
|
|
212
|
-
finish(false);
|
|
213
|
-
return value;
|
|
214
|
-
}, (error) => {
|
|
215
|
-
finish(true, error);
|
|
216
|
-
throw error;
|
|
217
|
-
});
|
|
218
|
-
finish(false);
|
|
219
|
-
return answer;
|
|
220
|
-
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Context, type StoreSource } from "../context/context.js";
|
|
2
|
+
import { type ContextStore } from "../context/store.js";
|
|
3
|
+
import { type ErrorReporter, type Outcome, type Tracer } from "../observability/ports.js";
|
|
4
|
+
/**
|
|
5
|
+
* What every bounded context of this process runs with — set once, in `main`, the way
|
|
6
|
+
* sincpro_framework's process is: who traces, who reports, where the context lives.
|
|
7
|
+
*
|
|
8
|
+
* // entrypoints/main.tsx — the platform's pieces, built from the application's settings
|
|
9
|
+
* configureProcess({
|
|
10
|
+
* tracer: browserTracer({ endpoint: settings.otlp, service: "forge-web" }),
|
|
11
|
+
* reporter: glitchtipReporter({ dsn: settings.glitchtip }),
|
|
12
|
+
* context: memoryContexts(),
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Context: everything is optional, and nothing breaks without it. A process that never configures
|
|
16
|
+
* runs every use case untraced, unreported, with its context in memory — so a bounded context is
|
|
17
|
+
* usable the moment it is imported, and `main` adds what it watches with. The bounded contexts
|
|
18
|
+
* read the process on every call, never at import: `project` may be defined before `main` runs.
|
|
19
|
+
*
|
|
20
|
+
* It replaces everything at once: what is not given goes back to its default — a test calls it
|
|
21
|
+
* with its own tracer and starts from a clean context.
|
|
22
|
+
*/
|
|
23
|
+
export type ProcessOptions = {
|
|
24
|
+
/** The platform's (OpenTelemetry in a browser, a queue on a phone, memory in a test). */
|
|
25
|
+
tracer?: Tracer;
|
|
26
|
+
/** The platform's (GlitchTip, Sentry, memory in a test). Only what nobody expected reaches it. */
|
|
27
|
+
reporter?: ErrorReporter;
|
|
28
|
+
/** Where the context lives (`memoryContexts()` by default). */
|
|
29
|
+
context?: ContextStore;
|
|
30
|
+
/** How an error ended a use case (`outcomeOf` by default). Only `failed` is reported. */
|
|
31
|
+
outcomeOf?: (error: unknown) => Outcome;
|
|
32
|
+
};
|
|
33
|
+
/** The process as a use case reads it. Not public: `configureProcess` is the way in. */
|
|
34
|
+
export type Process = {
|
|
35
|
+
readonly tracer: Tracer;
|
|
36
|
+
readonly reporter: ErrorReporter | undefined;
|
|
37
|
+
readonly store: ContextStore;
|
|
38
|
+
readonly outcomeOf: (error: unknown) => Outcome;
|
|
39
|
+
};
|
|
40
|
+
export declare function configureProcess(options?: ProcessOptions): void;
|
|
41
|
+
/** The process's store as every level of the context reads it — the process's and each bounded context's. */
|
|
42
|
+
export declare const processStore: StoreSource;
|
|
43
|
+
/** What the process runs with now. */
|
|
44
|
+
export declare function processNow(): Process;
|
|
45
|
+
/**
|
|
46
|
+
* The process's level of the context: what every use case of every bounded context runs with —
|
|
47
|
+
* the tenant, the company, the language.
|
|
48
|
+
*
|
|
49
|
+
* processContext.set({ tenant_id: "acme" });
|
|
50
|
+
*/
|
|
51
|
+
export declare const processContext: Context;
|
|
52
|
+
/**
|
|
53
|
+
* How an error ended a use case, by default.
|
|
54
|
+
*
|
|
55
|
+
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
56
|
+
* the business answering, not a fault. Here the same: a `TransportError` the server answered
|
|
57
|
+
* with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
|
|
58
|
+
* else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported.
|
|
59
|
+
*/
|
|
60
|
+
export declare function outcomeOf(error: unknown): Outcome;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { TransportError } from "../client/transport.js";
|
|
2
|
+
import { contextAt, } from "../context/context.js";
|
|
3
|
+
import { memoryContexts } from "../context/store.js";
|
|
4
|
+
import { NO_TRACER, } from "../observability/ports.js";
|
|
5
|
+
function settled(options) {
|
|
6
|
+
return {
|
|
7
|
+
tracer: options.tracer ?? NO_TRACER,
|
|
8
|
+
reporter: options.reporter,
|
|
9
|
+
store: options.context ?? memoryContexts(),
|
|
10
|
+
outcomeOf: options.outcomeOf ?? outcomeOf,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
let current = settled({});
|
|
14
|
+
const replacing = new Set();
|
|
15
|
+
export function configureProcess(options = {}) {
|
|
16
|
+
const before = current.store;
|
|
17
|
+
current = settled(options);
|
|
18
|
+
if (current.store !== before)
|
|
19
|
+
for (const listener of [...replacing])
|
|
20
|
+
listener();
|
|
21
|
+
}
|
|
22
|
+
/** The process's store as every level of the context reads it — the process's and each bounded context's. */
|
|
23
|
+
export const processStore = {
|
|
24
|
+
current: () => current.store,
|
|
25
|
+
replaced(listener) {
|
|
26
|
+
replacing.add(listener);
|
|
27
|
+
return () => void replacing.delete(listener);
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
/** What the process runs with now. */
|
|
31
|
+
export function processNow() {
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The process's level of the context: what every use case of every bounded context runs with —
|
|
36
|
+
* the tenant, the company, the language.
|
|
37
|
+
*
|
|
38
|
+
* processContext.set({ tenant_id: "acme" });
|
|
39
|
+
*/
|
|
40
|
+
export const processContext = contextAt("process", processStore);
|
|
41
|
+
/**
|
|
42
|
+
* How an error ended a use case, by default.
|
|
43
|
+
*
|
|
44
|
+
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
45
|
+
* the business answering, not a fault. Here the same: a `TransportError` the server answered
|
|
46
|
+
* with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
|
|
47
|
+
* else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported.
|
|
48
|
+
*/
|
|
49
|
+
export function outcomeOf(error) {
|
|
50
|
+
if (error instanceof TransportError && error.status !== null)
|
|
51
|
+
return error.status >= 400 && error.status < 500 ? "refused" : "failed";
|
|
52
|
+
if (error !== null &&
|
|
53
|
+
typeof error === "object" &&
|
|
54
|
+
error.name === "AbortError")
|
|
55
|
+
return "aborted";
|
|
56
|
+
return "failed";
|
|
57
|
+
}
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import type { ErrorAbout, ErrorReporter, Outcome, Tracer } from "../observabilit
|
|
|
3
3
|
* A tracer and a reporter that keep what they were told, so a test reads it back.
|
|
4
4
|
*
|
|
5
5
|
* const tracer = memoryTracer();
|
|
6
|
-
*
|
|
7
|
-
* await project.
|
|
8
|
-
* tracer.spans.map((one) => one.name); // ["project/
|
|
6
|
+
* configureProcess({ tracer });
|
|
7
|
+
* await project.CloseIssue.execute(input);
|
|
8
|
+
* tracer.spans.map((one) => one.name); // ["project/CloseIssue", "project/Issues.close", …]
|
|
9
9
|
*
|
|
10
10
|
* Context: a use case is proven in Node with nothing of a platform; these are the platform's
|
|
11
11
|
* part, held in memory. Not test doubles of this package — real implementations of its ports.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/client-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The client side of sincpro_framework, for every platform: Feature and ApplicationService traced and reported by default, applications made of domains, and the typed client generated from a service's contract. No React, no DOM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"printWidth": 94
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@opentelemetry/api": "^1.
|
|
70
|
+
"@opentelemetry/api": "^1.9.0",
|
|
71
71
|
"@sincpro/criteria": "^2.0.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { type ErrorReporter, type Outcome, type Tracer } from "../observability/ports.js";
|
|
2
|
-
/**
|
|
3
|
-
* What every use case of an application runs with: who traces, who reports, and the context
|
|
4
|
-
* each one carries to the server.
|
|
5
|
-
*
|
|
6
|
-
* const runtime = createRuntime({
|
|
7
|
-
* tracer, // the platform's (OpenTelemetry, …)
|
|
8
|
-
* reporter, // the platform's (GlitchTip, …)
|
|
9
|
-
* context: () => ({ tenant_id: session.tenant }),
|
|
10
|
-
* });
|
|
11
|
-
* const project = bindServices(runtime, "project", { sendIssue, closeIssue });
|
|
12
|
-
* await project.closeIssue.execute({ issue_id: "acme-3" });
|
|
13
|
-
*
|
|
14
|
-
* Context: sincpro_framework's bus is what gives every use case its span, its error report and
|
|
15
|
-
* its context; here the same comes from the interceptors of `Feature`, and this is what they
|
|
16
|
-
* read. A use case that was never bound runs too — with nothing tracing and nothing reporting —
|
|
17
|
-
* so a Feature is tested by constructing it.
|
|
18
|
-
*/
|
|
19
|
-
export type RuntimeOptions = {
|
|
20
|
-
tracer?: Tracer;
|
|
21
|
-
reporter?: ErrorReporter;
|
|
22
|
-
/**
|
|
23
|
-
* What a use case runs with, read once where a gesture starts and inherited by every use case
|
|
24
|
-
* it executes. It is what services read (`this.context`) and what travels to the server in
|
|
25
|
-
* `sincpro-context` — never a secret: the token goes in the transport's headers.
|
|
26
|
-
*/
|
|
27
|
-
context?: () => Readonly<Record<string, unknown>>;
|
|
28
|
-
/** How an error ended a use case (`outcomeOf` by default). Only `failed` is reported. */
|
|
29
|
-
outcomeOf?: (error: unknown) => Outcome;
|
|
30
|
-
};
|
|
31
|
-
export type Runtime = {
|
|
32
|
-
readonly tracer: Tracer;
|
|
33
|
-
readonly reporter: ErrorReporter | undefined;
|
|
34
|
-
readonly context: () => Readonly<Record<string, unknown>>;
|
|
35
|
-
readonly outcomeOf: (error: unknown) => Outcome;
|
|
36
|
-
};
|
|
37
|
-
export declare function createRuntime(options?: RuntimeOptions): Runtime;
|
|
38
|
-
/** Where a use case that was never bound runs: nothing traces, nothing reports. */
|
|
39
|
-
export declare const DETACHED: Runtime;
|
|
40
|
-
/**
|
|
41
|
-
* How an error ended a use case, by default.
|
|
42
|
-
*
|
|
43
|
-
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
44
|
-
* the business answering, not a fault. Here the same: a `TransportError` the server answered
|
|
45
|
-
* with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
|
|
46
|
-
* else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported.
|
|
47
|
-
*/
|
|
48
|
-
export declare function outcomeOf(error: unknown): Outcome;
|
package/dist/services/runtime.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { TransportError } from "../client/transport.js";
|
|
2
|
-
import { NO_TRACER, } from "../observability/ports.js";
|
|
3
|
-
const NO_CONTEXT = Object.freeze({});
|
|
4
|
-
export function createRuntime(options = {}) {
|
|
5
|
-
return {
|
|
6
|
-
tracer: options.tracer ?? NO_TRACER,
|
|
7
|
-
reporter: options.reporter,
|
|
8
|
-
context: options.context ?? (() => NO_CONTEXT),
|
|
9
|
-
outcomeOf: options.outcomeOf ?? outcomeOf,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
/** Where a use case that was never bound runs: nothing traces, nothing reports. */
|
|
13
|
-
export const DETACHED = createRuntime();
|
|
14
|
-
/**
|
|
15
|
-
* How an error ended a use case, by default.
|
|
16
|
-
*
|
|
17
|
-
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
18
|
-
* the business answering, not a fault. Here the same: a `TransportError` the server answered
|
|
19
|
-
* with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
|
|
20
|
-
* else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported.
|
|
21
|
-
*/
|
|
22
|
-
export function outcomeOf(error) {
|
|
23
|
-
if (error instanceof TransportError && error.status !== null)
|
|
24
|
-
return error.status >= 400 && error.status < 500 ? "refused" : "failed";
|
|
25
|
-
if (error !== null &&
|
|
26
|
-
typeof error === "object" &&
|
|
27
|
-
error.name === "AbortError")
|
|
28
|
-
return "aborted";
|
|
29
|
-
return "failed";
|
|
30
|
-
}
|