@sincpro/client-framework 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/AGENTS.md +22 -11
  2. package/MANIFESTO.md +7 -3
  3. package/README.md +58 -0
  4. package/dist/client/memory.js +8 -5
  5. package/dist/codegen/check.d.ts +64 -0
  6. package/dist/codegen/check.js +195 -0
  7. package/dist/codegen/cli.js +73 -6
  8. package/dist/codegen/client.js +5 -5
  9. package/dist/codegen/domain.d.ts +2 -1
  10. package/dist/codegen/domain.js +17 -5
  11. package/dist/codegen/entities.js +10 -3
  12. package/dist/codegen/index.d.ts +2 -0
  13. package/dist/codegen/index.js +2 -0
  14. package/dist/codegen/names.d.ts +12 -0
  15. package/dist/codegen/names.js +58 -0
  16. package/dist/codegen/scaffold.d.ts +24 -0
  17. package/dist/codegen/scaffold.js +123 -0
  18. package/dist/common/decimal.d.ts +49 -0
  19. package/dist/common/decimal.js +154 -0
  20. package/dist/{client → common}/ids.d.ts +7 -0
  21. package/dist/{client → common}/ids.js +12 -0
  22. package/dist/common/money.d.ts +74 -0
  23. package/dist/common/money.js +168 -0
  24. package/dist/common/serialization.d.ts +53 -0
  25. package/dist/common/serialization.js +168 -0
  26. package/dist/common/time.d.ts +41 -0
  27. package/dist/common/time.js +100 -0
  28. package/dist/context/codec.d.ts +26 -0
  29. package/dist/context/codec.js +44 -0
  30. package/dist/index.d.ts +11 -2
  31. package/dist/index.js +11 -2
  32. package/dist/observability/ports.d.ts +2 -1
  33. package/dist/services/bounded-context.js +5 -16
  34. package/dist/services/execution.d.ts +2 -2
  35. package/dist/services/execution.js +6 -4
  36. package/dist/services/feature.d.ts +4 -1
  37. package/dist/services/report.d.ts +15 -0
  38. package/dist/services/report.js +53 -0
  39. package/dist/services/screens.d.ts +2 -0
  40. package/dist/services/screens.js +67 -0
  41. package/package.json +6 -2
package/dist/index.js CHANGED
@@ -6,8 +6,10 @@
6
6
  * (`sideEffects: false`), so an application pays only for what it uses, and a use case, the
7
7
  * client it calls and the entity it reads come from the same place.
8
8
  *
9
+ * common/ the values of the wire, as the backend writes them: ids, a day and an instant,
10
+ * `Decimal`, `Money`, and how any value is written and read (`valuesOf`, `rebuilt`)
9
11
  * client/ the seam with a server: the transport and its REST and in-memory implementations,
10
- * entities with typed commands, the service's client contract, client-side ids
12
+ * entities with typed commands, the service's client contract
11
13
  * model/ model definitions: written by hand, read from a JSON Schema, kept once known
12
14
  * services/ the bounded context (`UseBoundedContext`), its `Feature` and `ApplicationService`,
13
15
  * the process they run in (`configureProcess`), what each run carries
@@ -19,11 +21,16 @@
19
21
  */
20
22
  export { entityKeyOf, kindOf, metasOf, versionsOf, } from "./client/contract.js";
21
23
  export { bodyFor, entity, listRoute, NEW_PARAM, pathFor, PICK_PARAM, QUERY_PARAM, SCOPE_PARAM, scopeOfRef, startingValuesOf, TRAIL_PARAM, WITH_PARAM, writeRoute, } from "./client/entity.js";
22
- export { isEntityId, newEntityId } from "./client/ids.js";
23
24
  export { memoryTransport, } from "./client/memory.js";
24
25
  export { restTransport, urlOf, } from "./client/rest.js";
25
26
  export { retryKeys, unanswered, } from "./client/retry.js";
26
27
  export { readRoute, TransportError, } from "./client/transport.js";
28
+ export { Decimal } from "./common/decimal.js";
29
+ export { isEntityId, momentOf, newEntityId } from "./common/ids.js";
30
+ export { Money } from "./common/money.js";
31
+ export { BYTES, pack, readValues, rebuilt, unpack, valuesOf, } from "./common/serialization.js";
32
+ export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./common/time.js";
33
+ export { CONTEXT_HEADER, plainCodec, travelling, travels, } from "./context/codec.js";
27
34
  export {} from "./context/context.js";
28
35
  export { memoryContexts, } from "./context/store.js";
29
36
  export { defineMeta, field, } from "./model/meta.js";
