@sincpro/client-framework 0.6.0 → 1.0.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 +54 -25
- package/MANIFESTO.md +59 -39
- package/README.md +324 -345
- package/dist/client/{memory.d.ts → adapters/memory.d.ts} +1 -1
- package/dist/client/{memory.js → adapters/memory.js} +2 -2
- package/dist/{adapters → client/adapters}/parquet.d.ts +1 -1
- package/dist/client/{rest.d.ts → adapters/rest.d.ts} +1 -1
- package/dist/client/{rest.js → adapters/rest.js} +2 -2
- package/dist/client/{entity.d.ts → domain/entity.d.ts} +6 -10
- package/dist/client/index.d.ts +16 -0
- package/dist/client/index.js +16 -0
- package/dist/client/services/propagation.d.ts +32 -0
- package/dist/{services/execution.js → client/services/propagation.js} +5 -21
- package/dist/client/{retry.js → services/retry.js} +1 -1
- package/dist/codegen/{metamodel.d.ts → domain/metamodel.d.ts} +17 -2
- package/dist/codegen/{names.d.ts → domain/names.d.ts} +2 -2
- package/dist/codegen/{names.js → domain/names.js} +3 -3
- package/dist/codegen/{cli.js → entrypoint/cli.js} +9 -8
- package/dist/codegen/index.d.ts +7 -7
- package/dist/codegen/index.js +5 -10
- package/dist/codegen/{client.d.ts → services/client.d.ts} +3 -1
- package/dist/codegen/{client.js → services/client.js} +9 -1
- package/dist/codegen/services/domain.d.ts +20 -0
- package/dist/codegen/{domain.js → services/domain.js} +82 -25
- package/dist/codegen/{entities.d.ts → services/entities.d.ts} +4 -3
- package/dist/codegen/{entities.js → services/entities.js} +18 -4
- package/dist/codegen/{rest.d.ts → services/rest.d.ts} +1 -1
- package/dist/codegen/{scaffold.d.ts → services/scaffold.d.ts} +2 -2
- package/dist/codegen/{scaffold.js → services/scaffold.js} +31 -36
- package/dist/common/index.d.ts +10 -0
- package/dist/common/index.js +10 -0
- package/dist/context/adapters/memory.d.ts +6 -0
- package/dist/context/adapters/memory.js +34 -0
- package/dist/context/adapters/plain-codec.d.ts +3 -0
- package/dist/context/adapters/plain-codec.js +16 -0
- package/dist/context/{codec.d.ts → domain/codec.d.ts} +0 -1
- package/dist/context/{codec.js → domain/codec.js} +0 -14
- package/dist/context/domain/execution.d.ts +33 -0
- package/dist/context/domain/execution.js +11 -0
- package/dist/context/{store.d.ts → domain/store.d.ts} +0 -5
- package/dist/context/{store.js → domain/store.js} +1 -34
- package/dist/context/{context.d.ts → entrypoint/context.d.ts} +1 -1
- package/dist/context/index.d.ts +17 -0
- package/dist/context/index.js +15 -0
- package/dist/context/infrastructure/process.d.ts +13 -0
- package/dist/context/infrastructure/process.js +35 -0
- package/dist/core/domain/bus.d.ts +18 -0
- package/dist/core/domain/bus.js +18 -0
- package/dist/core/domain/dto.d.ts +32 -0
- package/dist/core/domain/dto.js +20 -0
- package/dist/core/domain/errors.d.ts +18 -0
- package/dist/core/domain/errors.js +1 -0
- package/dist/core/domain/services.d.ts +37 -0
- package/dist/core/domain/services.js +28 -0
- package/dist/core/entrypoint/bounded-context.d.ts +70 -0
- package/dist/core/entrypoint/bounded-context.js +252 -0
- package/dist/core/entrypoint/screens.d.ts +6 -0
- package/dist/core/entrypoint/screens.js +136 -0
- package/dist/core/index.d.ts +17 -0
- package/dist/core/index.js +16 -0
- package/dist/core/infrastructure/process.d.ts +56 -0
- package/dist/core/infrastructure/process.js +35 -0
- package/dist/core/services/binding.d.ts +6 -0
- package/dist/core/services/binding.js +13 -0
- package/dist/core/services/error-handlers.d.ts +11 -0
- package/dist/core/services/error-handlers.js +21 -0
- package/dist/core/services/execution.d.ts +35 -0
- package/dist/core/services/execution.js +137 -0
- package/dist/index.d.ts +16 -38
- package/dist/index.js +16 -38
- package/dist/model/index.d.ts +10 -0
- package/dist/model/index.js +10 -0
- package/dist/model/{metas.d.ts → infrastructure/metas.d.ts} +1 -1
- package/dist/model/{schema.d.ts → services/schema.d.ts} +1 -1
- package/dist/model/{schema.js → services/schema.js} +1 -1
- package/dist/observability/adapters/console.d.ts +12 -0
- package/dist/observability/adapters/console.js +35 -0
- package/dist/observability/adapters/memory.d.ts +41 -0
- package/dist/observability/adapters/memory.js +49 -0
- package/dist/{adapters → observability/adapters}/opentelemetry.d.ts +1 -1
- package/dist/{adapters → observability/adapters}/sentry.d.ts +1 -1
- package/dist/observability/domain/errors.d.ts +23 -0
- package/dist/observability/domain/errors.js +8 -0
- package/dist/observability/domain/logging.d.ts +42 -0
- package/dist/observability/domain/logging.js +1 -0
- package/dist/observability/domain/tracing.d.ts +49 -0
- package/dist/observability/domain/tracing.js +20 -0
- package/dist/observability/index.d.ts +16 -0
- package/dist/observability/index.js +4 -0
- package/dist/observability/infrastructure/registry.d.ts +21 -0
- package/dist/observability/infrastructure/registry.js +18 -0
- package/dist/observability/services/logging.d.ts +27 -0
- package/dist/observability/services/logging.js +105 -0
- package/dist/observability/services/outcome.d.ts +12 -0
- package/dist/observability/services/outcome.js +20 -0
- package/dist/{services/report.js → observability/services/reporting.js} +4 -3
- package/dist/testing/index.d.ts +3 -33
- package/dist/testing/index.js +5 -37
- package/package.json +16 -8
- package/dist/codegen/domain.d.ts +0 -15
- package/dist/observability/ports.d.ts +0 -65
- package/dist/observability/ports.js +0 -21
- package/dist/services/bounded-context.d.ts +0 -79
- package/dist/services/bounded-context.js +0 -261
- package/dist/services/execution.d.ts +0 -63
- package/dist/services/feature.d.ts +0 -85
- package/dist/services/feature.js +0 -86
- package/dist/services/process.d.ts +0 -60
- package/dist/services/process.js +0 -57
- package/dist/services/screens.d.ts +0 -2
- package/dist/services/screens.js +0 -76
- /package/dist/{adapters → client/adapters}/parquet.js +0 -0
- /package/dist/client/{contract.d.ts → domain/contract.d.ts} +0 -0
- /package/dist/client/{contract.js → domain/contract.js} +0 -0
- /package/dist/client/{entity.js → domain/entity.js} +0 -0
- /package/dist/client/{transport.d.ts → domain/transport.d.ts} +0 -0
- /package/dist/client/{transport.js → domain/transport.js} +0 -0
- /package/dist/client/{discovery.d.ts → services/discovery.d.ts} +0 -0
- /package/dist/client/{discovery.js → services/discovery.js} +0 -0
- /package/dist/client/{retry.d.ts → services/retry.d.ts} +0 -0
- /package/dist/codegen/{metamodel.js → domain/metamodel.js} +0 -0
- /package/dist/codegen/{cli.d.ts → entrypoint/cli.d.ts} +0 -0
- /package/dist/codegen/{check.d.ts → services/check.d.ts} +0 -0
- /package/dist/codegen/{check.js → services/check.js} +0 -0
- /package/dist/codegen/{rest.js → services/rest.js} +0 -0
- /package/dist/context/{context.js → entrypoint/context.js} +0 -0
- /package/dist/model/{meta.d.ts → domain/meta.d.ts} +0 -0
- /package/dist/model/{meta.js → domain/meta.js} +0 -0
- /package/dist/model/{metas.js → infrastructure/metas.js} +0 -0
- /package/dist/{adapters → observability/adapters}/opentelemetry.js +0 -0
- /package/dist/{adapters → observability/adapters}/sentry.js +0 -0
- /package/dist/{services/report.d.ts → observability/services/reporting.d.ts} +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a platform plugs for execution traces: one span per use case.
|
|
3
|
+
*
|
|
4
|
+
* Context: sincpro_framework opens one span per use case; a client has to do the same, and
|
|
5
|
+
* continue the backend's trace. The vendor — OpenTelemetry in a browser, a queue on a phone,
|
|
6
|
+
* memory in a test — is the platform's, so this package defines the contract and calls it, and
|
|
7
|
+
* bundles none. Two implementations at least (a platform's and a test's) are why it is a port.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here may break a use case: a tracer that fails is ignored, and the use case answers as
|
|
10
|
+
* it would have.
|
|
11
|
+
*/
|
|
12
|
+
/** What is said about a span: plain values only, the way OpenTelemetry takes them. */
|
|
13
|
+
export type Attributes = Readonly<Record<string, string | number | boolean>>;
|
|
14
|
+
/**
|
|
15
|
+
* How a use case ended.
|
|
16
|
+
*
|
|
17
|
+
* - `ok`: it answered.
|
|
18
|
+
* - `refused`: expected — the server said no and explained why (a validation, a conflict, a
|
|
19
|
+
* permission), or the bounded context declared the error expected (`expectErrors`). Never
|
|
20
|
+
* reported.
|
|
21
|
+
* - `aborted`: whoever asked stopped waiting (a screen that closed). Never reported.
|
|
22
|
+
* - `failed`: anything else — a bug, a server that broke, a server nobody could reach. Reported.
|
|
23
|
+
*/
|
|
24
|
+
export type Outcome = "ok" | "refused" | "aborted" | "failed";
|
|
25
|
+
/** One span: a use case, open while it runs. */
|
|
26
|
+
export interface Span {
|
|
27
|
+
/** 32 hex — the id the server continues and a person quotes as a support code. Empty when
|
|
28
|
+
* nothing traces. */
|
|
29
|
+
readonly traceId: string;
|
|
30
|
+
/** 16 hex. Empty when nothing traces. */
|
|
31
|
+
readonly spanId: string;
|
|
32
|
+
/** Whether this trace is kept: the server follows the same decision. */
|
|
33
|
+
readonly sampled: boolean;
|
|
34
|
+
/** Vendor state that travels beside the trace (`tracestate`), when there is any. */
|
|
35
|
+
readonly state?: string;
|
|
36
|
+
/** Says more about the use case while it runs. */
|
|
37
|
+
set(attributes: Attributes): void;
|
|
38
|
+
/** Closes it, once. `error` is what it failed or was refused with. */
|
|
39
|
+
end(outcome: Outcome, error?: unknown): void;
|
|
40
|
+
}
|
|
41
|
+
/** Opens spans. One per platform; `NO_TRACER` when nothing traces. */
|
|
42
|
+
export interface Tracer {
|
|
43
|
+
start(name: string, options: {
|
|
44
|
+
parent?: Span;
|
|
45
|
+
attributes?: Attributes;
|
|
46
|
+
}): Span;
|
|
47
|
+
}
|
|
48
|
+
/** Traces nothing: what a use case runs with until a platform plugs a tracer. */
|
|
49
|
+
export declare const NO_TRACER: Tracer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a platform plugs for execution traces: one span per use case.
|
|
3
|
+
*
|
|
4
|
+
* Context: sincpro_framework opens one span per use case; a client has to do the same, and
|
|
5
|
+
* continue the backend's trace. The vendor — OpenTelemetry in a browser, a queue on a phone,
|
|
6
|
+
* memory in a test — is the platform's, so this package defines the contract and calls it, and
|
|
7
|
+
* bundles none. Two implementations at least (a platform's and a test's) are why it is a port.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here may break a use case: a tracer that fails is ignored, and the use case answers as
|
|
10
|
+
* it would have.
|
|
11
|
+
*/
|
|
12
|
+
const EMPTY_SPAN = {
|
|
13
|
+
traceId: "",
|
|
14
|
+
spanId: "",
|
|
15
|
+
sampled: false,
|
|
16
|
+
set: () => undefined,
|
|
17
|
+
end: () => undefined,
|
|
18
|
+
};
|
|
19
|
+
/** Traces nothing: what a use case runs with until a platform plugs a tracer. */
|
|
20
|
+
export const NO_TRACER = { start: () => EMPTY_SPAN };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Observability: the ports a platform plugs (`Tracer`, `ErrorReporter`, `Logger`), the console
|
|
3
|
+
* engine logs go to by default, and how code writes and reports.
|
|
4
|
+
*
|
|
5
|
+
* domain/ the ports: tracing, errors, logging
|
|
6
|
+
* adapters/ console (default), memory (`./testing`), OpenTelemetry and Sentry (optional
|
|
7
|
+
* entries `./opentelemetry`, `./sentry`)
|
|
8
|
+
* infrastructure/ the ports this process runs with (`configureProcess` plugs them)
|
|
9
|
+
* services/ the loggers, `reportError`, the default `outcomeOf`
|
|
10
|
+
*/
|
|
11
|
+
export type { ErrorAbout, ErrorReporter, } from "./domain/errors.js";
|
|
12
|
+
export type { LayerLogger, Logger, LogLayer, LogLevel, LogRecord, } from "./domain/logging.js";
|
|
13
|
+
export { type Attributes, NO_TRACER, type Outcome, type Span, type Tracer, } from "./domain/tracing.js";
|
|
14
|
+
export { combineLoggers, logger, loggerAdapter, } from "./services/logging.js";
|
|
15
|
+
export { outcomeOf } from "./services/outcome.js";
|
|
16
|
+
export { reportError } from "./services/reporting.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ErrorReporter } from "../domain/errors.js";
|
|
2
|
+
import type { Logger } from "../domain/logging.js";
|
|
3
|
+
import { type Outcome, type Tracer } from "../domain/tracing.js";
|
|
4
|
+
/**
|
|
5
|
+
* The observability this process runs with: who traces, who reports, where logs go and how an
|
|
6
|
+
* error ended a use case — the ports `configureProcess` plugged, read on every call.
|
|
7
|
+
*
|
|
8
|
+
* Context: sincpro_framework keeps the same state in its `observability_registry`, outside the
|
|
9
|
+
* buses, so a test resets everything with one call. Until a process configures, nothing traces,
|
|
10
|
+
* nothing reports, and logs go to the console in development mode.
|
|
11
|
+
*/
|
|
12
|
+
export type ObservabilityPorts = {
|
|
13
|
+
readonly tracer: Tracer;
|
|
14
|
+
readonly reporter: ErrorReporter | undefined;
|
|
15
|
+
readonly logger: Logger | undefined;
|
|
16
|
+
readonly outcomeOf: (error: unknown) => Outcome;
|
|
17
|
+
};
|
|
18
|
+
/** What the process observes with now. */
|
|
19
|
+
export declare function observabilityNow(): ObservabilityPorts;
|
|
20
|
+
/** @internal — `configureProcess` and `configureLogger` are the way in. */
|
|
21
|
+
export declare function replaceObservability(ports: Partial<ObservabilityPorts>): void;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { consoleLogger } from "../adapters/console.js";
|
|
2
|
+
import { NO_TRACER, } from "../domain/tracing.js";
|
|
3
|
+
import { outcomeOf } from "../services/outcome.js";
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
tracer: NO_TRACER,
|
|
6
|
+
reporter: undefined,
|
|
7
|
+
logger: consoleLogger({ mode: "development" }),
|
|
8
|
+
outcomeOf,
|
|
9
|
+
};
|
|
10
|
+
let current = DEFAULTS;
|
|
11
|
+
/** What the process observes with now. */
|
|
12
|
+
export function observabilityNow() {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
/** @internal — `configureProcess` and `configureLogger` are the way in. */
|
|
16
|
+
export function replaceObservability(ports) {
|
|
17
|
+
current = { ...current, ...ports };
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type Execution } from "../../context/domain/execution.js";
|
|
2
|
+
import type { LayerLogger, Logger, LogLayer } from "../domain/logging.js";
|
|
3
|
+
import type { Outcome } from "../domain/tracing.js";
|
|
4
|
+
/** The infrastructure's logger — the global one: the code that starts and holds the application. */
|
|
5
|
+
export declare const logger: LayerLogger;
|
|
6
|
+
/** The adapters' logger: what talks to a printer, a database, another system. */
|
|
7
|
+
export declare const loggerAdapter: LayerLogger;
|
|
8
|
+
/** @internal — a bounded context's own logger (`project.logger`). */
|
|
9
|
+
export declare function boundedContextLogger(name: string): LayerLogger;
|
|
10
|
+
/** @internal — what a Feature or ApplicationService finds as `this.logger` while it runs. */
|
|
11
|
+
export declare function executionLogger(layer: "feature" | "application_service", execution: Execution): LayerLogger;
|
|
12
|
+
/** Each destination keeps its own policy; one failing sink does not hold back the others. */
|
|
13
|
+
export declare function combineLoggers(...loggers: readonly Logger[]): Logger;
|
|
14
|
+
/** @internal — lifecycle records contain correlation and outcome, never business payloads. */
|
|
15
|
+
export declare function logExecution(layer: "feature" | "application_service", execution: Execution, completed?: {
|
|
16
|
+
outcome: Outcome;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
}): void;
|
|
19
|
+
/**
|
|
20
|
+
* @internal — an error a handler of the bounded context answered instead of raising: said once,
|
|
21
|
+
* with where it was handled, never with the error itself.
|
|
22
|
+
*/
|
|
23
|
+
export declare function logHandled(layer: LogLayer, correlation: {
|
|
24
|
+
execution: Execution;
|
|
25
|
+
} | {
|
|
26
|
+
boundedContext: string;
|
|
27
|
+
}): void;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { BIND } from "../../context/domain/execution.js";
|
|
2
|
+
import { observabilityNow } from "../infrastructure/registry.js";
|
|
3
|
+
const processSink = () => observabilityNow().logger;
|
|
4
|
+
/** Sink failures cannot replace a business outcome or cause unhandled async rejections. */
|
|
5
|
+
function deliver(logger, record) {
|
|
6
|
+
if (!logger)
|
|
7
|
+
return;
|
|
8
|
+
try {
|
|
9
|
+
const pending = logger.log(record);
|
|
10
|
+
if (pending)
|
|
11
|
+
void Promise.resolve(pending).catch(() => { });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Logging adapters are best-effort, as are tracing and error-reporting adapters.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function correlationOf(execution) {
|
|
18
|
+
return {
|
|
19
|
+
boundedContext: execution.boundedContext,
|
|
20
|
+
useCase: execution.useCase,
|
|
21
|
+
executionId: execution.id,
|
|
22
|
+
...(execution.span.traceId ? { traceId: execution.span.traceId } : {}),
|
|
23
|
+
...(execution.span.spanId ? { spanId: execution.span.spanId } : {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function layerLogger(layer, sink, correlation = {}) {
|
|
27
|
+
const write = (level, message, attributes) => {
|
|
28
|
+
deliver(sink(), {
|
|
29
|
+
timestamp: new Date().toISOString(),
|
|
30
|
+
level,
|
|
31
|
+
layer,
|
|
32
|
+
message,
|
|
33
|
+
...(attributes ? { attributes: { ...attributes } } : {}),
|
|
34
|
+
...correlation,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const logger = {
|
|
38
|
+
debug: (message, attributes) => write("debug", message, attributes),
|
|
39
|
+
info: (message, attributes) => write("info", message, attributes),
|
|
40
|
+
warn: (message, attributes) => write("warn", message, attributes),
|
|
41
|
+
error: (message, attributes) => write("error", message, attributes),
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(logger, BIND, {
|
|
44
|
+
value: (run) => layerLogger(layer, sink, correlationOf(run)),
|
|
45
|
+
enumerable: false,
|
|
46
|
+
});
|
|
47
|
+
return logger;
|
|
48
|
+
}
|
|
49
|
+
/** The infrastructure's logger — the global one: the code that starts and holds the application. */
|
|
50
|
+
export const logger = layerLogger("infrastructure", processSink);
|
|
51
|
+
/** The adapters' logger: what talks to a printer, a database, another system. */
|
|
52
|
+
export const loggerAdapter = layerLogger("adapter", processSink);
|
|
53
|
+
/** @internal — a bounded context's own logger (`project.logger`). */
|
|
54
|
+
export function boundedContextLogger(name) {
|
|
55
|
+
return layerLogger("bounded_context", processSink, { boundedContext: name });
|
|
56
|
+
}
|
|
57
|
+
/** @internal — what a Feature or ApplicationService finds as `this.logger` while it runs. */
|
|
58
|
+
export function executionLogger(layer, execution) {
|
|
59
|
+
return layerLogger(layer, processSink, correlationOf(execution));
|
|
60
|
+
}
|
|
61
|
+
/** Each destination keeps its own policy; one failing sink does not hold back the others. */
|
|
62
|
+
export function combineLoggers(...loggers) {
|
|
63
|
+
return {
|
|
64
|
+
log: (record) => {
|
|
65
|
+
for (const logger of loggers)
|
|
66
|
+
deliver(logger, record);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** @internal — lifecycle records contain correlation and outcome, never business payloads. */
|
|
71
|
+
export function logExecution(layer, execution, completed) {
|
|
72
|
+
const outcome = completed?.outcome;
|
|
73
|
+
const level = outcome === "failed"
|
|
74
|
+
? "error"
|
|
75
|
+
: outcome === "refused"
|
|
76
|
+
? "warn"
|
|
77
|
+
: completed
|
|
78
|
+
? "info"
|
|
79
|
+
: "debug";
|
|
80
|
+
deliver(processSink(), {
|
|
81
|
+
timestamp: new Date().toISOString(),
|
|
82
|
+
level,
|
|
83
|
+
layer,
|
|
84
|
+
event: completed ? "execution.finished" : "execution.started",
|
|
85
|
+
message: completed ? `Execution ${completed.outcome}` : "Execution started",
|
|
86
|
+
...correlationOf(execution),
|
|
87
|
+
...(completed ? completed : {}),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* @internal — an error a handler of the bounded context answered instead of raising: said once,
|
|
92
|
+
* with where it was handled, never with the error itself.
|
|
93
|
+
*/
|
|
94
|
+
export function logHandled(layer, correlation) {
|
|
95
|
+
deliver(processSink(), {
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
level: "info",
|
|
98
|
+
layer,
|
|
99
|
+
event: "error.handled",
|
|
100
|
+
message: "Error handled",
|
|
101
|
+
...("execution" in correlation
|
|
102
|
+
? correlationOf(correlation.execution)
|
|
103
|
+
: { boundedContext: correlation.boundedContext }),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Outcome } from "../domain/tracing.js";
|
|
2
|
+
/**
|
|
3
|
+
* How an error ended a use case, by default.
|
|
4
|
+
*
|
|
5
|
+
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
6
|
+
* the business answering, not a fault. Here the same: a `TransportError` the server answered
|
|
7
|
+
* with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
|
|
8
|
+
* else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported. A bounded
|
|
9
|
+
* context adds its own expected errors (`expectErrors`); a process replaces this whole
|
|
10
|
+
* (`configureProcess({ outcomeOf })`).
|
|
11
|
+
*/
|
|
12
|
+
export declare function outcomeOf(error: unknown): Outcome;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { TransportError } from "../../client/domain/transport.js";
|
|
2
|
+
/**
|
|
3
|
+
* How an error ended a use case, by default.
|
|
4
|
+
*
|
|
5
|
+
* Context: the backend reports what nobody expected and only that — a refusal it explained is
|
|
6
|
+
* the business answering, not a fault. Here the same: a `TransportError` the server answered
|
|
7
|
+
* with a 4xx is `refused`; a request the caller stopped waiting for is `aborted`; everything
|
|
8
|
+
* else — a bug, a 5xx, a server nobody could reach — is `failed`, and reported. A bounded
|
|
9
|
+
* context adds its own expected errors (`expectErrors`); a process replaces this whole
|
|
10
|
+
* (`configureProcess({ outcomeOf })`).
|
|
11
|
+
*/
|
|
12
|
+
export function outcomeOf(error) {
|
|
13
|
+
if (error instanceof TransportError && error.status !== null)
|
|
14
|
+
return error.status >= 400 && error.status < 500 ? "refused" : "failed";
|
|
15
|
+
if (error !== null &&
|
|
16
|
+
typeof error === "object" &&
|
|
17
|
+
error.name === "AbortError")
|
|
18
|
+
return "aborted";
|
|
19
|
+
return "failed";
|
|
20
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { newEntityId } from "
|
|
2
|
-
import { processContext
|
|
1
|
+
import { newEntityId } from "../../common/ids.js";
|
|
2
|
+
import { processContext } from "../../context/infrastructure/process.js";
|
|
3
|
+
import { observabilityNow } from "../infrastructure/registry.js";
|
|
3
4
|
/**
|
|
4
5
|
* Whether this is the first time this error is reported. A Feature that fails inside an
|
|
5
6
|
* ApplicationService throws through it, and a screen that draws the failure may break on it:
|
|
@@ -28,7 +29,7 @@ export function firstReport(error) {
|
|
|
28
29
|
* ignored: telling about a failure never becomes a second one.
|
|
29
30
|
*/
|
|
30
31
|
export function reportError(error, where) {
|
|
31
|
-
const settings =
|
|
32
|
+
const settings = observabilityNow();
|
|
32
33
|
try {
|
|
33
34
|
// What the server explained, or what the caller stopped waiting for, is not a fault here either.
|
|
34
35
|
if (settings.outcomeOf(error) !== "failed")
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,35 +1,5 @@
|
|
|
1
|
-
import type { ErrorAbout, ErrorReporter, Outcome, Tracer } from "../observability/ports.js";
|
|
2
1
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* const tracer = memoryTracer();
|
|
6
|
-
* configureProcess({ tracer });
|
|
7
|
-
* await project.CloseIssue.execute(input);
|
|
8
|
-
* tracer.spans.map((one) => one.name); // ["project/CloseIssue", "project/Issues.close", …]
|
|
9
|
-
*
|
|
10
|
-
* Context: a use case is proven in Node with nothing of a platform; these are the platform's
|
|
11
|
-
* part, held in memory. Not test doubles of this package — real implementations of its ports.
|
|
2
|
+
* `@sincpro/client-framework/testing`: the observability ports in memory, so a use case is
|
|
3
|
+
* proven in Node with nothing of a platform. Not part of what an application runs.
|
|
12
4
|
*/
|
|
13
|
-
|
|
14
|
-
export type RecordedSpan = {
|
|
15
|
-
name: string;
|
|
16
|
-
traceId: string;
|
|
17
|
-
spanId: string;
|
|
18
|
-
/** The span it was opened under, when it was. */
|
|
19
|
-
parentSpanId: string | undefined;
|
|
20
|
-
attributes: Record<string, string | number | boolean>;
|
|
21
|
-
/** How it ended; `undefined` while it is open. */
|
|
22
|
-
outcome: Outcome | undefined;
|
|
23
|
-
error: unknown;
|
|
24
|
-
};
|
|
25
|
-
export type MemoryTracer = Tracer & {
|
|
26
|
-
readonly spans: RecordedSpan[];
|
|
27
|
-
};
|
|
28
|
-
export declare function memoryTracer(): MemoryTracer;
|
|
29
|
-
export type MemoryReporter = ErrorReporter & {
|
|
30
|
-
readonly reports: {
|
|
31
|
-
error: unknown;
|
|
32
|
-
about: ErrorAbout;
|
|
33
|
-
}[];
|
|
34
|
-
};
|
|
35
|
-
export declare function memoryReporter(): MemoryReporter;
|
|
5
|
+
export { type MemoryLogger, memoryLogger, type MemoryReporter, memoryReporter, type MemoryTracer, memoryTracer, type RecordedSpan, } from "../observability/adapters/memory.js";
|
package/dist/testing/index.js
CHANGED
|
@@ -1,37 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return {
|
|
7
|
-
spans,
|
|
8
|
-
start(name, options) {
|
|
9
|
-
const recorded = {
|
|
10
|
-
name,
|
|
11
|
-
traceId: options.parent?.traceId || hex(16),
|
|
12
|
-
spanId: hex(8),
|
|
13
|
-
parentSpanId: options.parent?.spanId || undefined,
|
|
14
|
-
attributes: { ...options.attributes },
|
|
15
|
-
outcome: undefined,
|
|
16
|
-
error: undefined,
|
|
17
|
-
};
|
|
18
|
-
spans.push(recorded);
|
|
19
|
-
return {
|
|
20
|
-
traceId: recorded.traceId,
|
|
21
|
-
spanId: recorded.spanId,
|
|
22
|
-
sampled: true,
|
|
23
|
-
set: (attributes) => void Object.assign(recorded.attributes, attributes),
|
|
24
|
-
end: (outcome, error) => {
|
|
25
|
-
if (recorded.outcome !== undefined)
|
|
26
|
-
return;
|
|
27
|
-
recorded.outcome = outcome;
|
|
28
|
-
recorded.error = error;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
export function memoryReporter() {
|
|
35
|
-
const reports = [];
|
|
36
|
-
return { reports, report: (error, about) => void reports.push({ error, about }) };
|
|
37
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `@sincpro/client-framework/testing`: the observability ports in memory, so a use case is
|
|
3
|
+
* proven in Node with nothing of a platform. Not part of what an application runs.
|
|
4
|
+
*/
|
|
5
|
+
export { memoryLogger, memoryReporter, memoryTracer, } from "../observability/adapters/memory.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/client-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./core": {
|
|
14
|
+
"types": "./dist/core/index.d.ts",
|
|
15
|
+
"default": "./dist/core/index.js"
|
|
16
|
+
},
|
|
13
17
|
"./codegen": {
|
|
14
18
|
"types": "./dist/codegen/index.d.ts",
|
|
15
19
|
"default": "./dist/codegen/index.js"
|
|
@@ -18,21 +22,25 @@
|
|
|
18
22
|
"types": "./dist/testing/index.d.ts",
|
|
19
23
|
"default": "./dist/testing/index.js"
|
|
20
24
|
},
|
|
25
|
+
"./console": {
|
|
26
|
+
"types": "./dist/observability/adapters/console.d.ts",
|
|
27
|
+
"default": "./dist/observability/adapters/console.js"
|
|
28
|
+
},
|
|
21
29
|
"./opentelemetry": {
|
|
22
|
-
"types": "./dist/adapters/opentelemetry.d.ts",
|
|
23
|
-
"default": "./dist/adapters/opentelemetry.js"
|
|
30
|
+
"types": "./dist/observability/adapters/opentelemetry.d.ts",
|
|
31
|
+
"default": "./dist/observability/adapters/opentelemetry.js"
|
|
24
32
|
},
|
|
25
33
|
"./sentry": {
|
|
26
|
-
"types": "./dist/adapters/sentry.d.ts",
|
|
27
|
-
"default": "./dist/adapters/sentry.js"
|
|
34
|
+
"types": "./dist/observability/adapters/sentry.d.ts",
|
|
35
|
+
"default": "./dist/observability/adapters/sentry.js"
|
|
28
36
|
},
|
|
29
37
|
"./parquet": {
|
|
30
|
-
"types": "./dist/adapters/parquet.d.ts",
|
|
31
|
-
"default": "./dist/adapters/parquet.js"
|
|
38
|
+
"types": "./dist/client/adapters/parquet.d.ts",
|
|
39
|
+
"default": "./dist/client/adapters/parquet.js"
|
|
32
40
|
}
|
|
33
41
|
},
|
|
34
42
|
"bin": {
|
|
35
|
-
"sincpro-client": "./dist/codegen/cli.js"
|
|
43
|
+
"sincpro-client": "./dist/codegen/entrypoint/cli.js"
|
|
36
44
|
},
|
|
37
45
|
"files": [
|
|
38
46
|
"dist",
|
package/dist/codegen/domain.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { Metamodel } from "./metamodel.js";
|
|
2
|
-
/**
|
|
3
|
-
* The TypeScript of every DTO of `metamodel`, as the text of one file.
|
|
4
|
-
*
|
|
5
|
-
* 1. The models, by name: an entity's contract, a related model by its own name.
|
|
6
|
-
* 2. Each operation, by its local name (its identity without `<context>.`): its input as
|
|
7
|
-
* `<Name>` and its answer by the backend's own name for it (`answerOf`: `ResponseX`), with
|
|
8
|
-
* `<Name>Answer` kept as a deprecated alias — or as `<Name>Answer` when it has no name.
|
|
9
|
-
* 3. The shared definitions those reach — and the ones those reach, until none is new —, but
|
|
10
|
-
* never one a package of the stack already types (`Criteria`).
|
|
11
|
-
* Final: a header that says where it came from and not to edit it, the imports, then models,
|
|
12
|
-
* definitions and operations, each sorted by name; two of them with one TypeScript name are
|
|
13
|
-
* refused, naming both.
|
|
14
|
-
*/
|
|
15
|
-
export declare function domainFileOf(metamodel: Metamodel): string;
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What a platform plugs so every use case is traced and every unexpected error reported.
|
|
3
|
-
*
|
|
4
|
-
* Context: sincpro_framework opens one span per use case and reports what nobody expected; a
|
|
5
|
-
* client has to do the same, and continue the backend's trace. The vendor — OpenTelemetry in a
|
|
6
|
-
* browser, a queue on a phone, GlitchTip, memory in a test — is the platform's, so this package
|
|
7
|
-
* defines the contract and calls it, and bundles none. Two implementations at least (a
|
|
8
|
-
* platform's and a test's) are why these are ports.
|
|
9
|
-
*
|
|
10
|
-
* Nothing here may break a use case: a tracer or a reporter that throws is ignored, and the use
|
|
11
|
-
* case answers as it would have.
|
|
12
|
-
*/
|
|
13
|
-
/** What is said about a span: plain values only, the way OpenTelemetry takes them. */
|
|
14
|
-
export type Attributes = Readonly<Record<string, string | number | boolean>>;
|
|
15
|
-
/**
|
|
16
|
-
* How a use case ended.
|
|
17
|
-
*
|
|
18
|
-
* - `ok`: it answered.
|
|
19
|
-
* - `refused`: the server said no and explained why — a validation, a conflict, a permission.
|
|
20
|
-
* Expected; never reported.
|
|
21
|
-
* - `aborted`: whoever asked stopped waiting (a screen that closed). Never reported.
|
|
22
|
-
* - `failed`: anything else — a bug, a server that broke, a server nobody could reach. Reported.
|
|
23
|
-
*/
|
|
24
|
-
export type Outcome = "ok" | "refused" | "aborted" | "failed";
|
|
25
|
-
/** One span: a use case, open while it runs. */
|
|
26
|
-
export interface Span {
|
|
27
|
-
/** 32 hex — the id the server continues and a person quotes as a support code. Empty when
|
|
28
|
-
* nothing traces. */
|
|
29
|
-
readonly traceId: string;
|
|
30
|
-
/** 16 hex. Empty when nothing traces. */
|
|
31
|
-
readonly spanId: string;
|
|
32
|
-
/** Whether this trace is kept: the server follows the same decision. */
|
|
33
|
-
readonly sampled: boolean;
|
|
34
|
-
/** Vendor state that travels beside the trace (`tracestate`), when there is any. */
|
|
35
|
-
readonly state?: string;
|
|
36
|
-
/** Says more about the use case while it runs. */
|
|
37
|
-
set(attributes: Attributes): void;
|
|
38
|
-
/** Closes it, once. `error` is what it failed or was refused with. */
|
|
39
|
-
end(outcome: Outcome, error?: unknown): void;
|
|
40
|
-
}
|
|
41
|
-
/** Opens spans. One per platform; `NO_TRACER` when nothing traces. */
|
|
42
|
-
export interface Tracer {
|
|
43
|
-
start(name: string, options: {
|
|
44
|
-
parent?: Span;
|
|
45
|
-
attributes?: Attributes;
|
|
46
|
-
}): Span;
|
|
47
|
-
}
|
|
48
|
-
/** What a report says about where the error happened. */
|
|
49
|
-
export type ErrorAbout = {
|
|
50
|
-
/** `<bounded context>.<Service>[.<method>]`: the use case's identity, as the backend's — or,
|
|
51
|
-
* for what failed outside one (`reportError`), where it happened (`view project:issue/acme-3`). */
|
|
52
|
-
useCase: string;
|
|
53
|
-
/** The trace the error belongs to — the same id the server logged, when it got that far. */
|
|
54
|
-
traceId: string;
|
|
55
|
-
/** The execution's own id. */
|
|
56
|
-
executionId: string;
|
|
57
|
-
/** What the use case ran with: the process's context, its bounded context's, its scope's. */
|
|
58
|
-
context: Readonly<Record<string, unknown>>;
|
|
59
|
-
};
|
|
60
|
-
/** Reports what nobody expected. One per platform; none when nothing reports. */
|
|
61
|
-
export interface ErrorReporter {
|
|
62
|
-
report(error: unknown, about: ErrorAbout): void;
|
|
63
|
-
}
|
|
64
|
-
/** Traces nothing: what a use case runs with until a platform plugs a tracer. */
|
|
65
|
-
export declare const NO_TRACER: Tracer;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What a platform plugs so every use case is traced and every unexpected error reported.
|
|
3
|
-
*
|
|
4
|
-
* Context: sincpro_framework opens one span per use case and reports what nobody expected; a
|
|
5
|
-
* client has to do the same, and continue the backend's trace. The vendor — OpenTelemetry in a
|
|
6
|
-
* browser, a queue on a phone, GlitchTip, memory in a test — is the platform's, so this package
|
|
7
|
-
* defines the contract and calls it, and bundles none. Two implementations at least (a
|
|
8
|
-
* platform's and a test's) are why these are ports.
|
|
9
|
-
*
|
|
10
|
-
* Nothing here may break a use case: a tracer or a reporter that throws is ignored, and the use
|
|
11
|
-
* case answers as it would have.
|
|
12
|
-
*/
|
|
13
|
-
const EMPTY_SPAN = {
|
|
14
|
-
traceId: "",
|
|
15
|
-
spanId: "",
|
|
16
|
-
sampled: false,
|
|
17
|
-
set: () => undefined,
|
|
18
|
-
end: () => undefined,
|
|
19
|
-
};
|
|
20
|
-
/** Traces nothing: what a use case runs with until a platform plugs a tracer. */
|
|
21
|
-
export const NO_TRACER = { start: () => EMPTY_SPAN };
|
|
@@ -1,79 +0,0 @@
|
|
|
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
|
-
}
|