@sincpro/client-framework 0.6.0 → 1.1.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 +95 -25
- package/MANIFESTO.md +103 -40
- package/README.md +608 -333
- package/dist/circuit_breaker/domain/circuit.d.ts +29 -0
- package/dist/circuit_breaker/domain/circuit.js +37 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.d.ts +56 -0
- package/dist/circuit_breaker/entrypoint/circuit-breaker.js +94 -0
- package/dist/circuit_breaker/index.d.ts +9 -0
- package/dist/circuit_breaker/index.js +9 -0
- package/dist/client/adapters/json-rpc.d.ts +40 -0
- package/dist/client/adapters/json-rpc.js +97 -0
- 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/adapters/rest-operations.d.ts +19 -0
- package/dist/client/adapters/rest-operations.js +66 -0
- 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/domain/operation.d.ts +73 -0
- package/dist/client/domain/operation.js +19 -0
- package/dist/client/index.d.ts +20 -0
- package/dist/client/index.js +20 -0
- package/dist/client/services/guarded.d.ts +17 -0
- package/dist/client/services/guarded.js +19 -0
- package/dist/client/services/propagation.d.ts +45 -0
- package/dist/{services/execution.js → client/services/propagation.js} +49 -36
- 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} +15 -14
- package/dist/codegen/index.d.ts +7 -7
- package/dist/codegen/index.js +5 -10
- package/dist/codegen/services/client.d.ts +15 -0
- package/dist/codegen/services/client.js +111 -0
- 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} +8 -5
- package/dist/codegen/{scaffold.js → services/scaffold.js} +39 -43
- package/dist/common/failures.d.ts +83 -0
- package/dist/common/failures.js +212 -0
- package/dist/common/index.d.ts +11 -0
- package/dist/common/index.js +11 -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 +45 -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 +27 -0
- package/dist/core/domain/bus.js +26 -0
- package/dist/core/domain/dto.d.ts +62 -0
- package/dist/core/domain/dto.js +43 -0
- package/dist/core/domain/errors.d.ts +18 -0
- package/dist/core/domain/errors.js +1 -0
- package/dist/core/domain/pipeline.d.ts +89 -0
- package/dist/core/domain/pipeline.js +1 -0
- package/dist/core/domain/services.d.ts +47 -0
- package/dist/core/domain/services.js +31 -0
- package/dist/core/entrypoint/bounded-context.d.ts +139 -0
- package/dist/core/entrypoint/bounded-context.js +426 -0
- package/dist/core/entrypoint/screens.d.ts +6 -0
- package/dist/core/entrypoint/screens.js +136 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +17 -0
- package/dist/core/infrastructure/listeners.d.ts +28 -0
- package/dist/core/infrastructure/listeners.js +34 -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 +16 -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 +66 -0
- package/dist/core/services/execution.js +216 -0
- package/dist/core/services/interceptors.d.ts +15 -0
- package/dist/core/services/interceptors.js +25 -0
- package/dist/core/services/listening.d.ts +17 -0
- package/dist/core/services/listening.js +25 -0
- package/dist/event_driven/adapters/memory-event-store.d.ts +10 -0
- package/dist/event_driven/adapters/memory-event-store.js +23 -0
- package/dist/event_driven/adapters/stored-queue.d.ts +67 -0
- package/dist/event_driven/adapters/stored-queue.js +192 -0
- package/dist/event_driven/adapters/sync-queue.d.ts +11 -0
- package/dist/event_driven/adapters/sync-queue.js +19 -0
- package/dist/event_driven/domain/envelope.d.ts +28 -0
- package/dist/event_driven/domain/envelope.js +19 -0
- package/dist/event_driven/domain/queue.d.ts +40 -0
- package/dist/event_driven/domain/queue.js +1 -0
- package/dist/event_driven/domain/store.d.ts +56 -0
- package/dist/event_driven/domain/store.js +1 -0
- package/dist/event_driven/entrypoint/publisher.d.ts +26 -0
- package/dist/event_driven/entrypoint/publisher.js +35 -0
- package/dist/event_driven/entrypoint/subscriber.d.ts +34 -0
- package/dist/event_driven/entrypoint/subscriber.js +51 -0
- package/dist/event_driven/index.d.ts +19 -0
- package/dist/event_driven/index.js +17 -0
- package/dist/index.d.ts +21 -38
- package/dist/index.js +21 -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 +44 -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 +106 -0
- package/dist/observability/services/outcome.d.ts +12 -0
- package/dist/observability/services/outcome.js +17 -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/client.d.ts +0 -14
- package/dist/codegen/client.js +0 -201
- 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,212 @@
|
|
|
1
|
+
/** The kinds that fail the same way on every attempt — set aside at the first failure. */
|
|
2
|
+
export const PERMANENT_KINDS = new Set([
|
|
3
|
+
"invalid",
|
|
4
|
+
"unauthenticated",
|
|
5
|
+
"permission_denied",
|
|
6
|
+
"not_found",
|
|
7
|
+
"key_reused",
|
|
8
|
+
"domain",
|
|
9
|
+
]);
|
|
10
|
+
/** What the caller does next, by kind — the backend's `NEXT_STEP_OF`. */
|
|
11
|
+
const NEXT_STEP_OF = {
|
|
12
|
+
invalid: "fix_request",
|
|
13
|
+
unauthenticated: "authenticate",
|
|
14
|
+
permission_denied: "ask_for_access",
|
|
15
|
+
not_found: "do_not_retry",
|
|
16
|
+
conflict: "refresh_and_retry",
|
|
17
|
+
in_progress: "retry_later",
|
|
18
|
+
key_reused: "fix_request",
|
|
19
|
+
domain: "do_not_retry",
|
|
20
|
+
exhausted: "retry_later",
|
|
21
|
+
unavailable: "retry_later",
|
|
22
|
+
unknown_outcome: "verify_then_retry",
|
|
23
|
+
internal: "retry_later",
|
|
24
|
+
};
|
|
25
|
+
/** The kinds that are the system's problem: reported, never shown as the caller's answer. */
|
|
26
|
+
const FAULTS = new Set([
|
|
27
|
+
"internal",
|
|
28
|
+
"unavailable",
|
|
29
|
+
"unknown_outcome",
|
|
30
|
+
]);
|
|
31
|
+
const RETRYABLE_STEPS = new Set([
|
|
32
|
+
"retry_later",
|
|
33
|
+
"refresh_and_retry",
|
|
34
|
+
]);
|
|
35
|
+
/** A retryable kind that says nothing waits a second, as the backend's `DEFAULT_RETRY_AFTER`. */
|
|
36
|
+
const DEFAULT_RETRY_AFTER = 1;
|
|
37
|
+
const KINDS = new Set(Object.keys(NEXT_STEP_OF));
|
|
38
|
+
function isKind(value) {
|
|
39
|
+
return typeof value === "string" && KINDS.has(value);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The kind each wire's own code stands for, when an answer carries no `Problem` — the backend's
|
|
43
|
+
* tables, read back, so a transport of any wire (REST, JSON-RPC, gRPC, another backend's) builds
|
|
44
|
+
* errors that declare the same kinds:
|
|
45
|
+
*
|
|
46
|
+
* class RpcError extends Error {
|
|
47
|
+
* constructor(readonly code: number, message: string) { super(message); }
|
|
48
|
+
* get failureKind() { return kindOfJsonRpcCode(this.code); }
|
|
49
|
+
* }
|
|
50
|
+
*/
|
|
51
|
+
export function kindOfHttpStatus(status) {
|
|
52
|
+
if (status === 401)
|
|
53
|
+
return "unauthenticated";
|
|
54
|
+
if (status === 403)
|
|
55
|
+
return "permission_denied";
|
|
56
|
+
if (status === 404 || status === 410)
|
|
57
|
+
return "not_found";
|
|
58
|
+
if (status === 409 || status === 412)
|
|
59
|
+
return "conflict";
|
|
60
|
+
if (status === 429)
|
|
61
|
+
return "exhausted";
|
|
62
|
+
if (status === 502 || status === 503)
|
|
63
|
+
return "unavailable";
|
|
64
|
+
if (status === 504)
|
|
65
|
+
return "unknown_outcome";
|
|
66
|
+
if (status >= 500)
|
|
67
|
+
return "internal";
|
|
68
|
+
return "invalid";
|
|
69
|
+
}
|
|
70
|
+
/** The kind a gRPC status code stands for — the backend's gRPC table, read back. */
|
|
71
|
+
export function kindOfGrpcCode(code) {
|
|
72
|
+
switch (code) {
|
|
73
|
+
case 3: // INVALID_ARGUMENT
|
|
74
|
+
case 11: // OUT_OF_RANGE
|
|
75
|
+
return "invalid";
|
|
76
|
+
case 16: // UNAUTHENTICATED
|
|
77
|
+
return "unauthenticated";
|
|
78
|
+
case 7: // PERMISSION_DENIED
|
|
79
|
+
return "permission_denied";
|
|
80
|
+
case 5: // NOT_FOUND
|
|
81
|
+
case 12: // UNIMPLEMENTED
|
|
82
|
+
return "not_found";
|
|
83
|
+
case 6: // ALREADY_EXISTS
|
|
84
|
+
case 10: // ABORTED — also an idempotent run in flight
|
|
85
|
+
return "conflict";
|
|
86
|
+
case 9: // FAILED_PRECONDITION — also a reused idempotency key
|
|
87
|
+
return "domain";
|
|
88
|
+
case 8: // RESOURCE_EXHAUSTED
|
|
89
|
+
return "exhausted";
|
|
90
|
+
case 14: // UNAVAILABLE
|
|
91
|
+
return "unavailable";
|
|
92
|
+
case 1: // CANCELLED
|
|
93
|
+
case 4: // DEADLINE_EXCEEDED
|
|
94
|
+
return "unknown_outcome";
|
|
95
|
+
default: // UNKNOWN, INTERNAL, DATA_LOSS
|
|
96
|
+
return "internal";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The kind a JSON-RPC error code stands for — the backend's catalogue, read back. */
|
|
100
|
+
export function kindOfJsonRpcCode(code) {
|
|
101
|
+
switch (code) {
|
|
102
|
+
case -32700:
|
|
103
|
+
case -32600:
|
|
104
|
+
case -32602:
|
|
105
|
+
return "invalid";
|
|
106
|
+
case -32601:
|
|
107
|
+
case -32004:
|
|
108
|
+
return "not_found";
|
|
109
|
+
case -32000:
|
|
110
|
+
return "unavailable";
|
|
111
|
+
case -32001:
|
|
112
|
+
return "unauthenticated";
|
|
113
|
+
case -32003:
|
|
114
|
+
return "permission_denied";
|
|
115
|
+
case -32009:
|
|
116
|
+
return "conflict";
|
|
117
|
+
case -32010:
|
|
118
|
+
return "domain";
|
|
119
|
+
case -32022:
|
|
120
|
+
return "key_reused";
|
|
121
|
+
case -32024:
|
|
122
|
+
return "unknown_outcome";
|
|
123
|
+
case -32029: // an idempotent run in flight, or a limit spent — the problem says which
|
|
124
|
+
return "exhausted";
|
|
125
|
+
default:
|
|
126
|
+
return "internal";
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function upperSnake(kind) {
|
|
130
|
+
return kind.toUpperCase();
|
|
131
|
+
}
|
|
132
|
+
/** What the error declares: on itself first, then on its class. */
|
|
133
|
+
function declared(error) {
|
|
134
|
+
const own = error;
|
|
135
|
+
const type = (error.constructor ?? {});
|
|
136
|
+
return {
|
|
137
|
+
...(isKind(own.failureKind ?? type.failureKind)
|
|
138
|
+
? { failureKind: own.failureKind ?? type.failureKind }
|
|
139
|
+
: {}),
|
|
140
|
+
...(typeof (own.reason ?? type.reason) === "string"
|
|
141
|
+
? { reason: own.reason ?? type.reason }
|
|
142
|
+
: {}),
|
|
143
|
+
...(typeof (own.retryAfter ?? type.retryAfter) === "number"
|
|
144
|
+
? { retryAfter: own.retryAfter ?? type.retryAfter }
|
|
145
|
+
: {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function isProblem(value) {
|
|
149
|
+
return (value !== null &&
|
|
150
|
+
typeof value === "object" &&
|
|
151
|
+
isKind(value.kind) &&
|
|
152
|
+
typeof value.next_step === "string");
|
|
153
|
+
}
|
|
154
|
+
function failure(kind, said, aborted = false) {
|
|
155
|
+
const nextStep = said.nextStep ?? NEXT_STEP_OF[kind];
|
|
156
|
+
const retryable = RETRYABLE_STEPS.has(nextStep);
|
|
157
|
+
return {
|
|
158
|
+
kind,
|
|
159
|
+
reason: said.reason ?? upperSnake(kind),
|
|
160
|
+
nextStep,
|
|
161
|
+
retryable,
|
|
162
|
+
retryAfter: retryable ? (said.retryAfter ?? DEFAULT_RETRY_AFTER) : undefined,
|
|
163
|
+
permanent: !aborted && PERMANENT_KINDS.has(kind),
|
|
164
|
+
fault: !aborted && FAULTS.has(kind),
|
|
165
|
+
aborted,
|
|
166
|
+
problem: said.problem,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The failure `error` is.
|
|
171
|
+
*
|
|
172
|
+
* 1. Whoever asked stopped waiting (`AbortError`): aborted — a write may have run, so its kind is
|
|
173
|
+
* `unknown_outcome`, but it is nobody's failure.
|
|
174
|
+
* 2. What the error declares (`failureKind`, `reason`, `retryAfter`), on itself or its class.
|
|
175
|
+
* 3. What the server said (`problem`, a `Problem` — the same over REST, JSON-RPC and gRPC): its
|
|
176
|
+
* kind, reason, next step and wait.
|
|
177
|
+
* 4. A REST answer (`TransportError`) with a status and no problem: the kind its HTTP status
|
|
178
|
+
* stands for. Another wire's transport declares its kind instead (`kindOfGrpcCode`,
|
|
179
|
+
* `kindOfJsonRpcCode`).
|
|
180
|
+
* 5. A request that got no answer (`status: null`): `unknown_outcome` — it may have run.
|
|
181
|
+
* Final: anything else is `internal`, a fault.
|
|
182
|
+
*/
|
|
183
|
+
export function failureOf(error) {
|
|
184
|
+
if (error === null || typeof error !== "object")
|
|
185
|
+
return failure("internal", {});
|
|
186
|
+
const named = error.name;
|
|
187
|
+
if (named === "AbortError")
|
|
188
|
+
return failure("unknown_outcome", {}, true);
|
|
189
|
+
const own = declared(error);
|
|
190
|
+
if (own.failureKind !== undefined)
|
|
191
|
+
return failure(own.failureKind, {
|
|
192
|
+
...(own.reason !== undefined ? { reason: own.reason } : {}),
|
|
193
|
+
...(own.retryAfter !== undefined ? { retryAfter: own.retryAfter } : {}),
|
|
194
|
+
});
|
|
195
|
+
const answered = error;
|
|
196
|
+
if (isProblem(answered.problem)) {
|
|
197
|
+
const problem = answered.problem;
|
|
198
|
+
return failure(problem.kind, {
|
|
199
|
+
reason: problem.reason,
|
|
200
|
+
nextStep: problem.next_step,
|
|
201
|
+
...(problem.retry_after !== undefined ? { retryAfter: problem.retry_after } : {}),
|
|
202
|
+
problem,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if ("problem" in answered && "status" in answered) {
|
|
206
|
+
if (typeof answered.status === "number")
|
|
207
|
+
return failure(kindOfHttpStatus(answered.status), {});
|
|
208
|
+
if (answered.status === null)
|
|
209
|
+
return failure("unknown_outcome", {});
|
|
210
|
+
}
|
|
211
|
+
return failure("internal", {});
|
|
212
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values of the wire, as the backend writes them: ids, a day and an instant, `Decimal`,
|
|
3
|
+
* `Money`, a to-many as a page of its records, and how any value is written and read.
|
|
4
|
+
*/
|
|
5
|
+
export { collectionOf, type EntityCollection, type HeldCollection, isCollection, isPartial, relatedCriteria, type RelationKeys, totalOf, } from "./collection.js";
|
|
6
|
+
export { Decimal, type DecimalLike } from "./decimal.js";
|
|
7
|
+
export { type DeclaredFailure, type Failure, failureOf, kindOfGrpcCode, kindOfHttpStatus, kindOfJsonRpcCode, PERMANENT_KINDS, } from "./failures.js";
|
|
8
|
+
export { isEntityId, momentOf, newEntityId } from "./ids.js";
|
|
9
|
+
export { Money } from "./money.js";
|
|
10
|
+
export { BYTES, type JsonValue, pack, readValues, type Rebuild, rebuilt, unpack, valuesOf, } from "./serialization.js";
|
|
11
|
+
export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./time.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The values of the wire, as the backend writes them: ids, a day and an instant, `Decimal`,
|
|
3
|
+
* `Money`, a to-many as a page of its records, and how any value is written and read.
|
|
4
|
+
*/
|
|
5
|
+
export { collectionOf, isCollection, isPartial, relatedCriteria, totalOf, } from "./collection.js";
|
|
6
|
+
export { Decimal } from "./decimal.js";
|
|
7
|
+
export { failureOf, kindOfGrpcCode, kindOfHttpStatus, kindOfJsonRpcCode, PERMANENT_KINDS, } from "./failures.js";
|
|
8
|
+
export { isEntityId, momentOf, newEntityId } from "./ids.js";
|
|
9
|
+
export { Money } from "./money.js";
|
|
10
|
+
export { BYTES, pack, readValues, rebuilt, unpack, valuesOf, } from "./serialization.js";
|
|
11
|
+
export { dayOf, installTimeZone, instantOf, isDay, timeZoneNow, wallTimeOf, } from "./time.js";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context in this process's memory: the default, on every platform. A test gets a fresh one
|
|
3
|
+
* from `configureProcess()`.
|
|
4
|
+
*/
|
|
5
|
+
export function memoryContexts() {
|
|
6
|
+
const held = new Map();
|
|
7
|
+
const versions = new Map();
|
|
8
|
+
const listeners = new Map();
|
|
9
|
+
const changed = (key) => {
|
|
10
|
+
versions.set(key, (versions.get(key) ?? 0) + 1);
|
|
11
|
+
for (const listener of [...(listeners.get(key) ?? [])])
|
|
12
|
+
listener();
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
load: (key) => held.get(key),
|
|
16
|
+
save(key, values) {
|
|
17
|
+
// A copy: whoever handed the values may keep changing their object.
|
|
18
|
+
held.set(key, { ...values });
|
|
19
|
+
changed(key);
|
|
20
|
+
},
|
|
21
|
+
delete(key) {
|
|
22
|
+
if (!held.delete(key))
|
|
23
|
+
return;
|
|
24
|
+
changed(key);
|
|
25
|
+
},
|
|
26
|
+
version: (key) => versions.get(key) ?? 0,
|
|
27
|
+
watch(key, listener) {
|
|
28
|
+
const own = listeners.get(key) ?? new Set();
|
|
29
|
+
listeners.set(key, own);
|
|
30
|
+
own.add(listener);
|
|
31
|
+
return () => void own.delete(listener);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { travelling, } from "../domain/codec.js";
|
|
2
|
+
/** The codec for anything outside the codebase: only the values that travel, as JSON. */
|
|
3
|
+
export const plainCodec = {
|
|
4
|
+
dumps: (values) => JSON.stringify(travelling(values)),
|
|
5
|
+
loads(text) {
|
|
6
|
+
try {
|
|
7
|
+
const found = JSON.parse(text);
|
|
8
|
+
return found !== null && typeof found === "object" && !Array.isArray(found)
|
|
9
|
+
? found
|
|
10
|
+
: {};
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
};
|
|
@@ -23,4 +23,3 @@ export interface ContextCodec {
|
|
|
23
23
|
export declare function travels(value: unknown): boolean;
|
|
24
24
|
/** The values of `context` that travel; the rest stays in this process. */
|
|
25
25
|
export declare function travelling(context: ContextValues): ContextValues;
|
|
26
|
-
export declare const plainCodec: ContextCodec;
|
|
@@ -28,17 +28,3 @@ export function travels(value) {
|
|
|
28
28
|
export function travelling(context) {
|
|
29
29
|
return Object.fromEntries(Object.entries(context).filter(([, value]) => travels(value)));
|
|
30
30
|
}
|
|
31
|
-
export const plainCodec = {
|
|
32
|
-
dumps: (values) => JSON.stringify(travelling(values)),
|
|
33
|
-
loads(text) {
|
|
34
|
-
try {
|
|
35
|
-
const found = JSON.parse(text);
|
|
36
|
-
return found !== null && typeof found === "object" && !Array.isArray(found)
|
|
37
|
-
? found
|
|
38
|
-
: {};
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Span } from "../../observability/domain/tracing.js";
|
|
2
|
+
/**
|
|
3
|
+
* One run of a use case: its span, what it runs with, and who ran it.
|
|
4
|
+
*
|
|
5
|
+
* Context: sincpro_framework keeps the running use case in a context variable; a browser has
|
|
6
|
+
* nothing like it that survives an `await` (AsyncContext is a proposal, Zone.js patches the
|
|
7
|
+
* world), and a global stack mixes the parents of two use cases running at once — which a
|
|
8
|
+
* screen with several windows does all the time. So an execution is a value, handed to what
|
|
9
|
+
* the use case calls: the Features its `featureBus` runs, and its dependencies (`BIND`).
|
|
10
|
+
*/
|
|
11
|
+
export type Execution = {
|
|
12
|
+
/** UUID v7, as 32 hex: this run. */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** `<bounded context>.<Service>` — the use case's identity, as the backend names its own. */
|
|
15
|
+
readonly useCase: string;
|
|
16
|
+
/** The bounded context the use case belongs to. */
|
|
17
|
+
readonly boundedContext: string;
|
|
18
|
+
/** The run that executed this one: an ApplicationService, for its Features. */
|
|
19
|
+
readonly parent: Execution | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The chain it belongs to: the id of what started it all — the root run, or the first event
|
|
22
|
+
* of a flow it continues. Every run and event of one flow share it.
|
|
23
|
+
*/
|
|
24
|
+
readonly correlationId: string;
|
|
25
|
+
/** What directly caused it: its parent run, or the event it executes; none for a root. */
|
|
26
|
+
readonly causationId: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* What cancels it: the signal it was executed with, inherited by its Features — and by every
|
|
29
|
+
* request its bound transport sends.
|
|
30
|
+
*/
|
|
31
|
+
readonly signal: AbortSignal | undefined;
|
|
32
|
+
readonly span: Span;
|
|
33
|
+
/** What it runs with, read once where the gesture started and inherited by every child. */
|
|
34
|
+
readonly context: Readonly<Record<string, unknown>>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* A dependency that has something to say about the execution it is used in: given that
|
|
38
|
+
* execution, it answers itself bound to it. The transport, every generated client and the
|
|
39
|
+
* predefined loggers are; any other dependency of a bounded context is handed as it is.
|
|
40
|
+
*/
|
|
41
|
+
export declare const BIND: unique symbol;
|
|
42
|
+
export type Bindable<T> = {
|
|
43
|
+
[BIND]: (execution: Execution) => T;
|
|
44
|
+
};
|
|
45
|
+
export declare function isBindable<T>(value: T): value is T & Bindable<T>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dependency that has something to say about the execution it is used in: given that
|
|
3
|
+
* execution, it answers itself bound to it. The transport, every generated client and the
|
|
4
|
+
* predefined loggers are; any other dependency of a bounded context is handed as it is.
|
|
5
|
+
*/
|
|
6
|
+
export const BIND = Symbol.for("sincpro.client-framework.bind");
|
|
7
|
+
export function isBindable(value) {
|
|
8
|
+
return (value !== null &&
|
|
9
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
10
|
+
typeof value[BIND] === "function");
|
|
11
|
+
}
|
|
@@ -44,8 +44,3 @@ export interface ContextStore {
|
|
|
44
44
|
*/
|
|
45
45
|
watch?(key: string, listener: () => void): () => void;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
* The context in this process's memory: the default, on every platform. A test gets a fresh one
|
|
49
|
-
* from `configureProcess()`.
|
|
50
|
-
*/
|
|
51
|
-
export declare function memoryContexts(): ContextStore;
|
|
@@ -23,37 +23,4 @@
|
|
|
23
23
|
* Reads are synchronous on purpose: a use case reads its context where a gesture starts, and an
|
|
24
24
|
* asynchronous store would make every use case wait for it.
|
|
25
25
|
*/
|
|
26
|
-
|
|
27
|
-
* The context in this process's memory: the default, on every platform. A test gets a fresh one
|
|
28
|
-
* from `configureProcess()`.
|
|
29
|
-
*/
|
|
30
|
-
export function memoryContexts() {
|
|
31
|
-
const held = new Map();
|
|
32
|
-
const versions = new Map();
|
|
33
|
-
const listeners = new Map();
|
|
34
|
-
const changed = (key) => {
|
|
35
|
-
versions.set(key, (versions.get(key) ?? 0) + 1);
|
|
36
|
-
for (const listener of [...(listeners.get(key) ?? [])])
|
|
37
|
-
listener();
|
|
38
|
-
};
|
|
39
|
-
return {
|
|
40
|
-
load: (key) => held.get(key),
|
|
41
|
-
save(key, values) {
|
|
42
|
-
// A copy: whoever handed the values may keep changing their object.
|
|
43
|
-
held.set(key, { ...values });
|
|
44
|
-
changed(key);
|
|
45
|
-
},
|
|
46
|
-
delete(key) {
|
|
47
|
-
if (!held.delete(key))
|
|
48
|
-
return;
|
|
49
|
-
changed(key);
|
|
50
|
-
},
|
|
51
|
-
version: (key) => versions.get(key) ?? 0,
|
|
52
|
-
watch(key, listener) {
|
|
53
|
-
const own = listeners.get(key) ?? new Set();
|
|
54
|
-
listeners.set(key, own);
|
|
55
|
-
own.add(listener);
|
|
56
|
-
return () => void own.delete(listener);
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
26
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ContextStore, ContextValues } from "
|
|
1
|
+
import type { ContextStore, ContextValues } from "../domain/store.js";
|
|
2
2
|
/**
|
|
3
3
|
* One level of the context: the process's (`processContext`) or a bounded context's
|
|
4
4
|
* (`project.context`), read and written the same way on every platform.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context: the levels a call sees (the process, its bounded context, a scope), where they
|
|
3
|
+
* live (`ContextStore`, a port each platform implements), what travels with a request, and the
|
|
4
|
+
* execution a run carries.
|
|
5
|
+
*
|
|
6
|
+
* domain/ the ports and rules: `ContextStore`, `ContextCodec`, what travels, `Execution`
|
|
7
|
+
* adapters/ the implementations this package gives: memory, the plain codec
|
|
8
|
+
* entrypoint/ `Context`: one door over a level
|
|
9
|
+
* infrastructure/ the process's store and its level (`processContext`)
|
|
10
|
+
*/
|
|
11
|
+
export { memoryContexts } from "./adapters/memory.js";
|
|
12
|
+
export { plainCodec } from "./adapters/plain-codec.js";
|
|
13
|
+
export { CONTEXT_HEADER, type ContextCodec, travelling, travels, } from "./domain/codec.js";
|
|
14
|
+
export { BIND, type Bindable, type Execution, } from "./domain/execution.js";
|
|
15
|
+
export type { ContextStore, ContextValues, } from "./domain/store.js";
|
|
16
|
+
export type { Context } from "./entrypoint/context.js";
|
|
17
|
+
export { processContext } from "./infrastructure/process.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The context: the levels a call sees (the process, its bounded context, a scope), where they
|
|
3
|
+
* live (`ContextStore`, a port each platform implements), what travels with a request, and the
|
|
4
|
+
* execution a run carries.
|
|
5
|
+
*
|
|
6
|
+
* domain/ the ports and rules: `ContextStore`, `ContextCodec`, what travels, `Execution`
|
|
7
|
+
* adapters/ the implementations this package gives: memory, the plain codec
|
|
8
|
+
* entrypoint/ `Context`: one door over a level
|
|
9
|
+
* infrastructure/ the process's store and its level (`processContext`)
|
|
10
|
+
*/
|
|
11
|
+
export { memoryContexts } from "./adapters/memory.js";
|
|
12
|
+
export { plainCodec } from "./adapters/plain-codec.js";
|
|
13
|
+
export { CONTEXT_HEADER, travelling, travels, } from "./domain/codec.js";
|
|
14
|
+
export { BIND, } from "./domain/execution.js";
|
|
15
|
+
export { processContext } from "./infrastructure/process.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ContextStore } from "../domain/store.js";
|
|
2
|
+
import { type Context, type StoreSource } from "../entrypoint/context.js";
|
|
3
|
+
/** @internal — `configureProcess` is the way in. */
|
|
4
|
+
export declare function replaceProcessStore(store: ContextStore): void;
|
|
5
|
+
/** The process's store as every level of the context reads it — the process's and each bounded context's. */
|
|
6
|
+
export declare const processStore: StoreSource;
|
|
7
|
+
/**
|
|
8
|
+
* The process's level of the context: what every use case of every bounded context runs with —
|
|
9
|
+
* the tenant, the company, the language.
|
|
10
|
+
*
|
|
11
|
+
* processContext.set({ tenant_id: "acme" });
|
|
12
|
+
*/
|
|
13
|
+
export declare const processContext: Context;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { memoryContexts } from "../adapters/memory.js";
|
|
2
|
+
import { contextAt, } from "../entrypoint/context.js";
|
|
3
|
+
/**
|
|
4
|
+
* Where this process keeps its context: the store `configureProcess` chose (`memoryContexts()`
|
|
5
|
+
* until it chooses), read on every call, never at import.
|
|
6
|
+
*
|
|
7
|
+
* Context: a level made before `main` configures the process (`project` is defined when its
|
|
8
|
+
* module loads) still reads the store chosen later; whoever listens moves to the new store and
|
|
9
|
+
* hears the change.
|
|
10
|
+
*/
|
|
11
|
+
let current = memoryContexts();
|
|
12
|
+
const replacing = new Set();
|
|
13
|
+
/** @internal — `configureProcess` is the way in. */
|
|
14
|
+
export function replaceProcessStore(store) {
|
|
15
|
+
const before = current;
|
|
16
|
+
current = store;
|
|
17
|
+
if (current !== before)
|
|
18
|
+
for (const listener of [...replacing])
|
|
19
|
+
listener();
|
|
20
|
+
}
|
|
21
|
+
/** The process's store as every level of the context reads it — the process's and each bounded context's. */
|
|
22
|
+
export const processStore = {
|
|
23
|
+
current: () => current,
|
|
24
|
+
replaced(listener) {
|
|
25
|
+
replacing.add(listener);
|
|
26
|
+
return () => void replacing.delete(listener);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The process's level of the context: what every use case of every bounded context runs with —
|
|
31
|
+
* the tenant, the company, the language.
|
|
32
|
+
*
|
|
33
|
+
* processContext.set({ tenant_id: "acme" });
|
|
34
|
+
*/
|
|
35
|
+
export const processContext = contextAt("process", processStore);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Command, DomainEvent, Executable, ResponseClass } from "./dto.js";
|
|
2
|
+
import type { Cause, ExecuteOptions } from "./pipeline.js";
|
|
3
|
+
/** What the second argument of `execute` is: options, a delivered event's cause, or a type. */
|
|
4
|
+
export declare function optionsOf(second: unknown): ExecuteOptions;
|
|
5
|
+
/**
|
|
6
|
+
* What executes a Command or a domain event: the bounded context, a scope of it, an
|
|
7
|
+
* ApplicationService's features. A Command answers its Response; an event, whatever its
|
|
8
|
+
* subscriber returns.
|
|
9
|
+
*/
|
|
10
|
+
export interface CommandBus {
|
|
11
|
+
execute<R>(command: Command<R>, returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
|
|
12
|
+
execute<R>(command: Command<R>, options: ExecuteOptions): Promise<R>;
|
|
13
|
+
execute(event: DomainEvent, options?: ExecuteOptions | Cause): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
/** @internal — how a bus hands what it executes to its generation. */
|
|
16
|
+
export type Dispatch = (executable: Executable, options: ExecuteOptions) => Promise<unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* What an ApplicationService composes its context's Features through (`this.featureBus`): the
|
|
19
|
+
* same pipeline, as a child of the running execution, and Features only.
|
|
20
|
+
*/
|
|
21
|
+
export declare class FeatureBus implements CommandBus {
|
|
22
|
+
private readonly dispatch;
|
|
23
|
+
constructor(dispatch: Dispatch);
|
|
24
|
+
execute<R>(command: Command<R>, returnType?: ResponseClass<NoInfer<R>>): Promise<R>;
|
|
25
|
+
execute<R>(command: Command<R>, options: ExecuteOptions): Promise<R>;
|
|
26
|
+
execute(event: DomainEvent, options?: ExecuteOptions | Cause): Promise<unknown>;
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** What the second argument of `execute` is: options, a delivered event's cause, or a type. */
|
|
2
|
+
export function optionsOf(second) {
|
|
3
|
+
if (second === null || typeof second !== "object")
|
|
4
|
+
return {};
|
|
5
|
+
const given = second;
|
|
6
|
+
if (typeof given.id === "string" && typeof given.correlationId === "string")
|
|
7
|
+
return { cause: { id: given.id, correlationId: given.correlationId } };
|
|
8
|
+
return {
|
|
9
|
+
...(given.signal ? { signal: given.signal } : {}),
|
|
10
|
+
...(given.cause ? { cause: given.cause } : {}),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* What an ApplicationService composes its context's Features through (`this.featureBus`): the
|
|
15
|
+
* same pipeline, as a child of the running execution, and Features only.
|
|
16
|
+
*/
|
|
17
|
+
export class FeatureBus {
|
|
18
|
+
dispatch;
|
|
19
|
+
constructor(dispatch) {
|
|
20
|
+
this.dispatch = dispatch;
|
|
21
|
+
}
|
|
22
|
+
execute(executable, second) {
|
|
23
|
+
// As with Python's return_type, the answer is not validated at runtime.
|
|
24
|
+
return this.dispatch(executable, optionsOf(second));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The DTOs of the application layer, as sincpro_framework's `DataTransferObject`: a Command asks
|
|
3
|
+
* for work and names, by its type, the Response it answers.
|
|
4
|
+
*
|
|
5
|
+
* class CommandCancelIssue extends Command<ResponseCancelIssue> {
|
|
6
|
+
* constructor(readonly issue_id: string) { super(); }
|
|
7
|
+
* }
|
|
8
|
+
* const answer = await project.execute(new CommandCancelIssue("x")); // ResponseCancelIssue
|
|
9
|
+
*
|
|
10
|
+
* Context: the backend asks `bus(dto, ResponseType)` for the type; here the Command carries it, so
|
|
11
|
+
* `execute` infers it with no second argument. The association is type-only: absent from a
|
|
12
|
+
* serialized DTO, not a runtime validator. A generated DTO is a remote contract (a type), not one
|
|
13
|
+
* of these: a local Command may implement it.
|
|
14
|
+
*/
|
|
15
|
+
export declare abstract class DataTransferObject {
|
|
16
|
+
protected readonly dtoBrand: void;
|
|
17
|
+
}
|
|
18
|
+
export declare abstract class Response extends DataTransferObject {
|
|
19
|
+
}
|
|
20
|
+
declare const responseType: unique symbol;
|
|
21
|
+
export declare abstract class Command<R> extends DataTransferObject {
|
|
22
|
+
/** Type-only association: absent from serialized DTOs and not a runtime validator. */
|
|
23
|
+
readonly [responseType]: R;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A fact that already happened, as sincpro_framework's `DomainEvent`: executed like a Command, by
|
|
27
|
+
* the Feature or ApplicationService registered for its class — its subscriber in this bounded
|
|
28
|
+
* context —, through the same pipeline. Delivering it (a queue, an outbox) is the platform's.
|
|
29
|
+
*
|
|
30
|
+
* class OrderPaid extends DomainEvent {
|
|
31
|
+
* static readonly eventName = "shop.order.v1.paid";
|
|
32
|
+
* constructor(readonly order_id: string) { super(); }
|
|
33
|
+
* }
|
|
34
|
+
* project.feature(OrderPaid)(NotifyCustomer);
|
|
35
|
+
* await project.execute(new OrderPaid("o-1"));
|
|
36
|
+
*
|
|
37
|
+
* Context: the backend names an event on the wire by `name`, `<context>.<aggregate>.vN.<fact>`,
|
|
38
|
+
* and defaults it to the class's name; a client is minified, so the name is declared, as
|
|
39
|
+
* `serviceName` is. `name` is reserved, as the backend reserves it: every event reads its class's
|
|
40
|
+
* `eventName` there, and a subclass cannot make it a field.
|
|
41
|
+
*/
|
|
42
|
+
export declare abstract class DomainEvent extends DataTransferObject {
|
|
43
|
+
protected readonly eventBrand: void;
|
|
44
|
+
/** Its wire name: its class's `eventName`. */
|
|
45
|
+
get name(): string;
|
|
46
|
+
}
|
|
47
|
+
/** What an event class declares: the name it is known by on the wire. */
|
|
48
|
+
type EventNamed = {
|
|
49
|
+
readonly eventName: string;
|
|
50
|
+
};
|
|
51
|
+
export type AnyCommand = Command<unknown>;
|
|
52
|
+
/** What a service is registered for and executes: a Command, or a domain event. */
|
|
53
|
+
export type Executable = AnyCommand | DomainEvent;
|
|
54
|
+
/** What it answers: a Command's Response; whatever its subscriber returns, for an event. */
|
|
55
|
+
export type Answer<T> = T extends Command<infer R> ? R : unknown;
|
|
56
|
+
/** A Command's class: what a service is registered for, by exact constructor identity. */
|
|
57
|
+
export type CommandClass<C extends AnyCommand> = abstract new (...args: never[]) => C;
|
|
58
|
+
/** A domain event's class, with its wire name. */
|
|
59
|
+
export type EventClass<E extends DomainEvent> = (abstract new (...args: never[]) => E) & EventNamed;
|
|
60
|
+
/** A Response's class: the backend's `return_type`, accepted for the same reading. */
|
|
61
|
+
export type ResponseClass<R> = abstract new (...args: never[]) => R;
|
|
62
|
+
export {};
|