@@ -34,3 +41,5 @@ export { UseBoundedContext, } from "./services/bounded-context.js";
34
41
  export { BIND, bindable, bindDependency, bindTransport, headersOf, } from "./services/execution.js";
35
42
  export { ApplicationService, Feature, } from "./services/feature.js";
36
43
  export { configureProcess, outcomeOf, processContext, } from "./services/process.js";
44
+ export { reportError } from "./services/report.js";
45
+ export { screensTransport } from "./services/screens.js";
@@ -47,7 +47,8 @@ export interface Tracer {
47
47
  }
48
48
  /** What a report says about where the error happened. */
49
49
  export type ErrorAbout = {
50
- /** `<bounded context>.<Service>[.<method>]`: the use case's identity, as the backend's. */
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`). */
51
52
  useCase: string;
52
53
  /** The trace the error belongs to — the same id the server logged, when it got that far. */
53
54
  traceId: string;
@@ -1,12 +1,13 @@
1
- import { newEntityId } from "../client/ids.js";
1
+ import { newEntityId } from "../common/ids.js";
2
2
  import { contextAt } from "../context/context.js";
3
3
  import { bindDependency } from "./execution.js";
4
4
  import { composes, isApplicationServiceClass, isFeatureClass, methodsOf, } from "./feature.js";
5
5
  import { processContext, processNow, processStore, } from "./process.js";
6
+ import { firstReport } from "./report.js";
6
7
  /** Names `project` answers itself: no service is registered under them. */
7
8
  const RESERVED = new Set(["name", "context", "with", "scoped"]);
8
9
  /** Names a run answers on `this` itself: no dependency is added under them. */
9
- const ANSWERED = new Set(["context", "feature"]);
10
+ const ANSWERED = new Set(["context", "feature", "span"]);
10
11
  export class UseBoundedContext {
11
12
  name;
12
13
  /** This bounded context's level of the context, saved in the process's store. */
@@ -142,6 +143,8 @@ function viewOf(target, binding, execution) {
142
143
  get(held, property, receiver) {
143
144
  if (property === "context")
144
145
  return execution.context;
146
+ if (property === "span")
147
+ return execution.span;
145
148
  if (property === "feature" && isComposer)
146
149
  return (features ??= featuresOf(binding, execution));
147
150
  if (typeof property === "string") {
@@ -191,20 +194,6 @@ function contextOf(binding) {
191
194
  ...binding.scope,
192
195
  };
193
196
  }
194
- /**
195
- * Whether this is the first run to report this error. A Feature that fails inside an
196
- * ApplicationService throws through it: the error is reported once, by the run where it
197
- * happened — its span is the one that says where — and every run around it still ends `failed`.
198
- */
199
- const REPORTED = new WeakSet();
200
- function firstReport(error) {
201
- if (error === null || (typeof error !== "object" && typeof error !== "function"))
202
- return true;
203
- if (REPORTED.has(error))
204
- return false;
205
- REPORTED.add(error);
206
- return true;
207
- }
208
197
  /** What watches a use case never changes what it answers: a tracer or reporter that throws is ignored. */
209
198
  function quietly(work, otherwise) {
210
199
  try {
@@ -33,8 +33,8 @@ export type Bindable<T> = {
33
33
  };
34
34
  /**
35
35
  * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
36
- * (`sincpro-context`, the header sincpro_framework's entrances read). Nothing when there is no
37
- * trace and no context.
36
+ * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
37
+ * Nothing when there is no trace and no context that travels.
38
38
  */
39
39
  export declare function headersOf(execution: Execution): Record<string, string>;
40
40
  /**
@@ -1,3 +1,4 @@
1
+ import { CONTEXT_HEADER, plainCodec, travels } from "../context/codec.js";
1
2
  /**
2
3
  * A dependency that has something to say about the execution it is used in: given that
3
4
  * execution, it answers itself bound to it. The transport and every generated client are;
@@ -19,8 +20,8 @@ function isTransport(value) {
19
20
  }
20
21
  /**
21
22
  * What every request of an execution carries: its trace (`traceparent`, W3C) and its context
22
- * (`sincpro-context`, the header sincpro_framework's entrances read). Nothing when there is no
23
- * trace and no context.
23
+ * (`sincpro-context`, the header sincpro_framework's entrances read, written with `plainCodec`).
24
+ * Nothing when there is no trace and no context that travels.
24
25
  */
25
26
  export function headersOf(execution) {
26
27
  const headers = {};
@@ -31,8 +32,9 @@ export function headersOf(execution) {
31
32
  if (span.state)
32
33
  headers["tracestate"] = span.state;
33
34
  }
34
- if (Object.keys(context).length > 0)
35
- headers["sincpro-context"] = JSON.stringify(context);
35
+ // Only what travels: an object or a function held in the context stays in this process.
36
+ if (Object.values(context).some(travels))
37
+ headers[CONTEXT_HEADER] = plainCodec.dumps(context);
36
38
  return headers;
37
39
  }
38
40
  /**
@@ -1,4 +1,5 @@
1
1
  import type { ContextValues } from "../context/store.js";
2
+ import type { Span } from "../observability/ports.js";
2
3
  /**
3
4
  * `Feature` and `ApplicationService`: the two kinds of service a bounded context holds, as in
4
5
  * sincpro_framework. Each bounded context gives them its dependencies once, in its own base:
@@ -47,6 +48,8 @@ type Within<D extends object, C extends object> = Readonly<D> & {
47
48
  * read once where the gesture started and the same for every Feature it runs.
48
49
  */
49
50
  readonly context: Readonly<Partial<C>>;
51
+ /** This run's span: a use case says more about what it ran on (`this.span.set({ … })`). */
52
+ readonly span: Span;
50
53
  };
51
54
  /** Service classes by the name a bounded context gives them: `{ Issues, Notices }`. */
52
55
  export type ServiceClasses = Readonly<Record<string, new () => object>>;
@@ -55,7 +58,7 @@ export type ServiceClasses = Readonly<Record<string, new () => object>>;
55
58
  * methods, without its dependencies, its context or its `_` helpers.
56
59
  */
57
60
  export type ServiceMethods<I, D> = {
58
- readonly [M in keyof I as M extends keyof D | "context" | "feature" | `_${string}` ? never : I[M] extends (...args: never[]) => unknown ? M : never]: I[M];
61
+ readonly [M in keyof I as M extends keyof D | "context" | "feature" | "span" | `_${string}` ? never : I[M] extends (...args: never[]) => unknown ? M : never]: I[M];
59
62
  };
60
63
  /** The services of a set of classes, by their names. */
61
64
  export type Services<S extends ServiceClasses, D> = {
@@ -0,0 +1,15 @@
1
+ export declare function firstReport(error: unknown): boolean;
2
+ /**
3
+ * Reports what nobody expected that is not a use case — a screen that broke while drawing, a
4
+ * subscriber, a worker — through the process's reporter, with the process's context.
5
+ *
6
+ * <Composer onViewError={(error, { resource }) => reportError(error, `view ${formatRef(resource)}`)} />
7
+ * ErrorUtils.setGlobalHandler((error) => reportError(error, "react-native global"));
8
+ *
9
+ * Context: a use case reports its own failures (the bounded context does it for it); what fails
10
+ * outside one would only reach a console. `where` takes the place of the use case's name in the
11
+ * report. Only what failed is reported (`outcomeOf`: a refusal the server explained or an abort is
12
+ * not); an error a use case already reported is not reported again; and a reporter that throws is
13
+ * ignored: telling about a failure never becomes a second one.
14
+ */
15
+ export declare function reportError(error: unknown, where: string): void;
@@ -0,0 +1,53 @@
1
+ import { newEntityId } from "../common/ids.js";
2
+ import { processContext, processNow } from "./process.js";
3
+ /**
4
+ * Whether this is the first time this error is reported. A Feature that fails inside an
5
+ * ApplicationService throws through it, and a screen that draws the failure may break on it:
6
+ * the error is reported once, by the first that saw it — its span is the one that says where.
7
+ */
8
+ const REPORTED = new WeakSet();
9
+ export function firstReport(error) {
10
+ if (error === null || (typeof error !== "object" && typeof error !== "function"))
11
+ return true;
12
+ if (REPORTED.has(error))
13
+ return false;
14
+ REPORTED.add(error);
15
+ return true;
16
+ }
17
+ /**
18
+ * Reports what nobody expected that is not a use case — a screen that broke while drawing, a
19
+ * subscriber, a worker — through the process's reporter, with the process's context.
20
+ *
21
+ * <Composer onViewError={(error, { resource }) => reportError(error, `view ${formatRef(resource)}`)} />
22
+ * ErrorUtils.setGlobalHandler((error) => reportError(error, "react-native global"));
23
+ *
24
+ * Context: a use case reports its own failures (the bounded context does it for it); what fails
25
+ * outside one would only reach a console. `where` takes the place of the use case's name in the
26
+ * report. Only what failed is reported (`outcomeOf`: a refusal the server explained or an abort is
27
+ * not); an error a use case already reported is not reported again; and a reporter that throws is
28
+ * ignored: telling about a failure never becomes a second one.
29
+ */
30
+ export function reportError(error, where) {
31
+ const settings = processNow();
32
+ try {
33
+ // What the server explained, or what the caller stopped waiting for, is not a fault here either.
34
+ if (settings.outcomeOf(error) !== "failed")
35
+ return;
36
+ }
37
+ catch {
38
+ // An outcome that cannot be told is a failure.
39
+ }
40
+ if (!firstReport(error))
41
+ return;
42
+ try {
43
+ settings.reporter?.report(error, {
44
+ useCase: where,
45
+ traceId: "",
46
+ executionId: newEntityId(),
47
+ context: processContext.values,
48
+ });
49
+ }
50
+ catch {
51
+ // Ignored, as a reporter that fails inside a use case is.
52
+ }
53
+ }
@@ -0,0 +1,2 @@
1
+ import type { Transport } from "../client/transport.js";
2
+ export declare function screensTransport(transport: Transport, name?: string): Transport;
@@ -0,0 +1,67 @@
1
+ import { UseBoundedContext } from "./bounded-context.js";
2
+ import { Feature } from "./feature.js";
3
+ /**
4
+ * What a screen reads of an entity — a page, a record, a read of its own: sincpro_framework's
5
+ * built-in `EntityReads`, on the client.
6
+ */
7
+ class EntityReads extends Feature {
8
+ list(route, criteria, signal, options) {
9
+ this.span.set({ "http.route": route });
10
+ return this.transport.list(route, criteria, signal, options);
11
+ }
12
+ one(route, signal, options) {
13
+ this.span.set({ "http.route": route });
14
+ return this.transport.one(route, signal, options);
15
+ }
16
+ read(route, signal, options) {
17
+ this.span.set({ "http.route": route });
18
+ const read = this.transport.read;
19
+ if (read === undefined)
20
+ return Promise.reject(new Error(`this transport cannot read ${route}: it has no read()`));
21
+ return read.call(this.transport, route, signal, options);
22
+ }
23
+ }
24
+ /** What a screen writes — a save, a draft, a preview, a Command: each request a use case. */
25
+ class EntityWrites extends Feature {
26
+ send(method, route, body, signal, options) {
27
+ this.span.set({ "http.method": method, "http.route": route });
28
+ return this.transport.send(method, route, body, signal, options);
29
+ }
30
+ }
31
+ /**
32
+ * The transport, every call a use case of a bounded context of its own — `screens` unless named:
33
+ * what a platform hands the screens it draws.
34
+ *
35
+ * const screens = screensTransport(restTransport({ base: "/v1" }));
36
+ * await screens.list("/facturas", criteria); // span screens/EntityReads.list
37
+ *
38
+ * Context: a use case is traced, carries its context and reports what nobody expected; a list a
39
+ * screen reads through the transport was none of that — no span, no `traceparent` (the backend's
40
+ * trace started there, with no client in it), no company, and a 500 reported to nobody. Here
41
+ * each read and write runs as a use case of `screens` — `EntityReads.list`, `.one`, `.read`,
42
+ * `EntityWrites.send` —, so it gets all three the way any other does: the span (its route said
43
+ * on it), the trace and the process's context on the request, and a report only when it failed
44
+ * (a 4xx the server explained is the screen's to show). A bounded context's own dependencies are
45
+ * never given this: there its use case is the one that runs.
46
+ */
47
+ /** The transports this made: given one again, it is answered as it is — wrapped twice, each
48
+ * request would be two unrelated use cases, and the server would see only the outer's trace. */
49
+ const MADE = new WeakSet();
50
+ export function screensTransport(transport, name = "screens") {
51
+ if (MADE.has(transport))
52
+ return transport;
53
+ const screens = new UseBoundedContext(name)
54
+ .addDependency("transport", transport)
55
+ .register({ features: { EntityReads, EntityWrites } });
56
+ const reads = screens.EntityReads;
57
+ const writes = screens.EntityWrites;
58
+ const through = {
59
+ list: (route, criteria, signal, options) => reads.list(route, criteria, signal, options),
60
+ one: (route, signal, options) => reads.one(route, signal, options),
61
+ send: (method, route, body, signal, options) => writes.send(method, route, body, signal, options),
62
+ };
63
+ if (transport.read !== undefined)
64
+ through.read = (route, signal, options) => reads.read(route, signal, options);
65
+ MADE.add(through);
66
+ return through;
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sincpro/client-framework",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -68,11 +68,15 @@
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@opentelemetry/api": "^1.9.0",
71
- "@sincpro/criteria": "^2.0.0"
71
+ "@sincpro/criteria": "^2.0.0",
72
+ "typescript": ">=5.5 <7"
72
73
  },
73
74
  "peerDependenciesMeta": {
74
75
  "@opentelemetry/api": {
75
76
  "optional": true
77
+ },
78
+ "typescript": {
79
+ "optional": true
76
80
  }
77
81
  },
78
82
  "devDependencies": {