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