@cullet/erp-core 1.0.2 → 1.0.4
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/README.md +24 -24
- package/dist/gate-engine-registry.js.map +1 -1
- package/dist/gate-v1-payload.schema.js +5 -0
- package/dist/gate-v1-payload.schema.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/outcome.js.map +1 -1
- package/dist/policies/engines/v1/gate/index.d.ts +1 -0
- package/dist/policies/engines/v1/gate/index.js.map +1 -1
- package/dist/policy-service.js.map +1 -1
- package/dist/validation-code.js.map +1 -1
- package/dist/validation-error.js.map +1 -1
- package/dist/validation-field.js.map +1 -1
- package/meta.json +67 -67
- package/package.json +56 -56
- package/src/core/application/commands/command.ts +3 -3
- package/src/core/application/commands/requested-by.ts +60 -60
- package/src/core/application/index.ts +21 -21
- package/src/core/application/policy-error-mapper.ts +54 -54
- package/src/core/application/ports/index.ts +6 -6
- package/src/core/application/ports/logger.port.ts +4 -4
- package/src/core/application/ports/metrics.port.ts +3 -3
- package/src/core/application/ports/policy-port.ts +7 -7
- package/src/core/application/ports/repository.port.ts +3 -3
- package/src/core/application/ports/temporal-repository.port.ts +4 -4
- package/src/core/application/ports/tracer.port.ts +7 -7
- package/src/core/application/queries/query.ts +13 -13
- package/src/core/application/temporal/index.ts +7 -7
- package/src/core/application/temporal/temporal-context.ts +20 -20
- package/src/core/application/temporal/temporal-use-case.ts +22 -22
- package/src/core/application/use-case.ts +10 -10
- package/src/core/config/core-config.instance.ts +1 -1
- package/src/core/config/core-config.ts +59 -59
- package/src/core/config/index.ts +6 -6
- package/src/core/config/policy-reporter.ts +31 -31
- package/src/core/config/silent-policy-reporter.ts +1 -1
- package/src/core/domain/entity.ts +44 -44
- package/src/core/domain/rulesets/entity-ruleset.contracts.ts +6 -6
- package/src/core/domain/rulesets/ruleset-registry.ts +49 -49
- package/src/core/domain/rulesets/ruleset.contracts.ts +6 -6
- package/src/core/domain/rulesets/value-object-ruleset.contracts.ts +5 -5
- package/src/core/domain/temporal/index.ts +11 -11
- package/src/core/domain/temporal/temporal-range.ts +16 -16
- package/src/core/domain/temporal/temporal-snapshot.ts +24 -24
- package/src/core/domain/temporal/transaction-time.ts +30 -30
- package/src/core/domain/temporal/valid-time.ts +29 -29
- package/src/core/domain/value-object.ts +16 -16
- package/src/core/errors/app-error.ts +64 -64
- package/src/core/errors/authentication-error.ts +134 -134
- package/src/core/errors/authorization-error.ts +154 -154
- package/src/core/errors/business-rule-violation-error.ts +15 -15
- package/src/core/errors/conflict-error.ts +202 -201
- package/src/core/errors/error-codes.ts +48 -48
- package/src/core/errors/idempotency-error.ts +324 -320
- package/src/core/errors/index.ts +50 -50
- package/src/core/errors/integration-error.ts +120 -120
- package/src/core/errors/legacy-incompatible-error.ts +13 -13
- package/src/core/errors/not-found-error.ts +14 -14
- package/src/core/errors/serialization-error.ts +167 -167
- package/src/core/errors/temporal-error.ts +109 -107
- package/src/core/errors/types.ts +35 -35
- package/src/core/errors/unexpected-error.ts +10 -10
- package/src/core/errors/utils/index.ts +2 -2
- package/src/core/errors/utils/json-safe.ts +53 -53
- package/src/core/errors/validation-error.ts +18 -18
- package/src/core/exceptions/business-rule-violation-exception.ts +6 -6
- package/src/core/exceptions/domain-exception.ts +5 -5
- package/src/core/exceptions/entity-not-found-exception.ts +6 -6
- package/src/core/exceptions/invalid-state-transition-exception.ts +8 -8
- package/src/core/exceptions/invariant-violation-exception.ts +3 -3
- package/src/core/exceptions/validation-code.ts +35 -29
- package/src/core/exceptions/validation-exception.ts +19 -19
- package/src/core/exceptions/validation-field.ts +20 -19
- package/src/core/index.ts +7 -7
- package/src/core/policies/asof/asof.ts +47 -47
- package/src/core/policies/catalog/catalog.instance.ts +8 -6
- package/src/core/policies/catalog/index.ts +4 -4
- package/src/core/policies/catalog/policy-catalog-entry.ts +199 -197
- package/src/core/policies/catalog/policy-catalog.ts +101 -101
- package/src/core/policies/catalog/policy-key.ts +51 -51
- package/src/core/policies/context/context-builder.ts +335 -313
- package/src/core/policies/context/context-registry.instance.ts +9 -9
- package/src/core/policies/context/context-registry.ts +61 -61
- package/src/core/policies/context/context-resolver.ts +12 -12
- package/src/core/policies/context/context-seed.ts +19 -15
- package/src/core/policies/context/index.ts +9 -9
- package/src/core/policies/context/path.ts +135 -128
- package/src/core/policies/defs/in-memory-policy-definition-repo.ts +56 -55
- package/src/core/policies/defs/index.ts +10 -10
- package/src/core/policies/defs/policy-definition-repository.ts +3 -3
- package/src/core/policies/defs/policy-definition.ts +143 -142
- package/src/core/policies/defs/policy-scope.ts +21 -19
- package/src/core/policies/defs/policy-semver.ts +51 -51
- package/src/core/policies/engines/compute-engine-registry.ts +43 -43
- package/src/core/policies/engines/compute-evaluator-registry.ts +20 -20
- package/src/core/policies/engines/compute-registry.ts +45 -45
- package/src/core/policies/engines/compute-types.ts +15 -15
- package/src/core/policies/engines/condition-evaluator-reporter.ts +17 -17
- package/src/core/policies/engines/gate-engine-registry.ts +27 -25
- package/src/core/policies/engines/gate-types.ts +28 -28
- package/src/core/policies/engines/index.ts +28 -28
- package/src/core/policies/engines/parse-compute-payload.ts +51 -45
- package/src/core/policies/engines/parse-gate-payload.ts +42 -42
- package/src/core/policies/engines/v1/compute/compute-engine-v1.ts +39 -37
- package/src/core/policies/engines/v1/compute/compute-payload.schema.ts +43 -41
- package/src/core/policies/engines/v1/compute/index.ts +6 -6
- package/src/core/policies/engines/v1/compute/resolve-decision-table-v1.ts +41 -39
- package/src/core/policies/engines/v1/condition-evaluator.ts +557 -504
- package/src/core/policies/engines/v1/condition-schema.ts +35 -35
- package/src/core/policies/engines/v1/condition-types.ts +21 -21
- package/src/core/policies/engines/v1/gate/gate-engine-v1.ts +134 -130
- package/src/core/policies/engines/v1/gate/gate-types-v1.ts +5 -5
- package/src/core/policies/engines/v1/gate/gate-v1-payload.schema.ts +32 -30
- package/src/core/policies/engines/v1/gate/index.ts +4 -4
- package/src/core/policies/engines/v1/index.ts +8 -8
- package/src/core/policies/index.ts +72 -72
- package/src/core/policies/package/policy-package.ts +3 -3
- package/src/core/policies/policy-ids.ts +25 -25
- package/src/core/policies/resolver/policy-resolver.ts +34 -34
- package/src/core/policies/service/index.ts +6 -6
- package/src/core/policies/service/policy-evaluation-error.ts +141 -133
- package/src/core/policies/service/policy-service.ts +432 -419
- package/src/core/policies/utils/date.ts +3 -3
- package/src/core/policies/utils/hash.ts +61 -61
- package/src/core/result/outcome.ts +131 -126
- package/src/core/result/result.ts +130 -127
- package/src/core/shared/aggregate-version.ts +5 -5
- package/src/core/shared/hashing.ts +18 -18
- package/src/core/shared/immutable.ts +29 -29
- package/src/core/shared/temporal-guards.ts +9 -9
- package/src/core/versioning/domain-event-contracts.ts +40 -40
- package/src/core/versioning/version.ts +19 -19
- package/src/examples/rulesets/entity/order-creation-rules-v1.ts +18 -18
- package/src/examples/rulesets/entity/order-invariants-v1.ts +31 -29
- package/src/examples/rulesets/entity/order-invariants-v2.ts +12 -12
- package/src/examples/rulesets/entity/order.ts +127 -127
- package/src/examples/rulesets/value-object/cpf-rules-v1.ts +41 -39
- package/src/examples/rulesets/value-object/cpf-rules-v2.ts +14 -12
- package/src/examples/rulesets/value-object/cpf.ts +24 -24
- package/src/examples/rulesets/value-object/customer.ts +42 -39
- package/src/examples/rulesets/value-object/person-name-rules-v1.ts +27 -27
- package/src/examples/rulesets/value-object/person-name-rules-v2.ts +13 -13
- package/src/examples/rulesets/value-object/person-name.ts +29 -26
- package/src/index.ts +2 -2
- package/src/version.ts +1 -1
|
@@ -2,390 +2,412 @@ import { Result } from "../../result/result";
|
|
|
2
2
|
|
|
3
3
|
import type { ContextResolverRegistry } from "./context-registry";
|
|
4
4
|
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
ContextResolverResilienceOptions,
|
|
6
|
+
ContextResolverRetryOptions,
|
|
7
|
+
ContextValueResolver,
|
|
8
8
|
} from "./context-resolver";
|
|
9
9
|
import type { ContextSeed } from "./context-seed";
|
|
10
10
|
import { PolicyContextPath } from "./path";
|
|
11
11
|
|
|
12
12
|
interface NormalizedContextResolverRetryOptions {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
readonly maxAttempts: number;
|
|
14
|
+
readonly initialDelayMs: number;
|
|
15
|
+
readonly maxDelayMs: number;
|
|
16
|
+
readonly backoffMultiplier: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
interface NormalizedContextResolverCircuitBreakerOptions {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
readonly failureThreshold: number;
|
|
21
|
+
readonly cooldownMs: number;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
interface NormalizedContextResolverResilienceOptions {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
readonly timeoutMs: number | null;
|
|
26
|
+
readonly retry: NormalizedContextResolverRetryOptions;
|
|
27
|
+
readonly circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
interface ContextResolverCircuitState {
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
readonly failureCount: number;
|
|
32
|
+
readonly openUntilMs: number | null;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export interface PolicyContextBuilderOptions {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
readonly defaultResolverResilience?: ContextResolverResilienceOptions;
|
|
37
|
+
readonly now?: () => number;
|
|
38
|
+
readonly sleep?: (delayMs: number) => Promise<void>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const DEFAULT_RESOLVER_RETRY_OPTIONS: NormalizedContextResolverRetryOptions = {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
maxAttempts: 1,
|
|
43
|
+
initialDelayMs: 25,
|
|
44
|
+
maxDelayMs: 1_000,
|
|
45
|
+
backoffMultiplier: 2,
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Builds a context object by resolving required paths from a seed.
|
|
50
50
|
*/
|
|
51
51
|
export class PolicyContextBuilder {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
52
|
+
private readonly circuitStates = new Map<
|
|
53
|
+
string,
|
|
54
|
+
ContextResolverCircuitState
|
|
55
|
+
>();
|
|
56
|
+
private readonly defaultResolverResilience?: ContextResolverResilienceOptions;
|
|
57
|
+
private readonly now: () => number;
|
|
58
|
+
private readonly sleep: (delayMs: number) => Promise<void>;
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
private readonly registry: ContextResolverRegistry,
|
|
62
|
+
options: PolicyContextBuilderOptions = {},
|
|
63
|
+
) {
|
|
64
|
+
this.defaultResolverResilience = options.defaultResolverResilience;
|
|
65
|
+
this.now = options.now ?? Date.now;
|
|
66
|
+
this.sleep =
|
|
67
|
+
options.sleep ??
|
|
68
|
+
(async (delayMs) => {
|
|
69
|
+
if (delayMs <= 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await new Promise<void>((resolve) => {
|
|
74
|
+
setTimeout(resolve, delayMs);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private static clampPositiveInteger(
|
|
80
|
+
value: number | undefined,
|
|
81
|
+
fallback: number,
|
|
82
|
+
): number {
|
|
83
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
84
|
+
return fallback;
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
|
|
74
|
-
setTimeout(resolve, delayMs);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
private static clampPositiveInteger(
|
|
80
|
-
value: number | undefined,
|
|
81
|
-
fallback: number,
|
|
82
|
-
): number {
|
|
83
|
-
if (value === undefined || !Number.isFinite(value)) {
|
|
84
|
-
return fallback;
|
|
87
|
+
return Math.max(1, Math.trunc(value));
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
private static clampNonNegativeInteger(
|
|
91
|
+
value: number | undefined,
|
|
92
|
+
fallback: number,
|
|
93
|
+
): number {
|
|
94
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
95
|
+
return fallback;
|
|
96
|
+
}
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
value: number | undefined,
|
|
92
|
-
fallback: number,
|
|
93
|
-
): number {
|
|
94
|
-
if (value === undefined || !Number.isFinite(value)) {
|
|
95
|
-
return fallback;
|
|
98
|
+
return Math.max(0, Math.trunc(value));
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
|
|
99
|
-
|
|
101
|
+
private static normalizeTimeoutMs(
|
|
102
|
+
value: number | undefined,
|
|
103
|
+
): number | null {
|
|
104
|
+
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
if (value === undefined || !Number.isFinite(value) || value <= 0) {
|
|
103
|
-
return null;
|
|
108
|
+
return Math.trunc(value);
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
private static mergeRetryOptions(
|
|
112
|
+
defaults: ContextResolverRetryOptions | false | undefined,
|
|
113
|
+
overrides: ContextResolverRetryOptions | false | undefined,
|
|
114
|
+
): ContextResolverRetryOptions | false | undefined {
|
|
115
|
+
if (overrides === false) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
108
118
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
119
|
+
if (overrides !== undefined) {
|
|
120
|
+
return {
|
|
121
|
+
...(defaults === false || defaults === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: defaults),
|
|
124
|
+
...overrides,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
...(defaults === false || defaults === undefined ? {} : defaults),
|
|
120
|
-
...overrides,
|
|
121
|
-
};
|
|
128
|
+
return defaults;
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
private static mergeCircuitBreakerOptions(
|
|
132
|
+
defaults:
|
|
133
|
+
| ContextResolverResilienceOptions["circuitBreaker"]
|
|
134
|
+
| undefined,
|
|
135
|
+
overrides:
|
|
136
|
+
| ContextResolverResilienceOptions["circuitBreaker"]
|
|
137
|
+
| undefined,
|
|
138
|
+
): ContextResolverResilienceOptions["circuitBreaker"] | undefined {
|
|
139
|
+
if (overrides === false) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
126
142
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
if (overrides !== undefined) {
|
|
144
|
+
return {
|
|
145
|
+
...(defaults === false || defaults === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: defaults),
|
|
148
|
+
...overrides,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
134
151
|
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
...(defaults === false || defaults === undefined ? {} : defaults),
|
|
138
|
-
...overrides,
|
|
139
|
-
};
|
|
152
|
+
return defaults;
|
|
140
153
|
}
|
|
141
154
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
retryOptions?.backoffMultiplier,
|
|
181
|
-
DEFAULT_RESOLVER_RETRY_OPTIONS.backoffMultiplier,
|
|
182
|
-
),
|
|
183
|
-
},
|
|
184
|
-
circuitBreaker:
|
|
185
|
-
circuitBreakerOptions === undefined
|
|
186
|
-
? null
|
|
187
|
-
: {
|
|
188
|
-
failureThreshold: PolicyContextBuilder.clampPositiveInteger(
|
|
189
|
-
circuitBreakerOptions.failureThreshold,
|
|
190
|
-
5,
|
|
191
|
-
),
|
|
192
|
-
cooldownMs: PolicyContextBuilder.clampPositiveInteger(
|
|
193
|
-
circuitBreakerOptions.cooldownMs,
|
|
194
|
-
30_000,
|
|
195
|
-
),
|
|
155
|
+
private resolveResilienceOptions(
|
|
156
|
+
resolver: ContextValueResolver,
|
|
157
|
+
): NormalizedContextResolverResilienceOptions {
|
|
158
|
+
const defaults = this.defaultResolverResilience;
|
|
159
|
+
const override = resolver.resilience;
|
|
160
|
+
const retry = PolicyContextBuilder.mergeRetryOptions(
|
|
161
|
+
defaults?.retry,
|
|
162
|
+
override?.retry,
|
|
163
|
+
);
|
|
164
|
+
const circuitBreaker = PolicyContextBuilder.mergeCircuitBreakerOptions(
|
|
165
|
+
defaults?.circuitBreaker,
|
|
166
|
+
override?.circuitBreaker,
|
|
167
|
+
);
|
|
168
|
+
const retryOptions = retry === false ? undefined : retry;
|
|
169
|
+
const circuitBreakerOptions =
|
|
170
|
+
circuitBreaker === false ? undefined : circuitBreaker;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
timeoutMs: PolicyContextBuilder.normalizeTimeoutMs(
|
|
174
|
+
override?.timeoutMs ?? defaults?.timeoutMs,
|
|
175
|
+
),
|
|
176
|
+
retry: {
|
|
177
|
+
maxAttempts: PolicyContextBuilder.clampPositiveInteger(
|
|
178
|
+
retryOptions?.maxAttempts,
|
|
179
|
+
DEFAULT_RESOLVER_RETRY_OPTIONS.maxAttempts,
|
|
180
|
+
),
|
|
181
|
+
initialDelayMs: PolicyContextBuilder.clampNonNegativeInteger(
|
|
182
|
+
retryOptions?.initialDelayMs,
|
|
183
|
+
DEFAULT_RESOLVER_RETRY_OPTIONS.initialDelayMs,
|
|
184
|
+
),
|
|
185
|
+
maxDelayMs: PolicyContextBuilder.clampNonNegativeInteger(
|
|
186
|
+
retryOptions?.maxDelayMs,
|
|
187
|
+
DEFAULT_RESOLVER_RETRY_OPTIONS.maxDelayMs,
|
|
188
|
+
),
|
|
189
|
+
backoffMultiplier: PolicyContextBuilder.clampPositiveInteger(
|
|
190
|
+
retryOptions?.backoffMultiplier,
|
|
191
|
+
DEFAULT_RESOLVER_RETRY_OPTIONS.backoffMultiplier,
|
|
192
|
+
),
|
|
196
193
|
},
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
private isCircuitOpen(
|
|
215
|
-
path: string,
|
|
216
|
-
circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null,
|
|
217
|
-
): boolean {
|
|
218
|
-
if (circuitBreaker === null) {
|
|
219
|
-
return false;
|
|
194
|
+
circuitBreaker:
|
|
195
|
+
circuitBreakerOptions === undefined
|
|
196
|
+
? null
|
|
197
|
+
: {
|
|
198
|
+
failureThreshold:
|
|
199
|
+
PolicyContextBuilder.clampPositiveInteger(
|
|
200
|
+
circuitBreakerOptions.failureThreshold,
|
|
201
|
+
5,
|
|
202
|
+
),
|
|
203
|
+
cooldownMs: PolicyContextBuilder.clampPositiveInteger(
|
|
204
|
+
circuitBreakerOptions.cooldownMs,
|
|
205
|
+
30_000,
|
|
206
|
+
),
|
|
207
|
+
},
|
|
208
|
+
};
|
|
220
209
|
}
|
|
221
210
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
211
|
+
private getRetryDelayMs(
|
|
212
|
+
attempt: number,
|
|
213
|
+
retry: NormalizedContextResolverRetryOptions,
|
|
214
|
+
): number {
|
|
215
|
+
return Math.min(
|
|
216
|
+
retry.initialDelayMs * retry.backoffMultiplier ** (attempt - 1),
|
|
217
|
+
retry.maxDelayMs,
|
|
218
|
+
);
|
|
225
219
|
}
|
|
226
220
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return false;
|
|
221
|
+
private resetCircuit(path: string): void {
|
|
222
|
+
this.circuitStates.delete(path);
|
|
230
223
|
}
|
|
231
224
|
|
|
232
|
-
|
|
233
|
-
|
|
225
|
+
private isCircuitOpen(
|
|
226
|
+
path: string,
|
|
227
|
+
circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null,
|
|
228
|
+
): boolean {
|
|
229
|
+
if (circuitBreaker === null) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
234
232
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (circuitBreaker === null) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
233
|
+
const state = this.circuitStates.get(path);
|
|
234
|
+
if (state === undefined || state.openUntilMs === null) {
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
242
237
|
|
|
243
|
-
|
|
244
|
-
|
|
238
|
+
if (state.openUntilMs <= this.now()) {
|
|
239
|
+
this.resetCircuit(path);
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
245
242
|
|
|
246
|
-
|
|
247
|
-
this.circuitStates.set(path, {
|
|
248
|
-
failureCount: 0,
|
|
249
|
-
openUntilMs: this.now() + circuitBreaker.cooldownMs,
|
|
250
|
-
});
|
|
251
|
-
return;
|
|
243
|
+
return true;
|
|
252
244
|
}
|
|
253
245
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
private recordResolverFailure(
|
|
247
|
+
path: string,
|
|
248
|
+
circuitBreaker: NormalizedContextResolverCircuitBreakerOptions | null,
|
|
249
|
+
): void {
|
|
250
|
+
if (circuitBreaker === null) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
259
253
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
254
|
+
const currentFailureCount =
|
|
255
|
+
this.circuitStates.get(path)?.failureCount ?? 0;
|
|
256
|
+
const nextFailureCount = currentFailureCount + 1;
|
|
264
257
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
): Promise<Result<unknown, string>> {
|
|
273
|
-
const execute = async (): Promise<Result<unknown, string>> => {
|
|
274
|
-
try {
|
|
275
|
-
return await resolver.resolve(seed);
|
|
276
|
-
} catch (error) {
|
|
277
|
-
return Result.err(
|
|
278
|
-
PolicyContextBuilder.describeThrownResolverError(error),
|
|
279
|
-
);
|
|
280
|
-
}
|
|
281
|
-
};
|
|
258
|
+
if (nextFailureCount >= circuitBreaker.failureThreshold) {
|
|
259
|
+
this.circuitStates.set(path, {
|
|
260
|
+
failureCount: 0,
|
|
261
|
+
openUntilMs: this.now() + circuitBreaker.cooldownMs,
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
282
265
|
|
|
283
|
-
|
|
284
|
-
|
|
266
|
+
this.circuitStates.set(path, {
|
|
267
|
+
failureCount: nextFailureCount,
|
|
268
|
+
openUntilMs: null,
|
|
269
|
+
});
|
|
285
270
|
}
|
|
286
271
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
timeoutId = setTimeout(() => {
|
|
294
|
-
resolve(Result.err(`timed out after ${timeoutMs}ms`));
|
|
295
|
-
}, timeoutMs);
|
|
296
|
-
}),
|
|
297
|
-
]);
|
|
298
|
-
} finally {
|
|
299
|
-
if (timeoutId !== undefined) {
|
|
300
|
-
clearTimeout(timeoutId);
|
|
301
|
-
}
|
|
272
|
+
private static describeThrownResolverError(error: unknown): string {
|
|
273
|
+
if (error instanceof Error) {
|
|
274
|
+
return `threw ${error.name}: ${error.message}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return `threw ${String(error)}`;
|
|
302
278
|
}
|
|
303
|
-
}
|
|
304
279
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
280
|
+
private async resolveOnce(
|
|
281
|
+
resolver: ContextValueResolver,
|
|
282
|
+
seed: ContextSeed,
|
|
283
|
+
timeoutMs: number | null,
|
|
284
|
+
): Promise<Result<unknown, string>> {
|
|
285
|
+
const execute = async (): Promise<Result<unknown, string>> => {
|
|
286
|
+
try {
|
|
287
|
+
return await resolver.resolve(seed);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
return Result.err(
|
|
290
|
+
PolicyContextBuilder.describeThrownResolverError(error),
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (timeoutMs === null) {
|
|
296
|
+
return execute();
|
|
297
|
+
}
|
|
311
298
|
|
|
312
|
-
|
|
313
|
-
|
|
299
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
return await Promise.race([
|
|
303
|
+
execute(),
|
|
304
|
+
new Promise<Result<unknown, string>>((resolve) => {
|
|
305
|
+
timeoutId = setTimeout(() => {
|
|
306
|
+
resolve(Result.err(`timed out after ${timeoutMs}ms`));
|
|
307
|
+
}, timeoutMs);
|
|
308
|
+
}),
|
|
309
|
+
]);
|
|
310
|
+
} finally {
|
|
311
|
+
if (timeoutId !== undefined) {
|
|
312
|
+
clearTimeout(timeoutId);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
314
315
|
}
|
|
315
316
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
resilience.timeoutMs,
|
|
323
|
-
);
|
|
317
|
+
private async resolveWithResilience(
|
|
318
|
+
path: string,
|
|
319
|
+
resolver: ContextValueResolver,
|
|
320
|
+
seed: ContextSeed,
|
|
321
|
+
): Promise<Result<unknown, string>> {
|
|
322
|
+
const resilience = this.resolveResilienceOptions(resolver);
|
|
324
323
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
324
|
+
if (this.isCircuitOpen(path, resilience.circuitBreaker)) {
|
|
325
|
+
return Result.err("circuit breaker is open");
|
|
326
|
+
}
|
|
329
327
|
|
|
330
|
-
|
|
328
|
+
let lastError = "resolver failed";
|
|
329
|
+
|
|
330
|
+
for (
|
|
331
|
+
let attempt = 1;
|
|
332
|
+
attempt <= resilience.retry.maxAttempts;
|
|
333
|
+
attempt++
|
|
334
|
+
) {
|
|
335
|
+
const result = await this.resolveOnce(
|
|
336
|
+
resolver,
|
|
337
|
+
seed,
|
|
338
|
+
resilience.timeoutMs,
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
if (result.isOk()) {
|
|
342
|
+
this.resetCircuit(path);
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
lastError = result.errorOrNull() ?? lastError;
|
|
347
|
+
|
|
348
|
+
if (attempt < resilience.retry.maxAttempts) {
|
|
349
|
+
await this.sleep(
|
|
350
|
+
this.getRetryDelayMs(attempt, resilience.retry),
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
331
354
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
355
|
+
this.recordResolverFailure(path, resilience.circuitBreaker);
|
|
356
|
+
return Result.err(lastError);
|
|
335
357
|
}
|
|
336
358
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Resolves all required context paths and returns a flat context object.
|
|
361
|
+
* If any required path cannot be resolved, returns an error.
|
|
362
|
+
*/
|
|
363
|
+
async build(
|
|
364
|
+
requirements: readonly string[],
|
|
365
|
+
seed: ContextSeed,
|
|
366
|
+
): Promise<Result<Record<string, unknown>, string>> {
|
|
367
|
+
const context: Record<string, unknown> = {};
|
|
368
|
+
|
|
369
|
+
for (const path of requirements) {
|
|
370
|
+
const resolver = this.registry.get(path);
|
|
371
|
+
if (!resolver) {
|
|
372
|
+
return Result.err(`Missing resolver for path "${path}"`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const result = await this.resolveWithResilience(
|
|
376
|
+
path,
|
|
377
|
+
resolver,
|
|
378
|
+
seed,
|
|
379
|
+
);
|
|
380
|
+
if (result.isErr()) {
|
|
381
|
+
return Result.err(
|
|
382
|
+
`Resolver error for path "${path}": ${result.errorOrNull()}`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const setPathResult = PolicyContextPath.set(
|
|
387
|
+
context,
|
|
388
|
+
path,
|
|
389
|
+
result.getOrNull(),
|
|
390
|
+
);
|
|
391
|
+
if (setPathResult.isErr()) {
|
|
392
|
+
return Result.err(setPathResult.errorOrNull()!);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
373
395
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
396
|
+
// Final validation: ensure every required path resolved to a defined value.
|
|
397
|
+
// `undefined` is rejected explicitly because resolvers that "succeed" with
|
|
398
|
+
// undefined indicate an unresolved field, not a present-but-empty one.
|
|
399
|
+
const missing = requirements.filter((path) => {
|
|
400
|
+
const result = PolicyContextPath.getOrAbsent(context, path);
|
|
401
|
+
return result.isErr() || result.getOrNull() === undefined;
|
|
402
|
+
});
|
|
403
|
+
if (missing.length > 0) {
|
|
404
|
+
return Result.err(
|
|
405
|
+
`Context missing required paths after resolution: ${missing.join(
|
|
406
|
+
", ",
|
|
407
|
+
)}`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
388
410
|
|
|
389
|
-
|
|
390
|
-
|
|
411
|
+
return Result.ok(context);
|
|
412
|
+
}
|
|
391
413
|
}
|