@classytic/primitives 0.19.0 → 0.21.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/CHANGELOG.md +920 -751
- package/README.md +160 -159
- package/dist/approval.d.mts +50 -11
- package/dist/approval.mjs +44 -10
- package/dist/bank-transaction.d.mts +1 -2
- package/dist/{brand-LLXqoh5J.d.mts → brand-Lj_75Y_r.d.mts} +6 -6
- package/dist/brand.d.mts +1 -1
- package/dist/cadence-5X6TiD49.d.mts +107 -0
- package/dist/cadence.d.mts +1 -92
- package/dist/cadence.mjs +135 -6
- package/dist/calendar-DJWSbcgE.d.mts +46 -0
- package/dist/calendar.d.mts +1 -45
- package/dist/calendar.mjs +2 -2
- package/dist/canonical.mjs +17 -0
- package/dist/condition.mjs +2 -2
- package/dist/context.d.mts +28 -2
- package/dist/currency.d.mts +2 -2
- package/dist/currency.mjs +134 -19
- package/dist/environment.d.mts +85 -0
- package/dist/environment.mjs +48 -0
- package/dist/event-infra.d.mts +16 -32
- package/dist/event-infra.mjs +16 -46
- package/dist/idempotency.d.mts +263 -0
- package/dist/idempotency.mjs +226 -0
- package/dist/identity-verification.d.mts +0 -1
- package/dist/memory-outbox.d.mts +63 -0
- package/dist/memory-outbox.mjs +172 -0
- package/dist/mixin.d.mts +1 -1
- package/dist/monetization.d.mts +1 -2
- package/dist/money-CfAvfNiM.d.mts +346 -0
- package/dist/money.d.mts +1 -140
- package/dist/money.mjs +6 -15
- package/dist/otp.mjs +1 -1
- package/dist/outbox.d.mts +49 -2
- package/dist/outbox.mjs +64 -1
- package/dist/payment-allocation-status.d.mts +10 -4
- package/dist/payment-allocation-status.mjs +14 -0
- package/dist/payment-events.d.mts +132 -5
- package/dist/payment-events.mjs +34 -2
- package/dist/payment-gateway.d.mts +211 -8
- package/dist/payment-gateway.mjs +18 -1
- package/dist/payment-method-kind.d.mts +23 -11
- package/dist/payment-method-kind.mjs +17 -0
- package/dist/period.d.mts +149 -7
- package/dist/period.mjs +316 -5
- package/dist/person-parse.d.mts +81 -0
- package/dist/person-parse.mjs +109 -0
- package/dist/phone.d.mts +0 -1
- package/dist/phone.mjs +3 -3
- package/dist/proration.d.mts +2 -3
- package/dist/proration.mjs +18 -6
- package/dist/retention.mjs +3 -3
- package/dist/shelf-life.mjs +1 -1
- package/dist/sla-policy.d.mts +1 -2
- package/dist/state-diagram.d.mts +0 -1
- package/dist/state-diagram.mjs +1 -1
- package/dist/state-machine.mjs +13 -0
- package/dist/suspension.d.mts +6 -4
- package/dist/suspension.mjs +3 -2
- package/dist/timezone-CxUjxmRU.d.mts +113 -0
- package/dist/timezone.d.mts +2 -100
- package/dist/timezone.mjs +38 -3
- package/dist/unit-cost-rate.d.mts +1 -3
- package/package.json +249 -232
- package/dist/currency-BCJnJBlb.d.mts +0 -119
package/dist/currency.mjs
CHANGED
|
@@ -26,11 +26,20 @@ const CURRENCIES = {
|
|
|
26
26
|
IDR: "IDR"
|
|
27
27
|
};
|
|
28
28
|
/**
|
|
29
|
-
* Number of minor units per major unit
|
|
29
|
+
* Number of minor units per major unit, by ISO 4217 code.
|
|
30
30
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* **This table must list EVERY currency whose exponent is not 2.** ISO 4217
|
|
32
|
+
* defaults to 2 decimals, and {@link minorUnitFactor} falls back to 100 for a
|
|
33
|
+
* code it does not know — which is correct for the ~150 two-decimal currencies
|
|
34
|
+
* and catastrophically wrong for the others. A missing zero-decimal entry does
|
|
35
|
+
* not throw: it makes `fromMajor(1500, 'GNF')` store 150 000 minor units, a
|
|
36
|
+
* 100× error that reads as a plausible number all the way to the gateway.
|
|
37
|
+
*
|
|
38
|
+
* So the exceptions are enumerated EXHAUSTIVELY (all 0-, 3- and 4-decimal
|
|
39
|
+
* codes), leaving the `?? 100` fallback reachable only by a code that is not a
|
|
40
|
+
* real ISO 4217 currency — a typo, a lowercased code, or a placeholder. The
|
|
41
|
+
* two-decimal entries below are redundant with the default and kept only as
|
|
42
|
+
* documentation of the common set.
|
|
34
43
|
*/
|
|
35
44
|
const MINOR_UNIT_FACTOR = {
|
|
36
45
|
USD: 100,
|
|
@@ -53,13 +62,31 @@ const MINOR_UNIT_FACTOR = {
|
|
|
53
62
|
HKD: 100,
|
|
54
63
|
THB: 100,
|
|
55
64
|
IDR: 100,
|
|
65
|
+
BIF: 1,
|
|
66
|
+
CLP: 1,
|
|
67
|
+
DJF: 1,
|
|
68
|
+
GNF: 1,
|
|
69
|
+
ISK: 1,
|
|
70
|
+
KMF: 1,
|
|
56
71
|
KRW: 1,
|
|
72
|
+
PYG: 1,
|
|
73
|
+
RWF: 1,
|
|
74
|
+
UGX: 1,
|
|
75
|
+
UYI: 1,
|
|
57
76
|
VND: 1,
|
|
77
|
+
VUV: 1,
|
|
78
|
+
XAF: 1,
|
|
79
|
+
XOF: 1,
|
|
80
|
+
XPF: 1,
|
|
58
81
|
BHD: 1e3,
|
|
59
|
-
|
|
82
|
+
IQD: 1e3,
|
|
60
83
|
JOD: 1e3,
|
|
84
|
+
KWD: 1e3,
|
|
85
|
+
LYD: 1e3,
|
|
61
86
|
OMR: 1e3,
|
|
62
|
-
TND: 1e3
|
|
87
|
+
TND: 1e3,
|
|
88
|
+
CLF: 1e4,
|
|
89
|
+
UYW: 1e4
|
|
63
90
|
};
|
|
64
91
|
/**
|
|
65
92
|
* ISO 4217 format — 3 uppercase letters. Exported so consumers can embed
|
|
@@ -78,26 +105,107 @@ const CURRENCY_PATTERN = /^[A-Z]{3}$/;
|
|
|
78
105
|
function toCurrencyCode(value) {
|
|
79
106
|
return CURRENCY_PATTERN.test(value) ? value : null;
|
|
80
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Validate + brand, or throw. The constructor form — same relationship to
|
|
110
|
+
* {@link toCurrencyCode} that `civilDate` has to `isCivilDate`.
|
|
111
|
+
*
|
|
112
|
+
* `Money` carries a {@link CurrencyCode}, not a bare string, and this is where
|
|
113
|
+
* an untrusted value earns the brand. Rejecting `'jpy'` here matters: lowercase
|
|
114
|
+
* misses {@link MINOR_UNIT_FACTOR}, so it silently becomes a 2-decimal currency
|
|
115
|
+
* and every JPY amount is off by 100×.
|
|
116
|
+
*/
|
|
117
|
+
function currencyCode(value) {
|
|
118
|
+
const code = toCurrencyCode(value);
|
|
119
|
+
if (code === null) throw new InvalidCurrencyCodeError(value);
|
|
120
|
+
return code;
|
|
121
|
+
}
|
|
122
|
+
var InvalidCurrencyCodeError = class extends Error {
|
|
123
|
+
name = "InvalidCurrencyCodeError";
|
|
124
|
+
value;
|
|
125
|
+
constructor(value) {
|
|
126
|
+
super(`Invalid ISO 4217 currency code '${value}' — expected three uppercase letters. A code that misses the minor-unit table is assumed to have two decimals, which is a 100× error for JPY-style currencies.`);
|
|
127
|
+
this.value = value;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Raised when an operation combines two currencies.
|
|
132
|
+
*
|
|
133
|
+
* Declared HERE rather than in `/money` so the FX seam below can throw it
|
|
134
|
+
* without a runtime import back into `/money` (which imports
|
|
135
|
+
* {@link minorUnitFactor} from this file). `/money` re-exports it, so
|
|
136
|
+
* `import { CurrencyMismatchError } from '@classytic/primitives/money'` still
|
|
137
|
+
* resolves.
|
|
138
|
+
*/
|
|
139
|
+
var CurrencyMismatchError = class extends Error {
|
|
140
|
+
name = "CurrencyMismatchError";
|
|
141
|
+
left;
|
|
142
|
+
right;
|
|
143
|
+
constructor(left, right) {
|
|
144
|
+
super(`Currency mismatch: ${left} vs ${right}`);
|
|
145
|
+
this.left = left;
|
|
146
|
+
this.right = right;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
81
149
|
/** Type predicate for `unknown` input. */
|
|
82
150
|
function isCurrencyCode(value) {
|
|
83
151
|
return typeof value === "string" && CURRENCY_PATTERN.test(value);
|
|
84
152
|
}
|
|
85
153
|
/**
|
|
86
|
-
* Minor units per major unit (
|
|
87
|
-
*
|
|
154
|
+
* Minor units per major unit (100 for USD, 1 for JPY, 1000 for KWD, 10 000 for CLF).
|
|
155
|
+
*
|
|
156
|
+
* Falls back to 100 for an unlisted code — the ISO 4217 default exponent. Because
|
|
157
|
+
* {@link MINOR_UNIT_FACTOR} enumerates every non-2-decimal currency, that fallback
|
|
158
|
+
* is now only reached by codes that are not ISO 4217 currencies at all; use
|
|
159
|
+
* {@link isKnownCurrency} at a config/boot boundary if you need to reject those
|
|
160
|
+
* rather than assume two decimals for them.
|
|
88
161
|
*/
|
|
89
162
|
function minorUnitFactor(currency) {
|
|
90
163
|
return MINOR_UNIT_FACTOR[currency] ?? 100;
|
|
91
164
|
}
|
|
92
165
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
166
|
+
* Does {@link MINOR_UNIT_FACTOR} carry an explicit exponent for this code?
|
|
167
|
+
*
|
|
168
|
+
* `false` means {@link minorUnitFactor} would ASSUME two decimals. That
|
|
169
|
+
* assumption is right for an ordinary ISO 4217 code and wrong by 100× for a
|
|
170
|
+
* mistyped or lowercased one (`'jpy'`), so a boot-time config validator that
|
|
171
|
+
* accepts an operator-supplied currency should gate on this instead of letting
|
|
172
|
+
* the default answer for it.
|
|
98
173
|
*/
|
|
99
|
-
function
|
|
100
|
-
return
|
|
174
|
+
function isKnownCurrency(currency) {
|
|
175
|
+
return Object.hasOwn(MINOR_UNIT_FACTOR, currency);
|
|
176
|
+
}
|
|
177
|
+
function roundMinor(raw, mode) {
|
|
178
|
+
if (mode === "floor") return Math.floor(raw);
|
|
179
|
+
if (mode === "ceil") return Math.ceil(raw);
|
|
180
|
+
return Math.round(Math.abs(raw)) * (raw < 0 ? -1 : 1);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Apply an {@link FxSnapshot} to a source-currency amount.
|
|
184
|
+
*
|
|
185
|
+
* ## Why this takes `Money` and not a number
|
|
186
|
+
*
|
|
187
|
+
* It used to take a bare `number`, which meant nothing could check the amount
|
|
188
|
+
* was denominated in `fx.sourceCurrency`. Applying a USD→BDT snapshot to a EUR
|
|
189
|
+
* amount type-checked, ran, and returned a plausible wrong number — the
|
|
190
|
+
* arithmetic is identical, only the meaning is wrong, so no test that asserts a
|
|
191
|
+
* total would notice. Taking `Money` lets the seam enforce its own
|
|
192
|
+
* precondition: a mismatch throws {@link CurrencyMismatchError}.
|
|
193
|
+
*
|
|
194
|
+
* ## Minor-unit exponents are handled
|
|
195
|
+
*
|
|
196
|
+
* The rate is quoted in MAJOR units (1 source = `rate` base), while `Money`
|
|
197
|
+
* holds minor units, and the two currencies need not share an exponent
|
|
198
|
+
* (USD has 2, JPY has 0). The conversion therefore goes
|
|
199
|
+
* `minor → major → ×rate → base minor`, not a naive `amount × rate`, which is
|
|
200
|
+
* off by 100× for any pair whose exponents differ.
|
|
201
|
+
*/
|
|
202
|
+
function convertWithSnapshot(source, fx, options = {}) {
|
|
203
|
+
if (source.currency !== fx.sourceCurrency) throw new CurrencyMismatchError(source.currency, fx.sourceCurrency);
|
|
204
|
+
if (!Number.isFinite(fx.rate)) throw new TypeError(`FxSnapshot.rate must be finite, got ${fx.rate}`);
|
|
205
|
+
return {
|
|
206
|
+
amount: roundMinor(source.amount / minorUnitFactor(fx.sourceCurrency) * fx.rate * minorUnitFactor(fx.baseCurrency), options.rounding ?? "half-away-from-zero"),
|
|
207
|
+
currency: currencyCode(fx.baseCurrency)
|
|
208
|
+
};
|
|
101
209
|
}
|
|
102
210
|
/**
|
|
103
211
|
* Reverse an {@link FxSnapshot} — reconstruct the source-currency amount
|
|
@@ -105,10 +213,17 @@ function convertWithSnapshot(sourceAmount, fx) {
|
|
|
105
213
|
* currency". Not round-trip safe under rounding; prefer storing the
|
|
106
214
|
* original source amount alongside the base amount when audit fidelity
|
|
107
215
|
* matters.
|
|
216
|
+
*
|
|
217
|
+
* Symmetrically guarded: a base amount that is not in `fx.baseCurrency` throws
|
|
218
|
+
* rather than dividing by a rate that does not describe it.
|
|
108
219
|
*/
|
|
109
|
-
function reverseWithSnapshot(
|
|
110
|
-
if (
|
|
111
|
-
|
|
220
|
+
function reverseWithSnapshot(base, fx, options = {}) {
|
|
221
|
+
if (base.currency !== fx.baseCurrency) throw new CurrencyMismatchError(base.currency, fx.baseCurrency);
|
|
222
|
+
if (fx.rate === 0 || !Number.isFinite(fx.rate)) throw new TypeError(`FxSnapshot.rate must be a non-zero finite number, got ${fx.rate}`);
|
|
223
|
+
return {
|
|
224
|
+
amount: roundMinor(base.amount / minorUnitFactor(fx.baseCurrency) / fx.rate * minorUnitFactor(fx.sourceCurrency), options.rounding ?? "half-away-from-zero"),
|
|
225
|
+
currency: currencyCode(fx.sourceCurrency)
|
|
226
|
+
};
|
|
112
227
|
}
|
|
113
228
|
/**
|
|
114
229
|
* Structural validation — checks shape only. Does NOT verify the rate is
|
|
@@ -121,4 +236,4 @@ function isFxSnapshot(value) {
|
|
|
121
236
|
return typeof v.sourceCurrency === "string" && typeof v.baseCurrency === "string" && typeof v.rate === "number" && Number.isFinite(v.rate) && v.rate > 0 && v.snapshotAt instanceof Date;
|
|
122
237
|
}
|
|
123
238
|
//#endregion
|
|
124
|
-
export { CURRENCIES, CURRENCY_PATTERN, MINOR_UNIT_FACTOR, convertWithSnapshot, isCurrencyCode, isFxSnapshot, minorUnitFactor, reverseWithSnapshot, toCurrencyCode };
|
|
239
|
+
export { CURRENCIES, CURRENCY_PATTERN, CurrencyMismatchError, InvalidCurrencyCodeError, MINOR_UNIT_FACTOR, convertWithSnapshot, currencyCode, isCurrencyCode, isFxSnapshot, isKnownCurrency, minorUnitFactor, reverseWithSnapshot, toCurrencyCode };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//#region src/composition/environment.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* `NODE_ENV` classification — the ONE answer to "which environment is this?".
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is a primitive and not four copies
|
|
6
|
+
*
|
|
7
|
+
* `NODE_ENV` has two accepted spellings per bucket in practice — `production` OR `prod`, `test` OR
|
|
8
|
+
* `qa` — so a local comparison against one literal is wrong for the other, and it is wrong in the
|
|
9
|
+
* expensive direction. Almost every call site computes a NEGATIVE (`!== 'production'`,
|
|
10
|
+
* `isProduction === false`), so a missed spelling does not close a gate, it OPENS one.
|
|
11
|
+
*
|
|
12
|
+
* That is not hypothetical. Every item below was found in shipped code, and **not one of them
|
|
13
|
+
* threw**:
|
|
14
|
+
*
|
|
15
|
+
* | site | what `NODE_ENV=prod` did |
|
|
16
|
+
* |---|---|
|
|
17
|
+
* | `arc/plugins/errorHandler.ts` | returned the RAW thrown message of a 500 to the client |
|
|
18
|
+
* | `arc/auth/sessionManager.ts` | issued session cookies **without `Secure`** |
|
|
19
|
+
* | gym: pos / revenue / membership / order / catalog / attendance | permitted NON-TRANSACTIONAL money writes in production |
|
|
20
|
+
* | gym `app.ts` | selected arc's DEVELOPMENT preset in production |
|
|
21
|
+
* | be-prod `media.engine.ts` | enabled the in-memory media driver fallback — uploads 200, files gone on restart |
|
|
22
|
+
* | be-prod `contract.ports.ts`, `crm.module.ts` | non-atomic receivable / lead-conversion writes |
|
|
23
|
+
* | be-prod `logger.ts` | pino's pretty TRANSPORT in production |
|
|
24
|
+
* | be-prod `guest-order.resource.ts` | rate limiter armed under `qa` and nowhere else |
|
|
25
|
+
*
|
|
26
|
+
* `qa` had the mirror problem: classified as development by one host and test by another, so
|
|
27
|
+
* multi-boot support silently vanished on a QA run.
|
|
28
|
+
*
|
|
29
|
+
* **This logic already existed four times** — be-prod `classifyEnv`, gym `businessMode()`, arc's own
|
|
30
|
+
* CLI config template (as a `z.preprocess`), and implicitly in arc's four raw comparison sites. All
|
|
31
|
+
* four agreed on the spellings and the buckets, which is exactly why collapsing them is mechanical
|
|
32
|
+
* rather than a behaviour change. A fifth copy was one new vertical away.
|
|
33
|
+
*
|
|
34
|
+
* ## Why primitives owns it
|
|
35
|
+
*
|
|
36
|
+
* arc's own ownership rule: *primitives owns pure cross-package contracts, arc owns runtime.* This
|
|
37
|
+
* is a pure total function with no dependencies, and its consumers are not all arc apps — kernels,
|
|
38
|
+
* workers, CLIs and the spine need the same answer without pulling in a web framework. primitives
|
|
39
|
+
* is already a required peer of arc, so every arc host has it at no additional cost.
|
|
40
|
+
*
|
|
41
|
+
* ## Prefer an EXPLICIT signal where one exists
|
|
42
|
+
*
|
|
43
|
+
* If a caller already has a declared environment — arc's `preset`, a `deployment.config`, an
|
|
44
|
+
* injected flag — **use that instead**. It is a specific instruction, and this function reads an
|
|
45
|
+
* ambient default. A specific setting must never be overridden by a general one.
|
|
46
|
+
*/
|
|
47
|
+
/** The three buckets everything downstream reasons about. Mutually exclusive and total. */
|
|
48
|
+
type EnvClass = 'development' | 'test' | 'production';
|
|
49
|
+
/**
|
|
50
|
+
* Classify a raw `NODE_ENV` value.
|
|
51
|
+
*
|
|
52
|
+
* Total by construction — every input yields exactly one bucket, so a caller can never observe a
|
|
53
|
+
* fourth state or `undefined`.
|
|
54
|
+
*
|
|
55
|
+
* ## Whitespace IS trimmed, and that is a deliberate divergence from the four copies
|
|
56
|
+
*
|
|
57
|
+
* None of the previous implementations trimmed. A value carrying a trailing space or newline —
|
|
58
|
+
* routine from a `.env` file, a Docker `ENV` line, or a CI variable pasted with a line ending —
|
|
59
|
+
* therefore matched nothing and fell through to `development`. That is the dangerous direction: a
|
|
60
|
+
* genuine production deployment classified as development, which is precisely the failure this
|
|
61
|
+
* function exists to prevent. Trimming can only ever move a value TOWARD the bucket its author
|
|
62
|
+
* plainly intended.
|
|
63
|
+
*
|
|
64
|
+
* ## An unrecognised value falls back to `development`
|
|
65
|
+
*
|
|
66
|
+
* Preserved from all four prior implementations, so adopting this changes no existing behaviour.
|
|
67
|
+
*
|
|
68
|
+
* Be aware of what it costs: a typo (`prodction`) classifies as development and therefore OPENS the
|
|
69
|
+
* gates a production deployment wanted shut. The stricter alternative — normalise known aliases and
|
|
70
|
+
* THROW on anything else — is more defensible ("an input you do not understand must fail, not
|
|
71
|
+
* widen"), but it is a breaking operational change for any deployment currently running an
|
|
72
|
+
* unrecognised value and belongs in its own release with its own test. Do not change it here
|
|
73
|
+
* silently.
|
|
74
|
+
*/
|
|
75
|
+
declare function classifyEnv(env: string | undefined | null): EnvClass;
|
|
76
|
+
/**
|
|
77
|
+
* `true` when the value classifies as production.
|
|
78
|
+
*
|
|
79
|
+
* A named predicate rather than a `classifyEnv(x) === 'production'` at each call site: the raw
|
|
80
|
+
* comparison is the shape that caused every defect listed above, so the codebase should not contain
|
|
81
|
+
* one — even a correct one — for a reader to copy.
|
|
82
|
+
*/
|
|
83
|
+
declare function isProductionEnv(env: string | undefined | null): boolean;
|
|
84
|
+
//#endregion
|
|
85
|
+
export { EnvClass, classifyEnv, isProductionEnv };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/composition/environment.ts
|
|
2
|
+
/**
|
|
3
|
+
* Classify a raw `NODE_ENV` value.
|
|
4
|
+
*
|
|
5
|
+
* Total by construction — every input yields exactly one bucket, so a caller can never observe a
|
|
6
|
+
* fourth state or `undefined`.
|
|
7
|
+
*
|
|
8
|
+
* ## Whitespace IS trimmed, and that is a deliberate divergence from the four copies
|
|
9
|
+
*
|
|
10
|
+
* None of the previous implementations trimmed. A value carrying a trailing space or newline —
|
|
11
|
+
* routine from a `.env` file, a Docker `ENV` line, or a CI variable pasted with a line ending —
|
|
12
|
+
* therefore matched nothing and fell through to `development`. That is the dangerous direction: a
|
|
13
|
+
* genuine production deployment classified as development, which is precisely the failure this
|
|
14
|
+
* function exists to prevent. Trimming can only ever move a value TOWARD the bucket its author
|
|
15
|
+
* plainly intended.
|
|
16
|
+
*
|
|
17
|
+
* ## An unrecognised value falls back to `development`
|
|
18
|
+
*
|
|
19
|
+
* Preserved from all four prior implementations, so adopting this changes no existing behaviour.
|
|
20
|
+
*
|
|
21
|
+
* Be aware of what it costs: a typo (`prodction`) classifies as development and therefore OPENS the
|
|
22
|
+
* gates a production deployment wanted shut. The stricter alternative — normalise known aliases and
|
|
23
|
+
* THROW on anything else — is more defensible ("an input you do not understand must fail, not
|
|
24
|
+
* widen"), but it is a breaking operational change for any deployment currently running an
|
|
25
|
+
* unrecognised value and belongs in its own release with its own test. Do not change it here
|
|
26
|
+
* silently.
|
|
27
|
+
*/
|
|
28
|
+
function classifyEnv(env) {
|
|
29
|
+
switch ((env ?? "").trim().toLowerCase()) {
|
|
30
|
+
case "production":
|
|
31
|
+
case "prod": return "production";
|
|
32
|
+
case "test":
|
|
33
|
+
case "qa": return "test";
|
|
34
|
+
default: return "development";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `true` when the value classifies as production.
|
|
39
|
+
*
|
|
40
|
+
* A named predicate rather than a `classifyEnv(x) === 'production'` at each call site: the raw
|
|
41
|
+
* comparison is the shape that caused every defect listed above, so the codebase should not contain
|
|
42
|
+
* one — even a correct one — for a reader to copy.
|
|
43
|
+
*/
|
|
44
|
+
function isProductionEnv(env) {
|
|
45
|
+
return classifyEnv(env) === "production";
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { classifyEnv, isProductionEnv };
|
package/dist/event-infra.d.mts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { DomainEvent, EventHandler, EventTransport, PublishManyResult } from "./events.mjs";
|
|
2
|
-
import { OutboxAcknowledgeOptions, OutboxStore, OutboxWriteOptions } from "./outbox.mjs";
|
|
3
|
-
|
|
4
2
|
//#region src/events/event-infra.d.ts
|
|
5
3
|
/** Minimal error-only logger — assignable from `console`, pino, fastify.log. */
|
|
6
4
|
interface ErrorLogger {
|
|
@@ -87,36 +85,22 @@ declare class InProcessEventBus implements EventTransport {
|
|
|
87
85
|
/** Factory form of {@link InProcessEventBus} for hosts that prefer functions. */
|
|
88
86
|
declare function createInProcessBus(options?: InProcessEventBusOptions): InProcessEventBus;
|
|
89
87
|
/**
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
* `repositoryAsOutboxStore`, or any durable implementation.
|
|
88
|
+
* `MemoryOutboxStore` does NOT live here. It is exported from exactly one
|
|
89
|
+
* place: `@classytic/primitives/memory-outbox`.
|
|
93
90
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
91
|
+
* This module used to also export it — first as a second, MINIMAL
|
|
92
|
+
* implementation (save / getPending / acknowledge / purge, no leases, no
|
|
93
|
+
* retry, no dead-lettering, no dedupe, no validation), then briefly as a
|
|
94
|
+
* back-compat re-export. Both are gone.
|
|
95
|
+
*
|
|
96
|
+
* The re-export was tempting: seven kernels imported it from here, and one
|
|
97
|
+
* line would have spared seven rebuilds. It was still the wrong call. Two live
|
|
98
|
+
* import paths for one class means new code picks either, the migration stays
|
|
99
|
+
* permanently half-done, and the next person has to learn which path is
|
|
100
|
+
* "real". The cost of the move is seven rebuilds; paying it once is cheaper
|
|
101
|
+
* than carrying a shim forever.
|
|
102
|
+
*
|
|
103
|
+
* This subpath is the in-process BUS. The outbox is its own subpath.
|
|
99
104
|
*/
|
|
100
|
-
declare class MemoryOutboxStore implements OutboxStore {
|
|
101
|
-
/**
|
|
102
|
-
* Deliberately NOT transactional — and saying so is the point.
|
|
103
|
-
*
|
|
104
|
-
* This store pushes onto an in-process array, which no database transaction
|
|
105
|
-
* can roll back. If a caller's Mongo write aborts after `save`, the event
|
|
106
|
-
* SURVIVES here and describes something that never happened. Declaring
|
|
107
|
-
* `false` lets a caller that needs real atomicity (`@classytic/access`) refuse
|
|
108
|
-
* at boot instead of silently inheriting a ghost-event path.
|
|
109
|
-
*/
|
|
110
|
-
readonly transactionalSave = false;
|
|
111
|
-
private events;
|
|
112
|
-
save(event: DomainEvent, _options?: OutboxWriteOptions): Promise<void>;
|
|
113
|
-
getPending(limit: number): Promise<DomainEvent[]>;
|
|
114
|
-
acknowledge(eventId: string, _options?: OutboxAcknowledgeOptions): Promise<void>;
|
|
115
|
-
purge(olderThanMs: number): Promise<number>;
|
|
116
|
-
/** Every saved event (acknowledged or not) — test inspection helper. */
|
|
117
|
-
all(): ReadonlyArray<DomainEvent>;
|
|
118
|
-
/** Reset the store — test helper. */
|
|
119
|
-
clear(): void;
|
|
120
|
-
}
|
|
121
105
|
//#endregion
|
|
122
|
-
export { ErrorLogger, InProcessEventBus, InProcessEventBusOptions,
|
|
106
|
+
export { ErrorLogger, InProcessEventBus, InProcessEventBusOptions, createInProcessBus };
|
package/dist/event-infra.mjs
CHANGED
|
@@ -78,52 +78,22 @@ function createInProcessBus(options) {
|
|
|
78
78
|
return new InProcessEventBus(options);
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* `repositoryAsOutboxStore`, or any durable implementation.
|
|
81
|
+
* `MemoryOutboxStore` does NOT live here. It is exported from exactly one
|
|
82
|
+
* place: `@classytic/primitives/memory-outbox`.
|
|
84
83
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
84
|
+
* This module used to also export it — first as a second, MINIMAL
|
|
85
|
+
* implementation (save / getPending / acknowledge / purge, no leases, no
|
|
86
|
+
* retry, no dead-lettering, no dedupe, no validation), then briefly as a
|
|
87
|
+
* back-compat re-export. Both are gone.
|
|
88
|
+
*
|
|
89
|
+
* The re-export was tempting: seven kernels imported it from here, and one
|
|
90
|
+
* line would have spared seven rebuilds. It was still the wrong call. Two live
|
|
91
|
+
* import paths for one class means new code picks either, the migration stays
|
|
92
|
+
* permanently half-done, and the next person has to learn which path is
|
|
93
|
+
* "real". The cost of the move is seven rebuilds; paying it once is cheaper
|
|
94
|
+
* than carrying a shim forever.
|
|
95
|
+
*
|
|
96
|
+
* This subpath is the in-process BUS. The outbox is its own subpath.
|
|
90
97
|
*/
|
|
91
|
-
var MemoryOutboxStore = class {
|
|
92
|
-
/**
|
|
93
|
-
* Deliberately NOT transactional — and saying so is the point.
|
|
94
|
-
*
|
|
95
|
-
* This store pushes onto an in-process array, which no database transaction
|
|
96
|
-
* can roll back. If a caller's Mongo write aborts after `save`, the event
|
|
97
|
-
* SURVIVES here and describes something that never happened. Declaring
|
|
98
|
-
* `false` lets a caller that needs real atomicity (`@classytic/access`) refuse
|
|
99
|
-
* at boot instead of silently inheriting a ghost-event path.
|
|
100
|
-
*/
|
|
101
|
-
transactionalSave = false;
|
|
102
|
-
events = [];
|
|
103
|
-
async save(event, _options) {
|
|
104
|
-
this.events.push({ event });
|
|
105
|
-
}
|
|
106
|
-
async getPending(limit) {
|
|
107
|
-
return this.events.filter((e) => !e.acknowledgedAt).slice(0, limit).map((e) => e.event);
|
|
108
|
-
}
|
|
109
|
-
async acknowledge(eventId, _options) {
|
|
110
|
-
const entry = this.events.find((e) => e.event.meta.id === eventId);
|
|
111
|
-
if (entry) entry.acknowledgedAt = /* @__PURE__ */ new Date();
|
|
112
|
-
}
|
|
113
|
-
async purge(olderThanMs) {
|
|
114
|
-
const cutoff = Date.now() - olderThanMs;
|
|
115
|
-
const before = this.events.length;
|
|
116
|
-
this.events = this.events.filter((e) => !e.acknowledgedAt || e.acknowledgedAt.getTime() >= cutoff);
|
|
117
|
-
return before - this.events.length;
|
|
118
|
-
}
|
|
119
|
-
/** Every saved event (acknowledged or not) — test inspection helper. */
|
|
120
|
-
all() {
|
|
121
|
-
return this.events.map((e) => e.event);
|
|
122
|
-
}
|
|
123
|
-
/** Reset the store — test helper. */
|
|
124
|
-
clear() {
|
|
125
|
-
this.events = [];
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
98
|
//#endregion
|
|
129
|
-
export { InProcessEventBus,
|
|
99
|
+
export { InProcessEventBus, createInProcessBus };
|