@shirudo/ddd-kit 3.0.0-rc.12 → 3.0.0-rc.13
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 +13 -8
- package/dist/chunks/kit-errors.d.ts +25 -26
- package/dist/chunks/kit-errors.js +51 -52
- package/dist/chunks/kit-errors.js.map +1 -1
- package/dist/chunks/read-stream-pages.js +91 -1365
- package/dist/chunks/read-stream-pages.js.map +1 -1
- package/dist/chunks/snapshot-store.d.ts +79 -129
- package/dist/chunks/value-object.js +1275 -0
- package/dist/chunks/value-object.js.map +1 -0
- package/dist/index.d.ts +130 -390
- package/dist/index.js +200 -868
- package/dist/index.js.map +1 -1
- package/dist/money.d.ts +3 -3
- package/dist/money.js +3 -3
- package/dist/money.js.map +1 -1
- package/dist/state-machine.d.ts +227 -0
- package/dist/state-machine.js +673 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +12 -11
- package/dist/testing.js.map +1 -1
- package/package.json +5 -1
package/dist/money.d.ts
CHANGED
|
@@ -44,7 +44,7 @@ interface Money {
|
|
|
44
44
|
* Type-level brand: a NON-EXPORTED unique symbol, never present at
|
|
45
45
|
* runtime. A string key ("__brand") could be spelled out by any
|
|
46
46
|
* caller; the module-private symbol cannot, so only the module's
|
|
47
|
-
* constructors
|
|
47
|
+
* constructors create the type. Convert foreign plain shapes with
|
|
48
48
|
* `moneyFromUnknown` (validates, copies, freezes) or `moneyFromDto`.
|
|
49
49
|
*/
|
|
50
50
|
readonly [MONEY_BRAND]: true;
|
|
@@ -68,7 +68,7 @@ type MoneyDto = {
|
|
|
68
68
|
readonly scale: number;
|
|
69
69
|
};
|
|
70
70
|
/**
|
|
71
|
-
* Validates an unknown plain shape and
|
|
71
|
+
* Validates an unknown plain shape and creates a FRESH, frozen
|
|
72
72
|
* {@link Money} from it: the result shares no reference with the
|
|
73
73
|
* input, so later mutation of the input cannot reach domain state.
|
|
74
74
|
* The door for re-hydrating foreign minor-units data (rows already
|
|
@@ -101,7 +101,7 @@ declare function moneyToDto(money: Money): MoneyDto;
|
|
|
101
101
|
* Narrows an unknown value to the canonical {@link Money} shape. A
|
|
102
102
|
* CHECK, not a door: narrowing neither copies nor freezes, so an
|
|
103
103
|
* external alias can still mutate the underlying object after the
|
|
104
|
-
* check. For anything entering domain state,
|
|
104
|
+
* check. For anything entering domain state, create a fresh frozen
|
|
105
105
|
* value with {@link moneyFromUnknown} instead.
|
|
106
106
|
*/
|
|
107
107
|
declare function isMoney(value: unknown): value is Money;
|
package/dist/money.js
CHANGED
|
@@ -113,7 +113,7 @@ function assertValidCurrency(currency) {
|
|
|
113
113
|
if (!isValidCurrency(currency)) throw new InvalidMoneyError(`currency must be a non-empty string without whitespace, at most ${MAX_CURRENCY_LENGTH} characters; got ${describeValue(currency)}`);
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
|
-
* Validates an unknown plain shape and
|
|
116
|
+
* Validates an unknown plain shape and creates a FRESH, frozen
|
|
117
117
|
* {@link Money} from it: the result shares no reference with the
|
|
118
118
|
* input, so later mutation of the input cannot reach domain state.
|
|
119
119
|
* The door for re-hydrating foreign minor-units data (rows already
|
|
@@ -174,7 +174,7 @@ function moneyToDto(money) {
|
|
|
174
174
|
* Narrows an unknown value to the canonical {@link Money} shape. A
|
|
175
175
|
* CHECK, not a door: narrowing neither copies nor freezes, so an
|
|
176
176
|
* external alias can still mutate the underlying object after the
|
|
177
|
-
* check. For anything entering domain state,
|
|
177
|
+
* check. For anything entering domain state, create a fresh frozen
|
|
178
178
|
* value with {@link moneyFromUnknown} instead.
|
|
179
179
|
*/
|
|
180
180
|
function isMoney(value) {
|
|
@@ -526,7 +526,7 @@ function moneyToSnapshot(money) {
|
|
|
526
526
|
* reads badly and, hand-rolled, usually catches too much; these
|
|
527
527
|
* wrappers apply the `result-vs-throw` guide's discipline instead.
|
|
528
528
|
*
|
|
529
|
-
* The contract, mirroring `
|
|
529
|
+
* The contract, mirroring `valueObjectOrIssues`: only the parser's DOCUMENTED
|
|
530
530
|
* rejections of the INPUT become `Err`. Anything else, an assertion
|
|
531
531
|
* firing, a typo-ed helper, a genuine bug, keeps propagating as a
|
|
532
532
|
* throw, because a bug wrapped in `Err` is a bug silently counted as
|
package/dist/money.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"money.js","names":["CACHE_LIMIT"],"sources":["../src/domain/value-object/money/errors.ts","../src/domain/value-object/money/money.ts","../src/domain/value-object/money/arithmetic.ts","../src/domain/value-object/money/parse.ts","../src/domain/value-object/money/factory.ts","../src/domain/value-object/money/format.ts","../src/domain/value-object/money/snapshot.ts","../src/domain/value-object/money/try-parse.ts"],"sourcesContent":["import { DomainError } from \"../../../errors/kit-errors\";\n\n/**\n * Renders a value for a diagnostic message with a hard size ceiling:\n * error messages must never carry attacker-sized payloads into logs.\n * Total: never throws, whatever the value (JSON.stringify alone would\n * crash on bigint and circular input, replacing the documented kit\n * error with a raw TypeError). Module-internal, not part of the\n * package entry.\n */\nexport function describeValue(value: unknown): string {\n\tif (typeof value === \"bigint\") {\n\t\tconst digits = value.toString();\n\t\treturn digits.length > 48\n\t\t\t? `${digits.slice(0, 48)}... (truncated bigint)`\n\t\t\t: `${digits}n`;\n\t}\n\tif (typeof value === \"string\" && value.length > 48) {\n\t\treturn `${JSON.stringify(value.slice(0, 48))} (truncated, ${value.length} chars)`;\n\t}\n\tlet rendered: string | undefined;\n\ttry {\n\t\trendered = JSON.stringify(value);\n\t} catch {\n\t\trendered = undefined;\n\t}\n\tif (rendered === undefined) {\n\t\ttry {\n\t\t\trendered = String(value);\n\t\t} catch {\n\t\t\trendered = `[unserializable ${typeof value}]`;\n\t\t}\n\t}\n\tif (rendered.length > 64) {\n\t\treturn `${rendered.slice(0, 64)} (truncated)`;\n\t}\n\treturn rendered;\n}\n\nexport class InvalidMoneyError extends DomainError<\"INVALID_MONEY\"> {\n\tconstructor(message: string) {\n\t\tsuper({ code: \"INVALID_MONEY\", message });\n\t}\n}\n\nexport class MoneyCurrencyMismatchError extends DomainError<\"MONEY_CURRENCY_MISMATCH\"> {\n\tconstructor(left: string, right: string) {\n\t\tsuper({\n\t\t\tcode: \"MONEY_CURRENCY_MISMATCH\",\n\t\t\tmessage: `money operations require the same currency; got ${describeValue(left)} and ${describeValue(right)}`,\n\t\t});\n\t}\n}\n\nexport class MoneyScaleMismatchError extends DomainError<\"MONEY_SCALE_MISMATCH\"> {\n\tconstructor(left: number, right: number) {\n\t\tsuper({\n\t\t\tcode: \"MONEY_SCALE_MISMATCH\",\n\t\t\tmessage: `money operations require the same scale; got ${left} and ${right} (rescaleMoney is the explicit conversion)`,\n\t\t});\n\t}\n}\n\nexport class MoneyPrecisionLossError extends DomainError<\"MONEY_PRECISION_LOSS\"> {\n\tconstructor(message: string) {\n\t\tsuper({ code: \"MONEY_PRECISION_LOSS\", message });\n\t}\n}\n\nexport class UnknownCurrencyError extends DomainError<\"UNKNOWN_CURRENCY\"> {\n\tconstructor(currency: string) {\n\t\tsuper({\n\t\t\tcode: \"UNKNOWN_CURRENCY\",\n\t\t\tmessage: `The currency scale resolver has no entry for ${describeValue(currency)}`,\n\t\t});\n\t}\n}\n","import { describeValue, InvalidMoneyError } from \"./errors\";\n\n/**\n * Currency identifier for {@link Money}. The kit deliberately ships no\n * currency table: which codes exist and which scale they use is the\n * consumer's decision (see `createMoneyFactory` for wiring a resolver\n * once). ISO 4217 alpha-3 codes are the recommended convention.\n */\nexport type CurrencyCode = string;\n\n/**\n * The canonical money representation for domain state, domain events,\n * and snapshots: an exact integer amount in minor units plus the\n * explicit scale that maps minor to major units.\n *\n * Plain data by design: values created by `moneyOfMinor` are frozen,\n * carry no methods or library internals, and survive `structuredClone`,\n * deep-freeze, and the kit's state diffing. The kit ships exact\n * operations only (lossless `addMoney`/`subtractMoney`/`negateMoney`\n * and lossless `rescaleMoney`); everything that carries a rounding or\n * distribution policy (multiplication, ratios, fees, division, lossy\n * rescaling, allocation, FX) is domain policy executed by your\n * calculation library at the use-case boundary (see\n * `moneyFromSnapshot` / `moneyToSnapshot`).\n *\n * Never `number`, never a decimal string, never `amountCents`: `10.99`\n * EUR is `{ amountMinor: 1099n, currency: \"EUR\", scale: 2 }`, and JPY\n * has scale 0, not an assumed 2.\n */\nexport interface Money {\n\t/**\n\t * Exact integer amount in minor units AT THIS VALUE'S SCALE. The\n\t * scale may deliberately differ from the currency's default\n\t * exponent (intermediate precision; the snapshot interop honors\n\t * the same distinction), so the reference unit is always\n\t * `this.scale`, never an assumed per-currency constant.\n\t */\n\treadonly amountMinor: bigint;\n\t/** Required currency identifier; operations never mix currencies. */\n\treadonly currency: CurrencyCode;\n\t/** Number of minor-unit digits per major unit (EUR 2, JPY 0). */\n\treadonly scale: number;\n\t/**\n\t * Type-level brand: a NON-EXPORTED unique symbol, never present at\n\t * runtime. A string key (\"__brand\") could be spelled out by any\n\t * caller; the module-private symbol cannot, so only the module's\n\t * constructors mint the type. Convert foreign plain shapes with\n\t * `moneyFromUnknown` (validates, copies, freezes) or `moneyFromDto`.\n\t */\n\treadonly [MONEY_BRAND]: true;\n}\n\ndeclare const MONEY_BRAND: unique symbol;\n\n/**\n * Wire shape for {@link Money}: `amountMinor` travels as a string\n * because JSON numbers are floats (silent precision loss past 2^53) and\n * `JSON.stringify` throws on bigint. Validate with `moneyFromDto`\n * immediately after deserialization; emit with `moneyToDto` right\n * before serialization.\n *\n * A type alias, not an interface, on purpose: only type aliases get the\n * implicit index signature that makes the DTO assignable to `JsonValue`,\n * which `PublishedCommand` payloads require.\n */\nexport type MoneyDto = {\n\t/** Integer string matching `/^-?\\d+$/`. */\n\treadonly amountMinor: string;\n\treadonly currency: string;\n\treadonly scale: number;\n};\n\nconst INTEGER_STRING = /^-?\\d+$/;\n\n/**\n * Hard bounds, enforced at the single construction door so hostile\n * input cannot buy unbounded CPU (BigInt conversion is superlinear in\n * digit count) or memory (cache keys, padded strings, log echoes):\n *\n * - amounts: fewer than 97 digits (uint256 is 78 digits; headroom)\n * - scale: at most 64 (ETH wei is 18)\n * - currency: at most 32 characters (ISO 4217 needs 3)\n *\n * Everything past a bound is `INVALID_MONEY` by construction, and the\n * DTO/parse boundaries reject oversized strings before converting them.\n */\nconst MONEY_AMOUNT_LIMIT = 10n ** 96n;\nconst MAX_MONEY_SCALE = 64;\nconst MAX_CURRENCY_LENGTH = 32;\nconst MAX_DTO_AMOUNT_LENGTH = 97; // sign + 96 digits\n\nfunction isValidAmount(amountMinor: unknown): amountMinor is bigint {\n\treturn (\n\t\ttypeof amountMinor === \"bigint\" &&\n\t\tamountMinor < MONEY_AMOUNT_LIMIT &&\n\t\tamountMinor > -MONEY_AMOUNT_LIMIT\n\t);\n}\n\nfunction isValidCurrency(currency: unknown): currency is string {\n\treturn (\n\t\ttypeof currency === \"string\" &&\n\t\tcurrency.length > 0 &&\n\t\tcurrency.length <= MAX_CURRENCY_LENGTH &&\n\t\t!/\\s/.test(currency)\n\t);\n}\n\n/**\n * Scale guard for operations that must validate a TARGET scale before\n * computing with it (10^scale on an unchecked value is an attack\n * surface). Module-internal export, not part of the package entry.\n */\nexport function assertValidScale(scale: number): void {\n\tif (!isValidScale(scale)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`scale must be an integer between 0 and ${MAX_MONEY_SCALE}; got ${describeValue(scale)}`,\n\t\t);\n\t}\n}\n\n/**\n * Currency counterpart to {@link assertValidScale}, for call sites\n * that must reject a wiring-provided currency before any input work.\n * Module-internal export, not part of the package entry.\n */\nexport function assertValidCurrency(currency: unknown): void {\n\tif (!isValidCurrency(currency)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`currency must be a non-empty string without whitespace, at most ${MAX_CURRENCY_LENGTH} characters; got ${describeValue(currency)}`,\n\t\t);\n\t}\n}\n\n/**\n * Validates an unknown plain shape and mints a FRESH, frozen\n * {@link Money} from it: the result shares no reference with the\n * input, so later mutation of the input cannot reach domain state.\n * The door for re-hydrating foreign minor-units data (rows already\n * mapped by an ORM, caches, deserialized snapshots); wire strings go\n * through `moneyFromDto` instead.\n */\nexport function moneyFromUnknown(value: unknown): Money {\n\tassertMoney(value);\n\treturn moneyOfMinor(value.amountMinor, value.currency, value.scale);\n}\n\nfunction isValidScale(scale: unknown): scale is number {\n\treturn (\n\t\ttypeof scale === \"number\" &&\n\t\tNumber.isSafeInteger(scale) &&\n\t\tscale >= 0 &&\n\t\tscale <= MAX_MONEY_SCALE\n\t);\n}\n\n/**\n * Constructs a frozen {@link Money} from an exact minor-unit amount.\n * The only door into the shape: rejects `number` amounts (floats have\n * no place in stored money), invalid scales, and empty or\n * whitespace-carrying currency codes with `InvalidMoneyError`.\n */\nexport function moneyOfMinor(\n\tamountMinor: bigint,\n\tcurrency: CurrencyCode,\n\tscale: number,\n): Money {\n\tif (typeof amountMinor !== \"bigint\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`amountMinor must be a bigint in minor units; got ${typeof amountMinor}`,\n\t\t);\n\t}\n\tif (!isValidAmount(amountMinor)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"amountMinor must stay below 97 digits (uint256 fits with headroom)\",\n\t\t);\n\t}\n\tassertValidCurrency(currency);\n\tassertValidScale(scale);\n\treturn Object.freeze({ amountMinor, currency, scale }) as Money;\n}\n\n/**\n * Validates a {@link MoneyDto} fresh off the wire and converts it to\n * {@link Money}. Takes `unknown` on purpose: this IS the trust\n * boundary, so callers never cast before validating. `amountMinor`\n * must be a plain integer string (`/^-?\\d+$/`); anything `Number()`\n * would tolerate but bigint arithmetic cannot represent exactly\n * (\"1e5\", \"10.99\", \"0x10\") is rejected with `InvalidMoneyError`.\n */\nexport function moneyFromDto(dto: unknown): Money {\n\tif (dto === null || typeof dto !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`MoneyDto must be an object; got ${dto === null ? \"null\" : typeof dto}`,\n\t\t);\n\t}\n\tconst { amountMinor, currency, scale } = dto as Partial<\n\t\tRecord<keyof MoneyDto, unknown>\n\t>;\n\t// The length ceiling runs BEFORE BigInt(): converting a hostile\n\t// multi-megabyte digit string is superlinear CPU.\n\tif (\n\t\ttypeof amountMinor !== \"string\" ||\n\t\tamountMinor.length > MAX_DTO_AMOUNT_LENGTH ||\n\t\t!INTEGER_STRING.test(amountMinor)\n\t) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`MoneyDto.amountMinor must be an integer string matching /^-?\\\\d+$/ with at most 96 digits; got ${describeValue(amountMinor)}`,\n\t\t);\n\t}\n\t// moneyOfMinor validates currency and scale.\n\treturn moneyOfMinor(\n\t\tBigInt(amountMinor),\n\t\tcurrency as CurrencyCode,\n\t\tscale as number,\n\t);\n}\n\n/**\n * Converts {@link Money} to its JSON-safe wire shape. Guards the input\n * so untyped callers cannot leak a number-amount object onto the wire.\n */\nexport function moneyToDto(money: Money): MoneyDto {\n\tassertMoney(money);\n\treturn {\n\t\tamountMinor: money.amountMinor.toString(),\n\t\tcurrency: money.currency,\n\t\tscale: money.scale,\n\t};\n}\n\n/**\n * Narrows an unknown value to the canonical {@link Money} shape. A\n * CHECK, not a door: narrowing neither copies nor freezes, so an\n * external alias can still mutate the underlying object after the\n * check. For anything entering domain state, mint a fresh frozen\n * value with {@link moneyFromUnknown} instead.\n */\nexport function isMoney(value: unknown): value is Money {\n\tif (value === null || typeof value !== \"object\") return false;\n\tconst candidate = value as Partial<Money>;\n\treturn (\n\t\tisValidAmount(candidate.amountMinor) &&\n\t\tisValidCurrency(candidate.currency) &&\n\t\tisValidScale(candidate.scale)\n\t);\n}\n\n/**\n * Loud form of {@link isMoney} for boundary functions. Module-internal\n * export, not part of the package entry.\n */\nexport function assertMoney(value: unknown): asserts value is Money {\n\tif (!isMoney(value)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"expected a Money value ({ amountMinor: bigint, currency: string, scale: number })\",\n\t\t);\n\t}\n}\n\n/**\n * REPRESENTATION equality, deliberately: amount, currency, AND scale.\n * `10.0` EUR at scale 1 and `10.00` EUR at scale 2 denote the same\n * monetary value but are NOT equal here, because silently conflating\n * scales is how precision bugs hide. For monetary-value comparison,\n * align the scales explicitly first (lossless `rescaleMoney` upscales\n * the coarser side) and then compare.\n */\nexport function moneyEquals(a: Money, b: Money): boolean {\n\treturn (\n\t\ta.amountMinor === b.amountMinor &&\n\t\ta.currency === b.currency &&\n\t\ta.scale === b.scale\n\t);\n}\n\n/** True when the amount is exactly zero. */\nexport function isZeroMoney(money: Money): boolean {\n\treturn money.amountMinor === 0n;\n}\n\n/** True when the amount is strictly greater than zero. */\nexport function isPositiveMoney(money: Money): boolean {\n\treturn money.amountMinor > 0n;\n}\n\n/** True when the amount is strictly less than zero. */\nexport function isNegativeMoney(money: Money): boolean {\n\treturn money.amountMinor < 0n;\n}\n\n/**\n * Renders the exact decimal representation (\"10.99\", \"-0.05\", \"10\").\n * The inverse of `parseMoneyInput` and the precision-safe input for\n * display formatting; never feed the result back into arithmetic.\n * Guards its input like the wire emitters do: a non-Money value fails\n * loudly instead of rendering garbage.\n */\nexport function moneyToDecimalString(money: Money): string {\n\tassertMoney(money);\n\tconst negative = money.amountMinor < 0n;\n\tconst digits = (negative ? -money.amountMinor : money.amountMinor).toString();\n\tconst sign = negative ? \"-\" : \"\";\n\tif (money.scale === 0) return sign + digits;\n\tconst padded = digits.padStart(money.scale + 1, \"0\");\n\tconst whole = padded.slice(0, -money.scale);\n\tconst fraction = padded.slice(-money.scale);\n\treturn `${sign}${whole}.${fraction}`;\n}\n","import {\n\tMoneyCurrencyMismatchError,\n\tMoneyPrecisionLossError,\n\tMoneyScaleMismatchError,\n} from \"./errors\";\nimport { assertValidScale, type Money, moneyOfMinor } from \"./money\";\n\n/**\n * Exact operations only. Everything here is closed (Money in, Money\n * out) and cannot lose information, so no rounding decision exists to\n * make. Anything that WOULD need one (multiplication, ratios, fees,\n * division, lossy rescaling) plus everything distributive (allocation:\n * splitting 10.00 EUR three ways must hand out 3.34 + 3.33 + 3.33, not\n * round three times) and rate-based (FX) is deliberately NOT\n * implemented by the kit: rounding timing, order, and remainder policy\n * are domain policy, and a battle-tested calculation library should\n * execute them at the use-case boundary (see the snapshot bridge). The\n * kit NEVER rounds; even over-precise parse input is rejected instead\n * of rounded.\n */\nfunction assertSameUnit(a: Money, b: Money): void {\n\tif (a.currency !== b.currency) {\n\t\tthrow new MoneyCurrencyMismatchError(a.currency, b.currency);\n\t}\n\tif (a.scale !== b.scale) {\n\t\tthrow new MoneyScaleMismatchError(a.scale, b.scale);\n\t}\n}\n\n/**\n * Exact addition of same-currency, same-scale amounts. Mismatches\n * throw (`MONEY_CURRENCY_MISMATCH` / `MONEY_SCALE_MISMATCH`); there is\n * no implicit conversion of either. A result past the amount bound\n * fails as `INVALID_MONEY` instead of wrapping.\n */\nexport function addMoney(a: Money, b: Money): Money {\n\tassertSameUnit(a, b);\n\treturn moneyOfMinor(a.amountMinor + b.amountMinor, a.currency, a.scale);\n}\n\n/** Exact subtraction under the same guards as {@link addMoney}. */\nexport function subtractMoney(a: Money, b: Money): Money {\n\tassertSameUnit(a, b);\n\treturn moneyOfMinor(a.amountMinor - b.amountMinor, a.currency, a.scale);\n}\n\n/** Exact sign flip; useful for ledger reversals and refunds. */\nexport function negateMoney(money: Money): Money {\n\treturn moneyOfMinor(-money.amountMinor, money.currency, money.scale);\n}\n\n/**\n * Converts to another scale, LOSSLESSLY or not at all: upscaling and\n * exact downscaling succeed; a downscale that would drop non-zero\n * digits throws `MONEY_PRECISION_LOSS`. There is deliberately no\n * rounding parameter; lossy conversions carry a rounding policy and\n * belong to your calculation library. The intended use is aligning\n * mixed-scale amounts for `addMoney`/`subtractMoney` by upscaling the\n * coarser one.\n */\nexport function rescaleMoney(money: Money, scale: number): Money {\n\t// Validated BEFORE 10^(scale diff): an unchecked target scale is\n\t// an amplification vector.\n\tassertValidScale(scale);\n\tif (scale === money.scale) return money;\n\tif (scale > money.scale) {\n\t\treturn moneyOfMinor(\n\t\t\tmoney.amountMinor * 10n ** BigInt(scale - money.scale),\n\t\t\tmoney.currency,\n\t\t\tscale,\n\t\t);\n\t}\n\tconst factor = 10n ** BigInt(money.scale - scale);\n\tif (money.amountMinor % factor !== 0n) {\n\t\tthrow new MoneyPrecisionLossError(\n\t\t\t`rescaling from scale ${money.scale} to ${scale} loses precision; lossy conversions belong to your calculation library, where the rounding policy is explicit`,\n\t\t);\n\t}\n\treturn moneyOfMinor(money.amountMinor / factor, money.currency, scale);\n}\n","import {\n\tdescribeValue,\n\tInvalidMoneyError,\n\tMoneyPrecisionLossError,\n} from \"./errors\";\nimport {\n\tassertValidScale,\n\ttype CurrencyCode,\n\ttype Money,\n\tmoneyOfMinor,\n} from \"./money\";\n\n/** Options for {@link parseMoneyInput}. */\nexport interface ParseMoneyInputOptions {\n\treadonly currency: CurrencyCode;\n\t/** Target scale; the kit ships no currency table, so it is explicit. */\n\treadonly scale: number;\n}\n\nconst DECIMAL_INPUT = /^(-?)(\\d+)(?:\\.(\\d+))?$/;\n\n// Rejected before the regex and long before BigInt(): a hostile\n// multi-megabyte \"amount\" must cost O(1), not superlinear conversion\n// work. 96 amount digits + 64 fraction digits + sign + dot fit easily.\nconst MAX_INPUT_LENGTH = 256;\n\n/**\n * Parses a plain decimal string (\"10.99\") into exact minor units,\n * without ever touching floating point. This is the safe replacement\n * for the classic bugs `Number(input) * 100` and `parseFloat`: no\n * exponents, no `Infinity`, no locale separators, and NO rounding; the\n * kit never rounds.\n *\n * EXACT OR REJECTED: missing fraction digits pad losslessly (\"10.5\" at\n * scale 2 is 1050n) and all-zero excess digits are accepted (\"10.990\"\n * at scale 2 is 1099n), but input that cannot be represented exactly\n * at the target scale throws `MONEY_PRECISION_LOSS`. Whether \"10.999\"\n * should be rejected or become 11.00 is a BUSINESS decision, not a\n * parsing feature: put it in a domain-named policy function (a\n * `normalizeQuotedPrice`, a `calculateVat`) that rounds via your\n * calculation library and returns `Money`.\n *\n * The grammar is deliberately strict (`/^-?\\d+(\\.\\d+)?$/`). Locale\n * input (\"10,99\", grouping, currency signs) is a UI concern; normalize\n * it to this grammar before calling.\n *\n * Takes `unknown` on purpose: this is the trust boundary for raw\n * request values, so callers pass `req.body.amount` directly instead\n * of coercing (`String([...])` silently joins arrays) or casting.\n */\nexport function parseMoneyInput(\n\tinput: unknown,\n\toptions: ParseMoneyInputOptions,\n): Money {\n\tconst { currency, scale } = options;\n\t// Validated BEFORE padEnd/BigInt: an out-of-range scale (e.g. from\n\t// a buggy resolver) must fail fast as INVALID_MONEY, not buy\n\t// superlinear conversion work or a raw RangeError first.\n\tassertValidScale(scale);\n\tif (typeof input !== \"string\" || input.length > MAX_INPUT_LENGTH) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money input must be a decimal string of at most ${MAX_INPUT_LENGTH} characters; got ${describeValue(input)}`,\n\t\t);\n\t}\n\tconst match = DECIMAL_INPUT.exec(input);\n\tif (!match) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money input must be a plain decimal string matching /^-?\\\\d+(\\\\.\\\\d+)?$/; got ${describeValue(input)}`,\n\t\t);\n\t}\n\tconst sign = match[1] ?? \"\";\n\tconst whole = match[2] ?? \"\";\n\tconst fraction = match[3] ?? \"\";\n\tif (fraction.length <= scale) {\n\t\treturn moneyOfMinor(\n\t\t\tBigInt(sign + whole + fraction.padEnd(scale, \"0\")),\n\t\t\tcurrency,\n\t\t\tscale,\n\t\t);\n\t}\n\tconst excess = fraction.slice(scale);\n\tif (/^0+$/.test(excess)) {\n\t\treturn moneyOfMinor(\n\t\t\tBigInt(sign + whole + fraction.slice(0, scale)),\n\t\t\tcurrency,\n\t\t\tscale,\n\t\t);\n\t}\n\tthrow new MoneyPrecisionLossError(\n\t\t`parsing ${describeValue(input)} at scale ${scale} would lose precision; accepting over-precise input is a business decision, round it in a domain-named policy function via your calculation library`,\n\t);\n}\n","import { UnknownCurrencyError } from \"./errors\";\nimport { type CurrencyCode, type Money, moneyOfMinor } from \"./money\";\nimport { parseMoneyInput } from \"./parse\";\n\n/**\n * Resolves a currency to its scale, or `undefined` for currencies it\n * does not know. The kit ships no currency table; this is the seam\n * where the consumer provides one, once, at the composition root. Any\n * source works as a one-liner: a plain record, the runtime's own data\n * via {@link currencyScaleFromIntl}, or the currency package of a\n * calculation library (`(code) => currencies[code]?.exponent`).\n */\nexport type CurrencyScaleResolver = (\n\tcurrency: CurrencyCode,\n) => number | undefined;\n\n/**\n * Currency-aware construction helpers bound to one\n * {@link CurrencyScaleResolver}. Unknown currencies fail loudly with\n * `UNKNOWN_CURRENCY` instead of guessing a scale.\n */\nexport interface MoneyFactory {\n\t/** `moneyOfMinor` with the scale resolved from the currency. */\n\tofMinor(amountMinor: bigint, currency: CurrencyCode): Money;\n\t/** `parseMoneyInput` (exact-only) with the scale resolved from the currency. */\n\tparse(input: unknown, currency: CurrencyCode): Money;\n\t/** Zero in the given currency at its resolved scale. */\n\tzero(currency: CurrencyCode): Money;\n\t/** The resolved scale; throws `UNKNOWN_CURRENCY` when unresolved. */\n\tscaleOf(currency: CurrencyCode): number;\n}\n\n/** Options for {@link createMoneyFactory}. */\nexport interface CreateMoneyFactoryOptions {\n\treadonly scaleFor: CurrencyScaleResolver;\n}\n\n/**\n * Binds a {@link CurrencyScaleResolver} once and returns construction\n * helpers that no longer need an explicit scale per call.\n *\n * @example\n * ```ts\n * const money = createMoneyFactory({\n * scaleFor: currencyScaleFromRecord({ EUR: 2, JPY: 0 }),\n * });\n * money.parse(\"10.99\", \"EUR\"); // { amountMinor: 1099n, currency: \"EUR\", scale: 2 }\n * ```\n */\nexport function createMoneyFactory(\n\toptions: CreateMoneyFactoryOptions,\n): MoneyFactory {\n\tconst { scaleFor } = options;\n\tconst scaleOf = (currency: CurrencyCode): number => {\n\t\tconst scale = scaleFor(currency);\n\t\tif (scale === undefined) throw new UnknownCurrencyError(currency);\n\t\treturn scale;\n\t};\n\treturn Object.freeze({\n\t\tofMinor: (amountMinor: bigint, currency: CurrencyCode) =>\n\t\t\tmoneyOfMinor(amountMinor, currency, scaleOf(currency)),\n\t\tparse: (input: unknown, currency: CurrencyCode) =>\n\t\t\tparseMoneyInput(input, { currency, scale: scaleOf(currency) }),\n\t\tzero: (currency: CurrencyCode) =>\n\t\t\tmoneyOfMinor(0n, currency, scaleOf(currency)),\n\t\tscaleOf,\n\t});\n}\n\n/**\n * Resolver over a plain currency-to-scale record\n * (`{ EUR: 2, JPY: 0 }`). The record is copied into a `Map` at\n * creation, so later mutation of the input and hostile own keys have\n * no effect.\n */\nexport function currencyScaleFromRecord(\n\trecord: Readonly<Record<string, number>>,\n): CurrencyScaleResolver {\n\tconst scales = new Map(Object.entries(record));\n\treturn (currency) => scales.get(currency);\n}\n\nconst CANONICAL_ISO_CODE = /^[A-Z]{3}$/;\n\n/**\n * Resolver backed by the runtime's own currency data (ICU via\n * `Intl.NumberFormat`), so no currency table ships with the kit or the\n * consumer. Resolves ONLY canonical uppercase ISO 4217 codes: Intl\n * itself would accept \"eur\", but a silent alias would let \"eur\"-Money\n * and \"EUR\"-Money circulate side by side until an operation throws\n * `MONEY_CURRENCY_MISMATCH`; here \"eur\" resolves to `undefined` and\n * fails fast as `UNKNOWN_CURRENCY` at the factory.\n *\n * A CONVENIENCE, not an enterprise source of truth: ICU resolves\n * well-formed but UNASSIGNED codes to its default of 2 rather than\n * `undefined`, and the data shifts with the runtime's ICU version.\n * Production money paths should pin a closed, versioned currency map\n * (`currencyScaleFromRecord`) or a calculation library's versioned\n * currency package; use this resolver for demos, prototypes, and\n * internal tooling.\n */\nexport function currencyScaleFromIntl(): CurrencyScaleResolver {\n\tconst cache = new Map<string, number | undefined>();\n\treturn (currency) => {\n\t\tif (typeof currency !== \"string\" || !CANONICAL_ISO_CODE.test(currency)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (cache.has(currency)) return cache.get(currency);\n\t\tlet scale: number | undefined;\n\t\ttry {\n\t\t\tscale = new Intl.NumberFormat(\"en\", {\n\t\t\t\tstyle: \"currency\",\n\t\t\t\tcurrency,\n\t\t\t}).resolvedOptions().maximumFractionDigits;\n\t\t} catch {\n\t\t\tscale = undefined;\n\t\t}\n\t\t// Bounded: attacker-influenced currency strings must not turn\n\t\t// the cache into a leak. The legitimate key population is tiny;\n\t\t// past the cap, misses stay correct and simply pay Intl again.\n\t\tif (cache.size < CACHE_LIMIT) cache.set(currency, scale);\n\t\treturn scale;\n\t};\n}\n\nconst CACHE_LIMIT = 1_000;\n","import { assertMoney, type Money, moneyToDecimalString } from \"./money\";\n\n// Bounded like the resolver cache: attacker-influenced currency/scale\n// pairs (any well-formed code formats) must not leak memory. Past the\n// cap, formatting stays correct and pays construction again.\nconst CACHE_LIMIT = 1_000;\n\nfunction formatterFor(\n\tlocale: string,\n\tcurrency: string,\n\tscale: number,\n): Intl.NumberFormat {\n\treturn new Intl.NumberFormat(locale, {\n\t\tstyle: \"currency\",\n\t\tcurrency,\n\t\tminimumFractionDigits: scale,\n\t\tmaximumFractionDigits: scale,\n\t});\n}\n\nfunction formatDecimal(formatter: Intl.NumberFormat, money: Money): string {\n\t// Intl accepts decimal strings since NumberFormat v3; the cast only\n\t// bridges TS lib types that predate it. A number round-trip instead\n\t// would silently lose precision past 2^53.\n\treturn formatter.format(moneyToDecimalString(money) as unknown as number);\n}\n\n/**\n * Formats for display via `Intl.NumberFormat`, feeding the exact\n * decimal string (never a float), with the money's own scale as the\n * fraction-digit count. Presentation only: the output is\n * locale-dependent text and must never flow back into parsing,\n * storage, or arithmetic. Non-Money input fails loudly with\n * `INVALID_MONEY` before Intl is touched.\n *\n * The currency must be well-formed for `Intl` (ISO alpha-3); for\n * non-ISO codes, format `moneyToDecimalString(money)` yourself.\n */\nexport function formatMoney(money: Money, locale: string): string {\n\tassertMoney(money);\n\treturn formatDecimal(\n\t\tformatterFor(locale, money.currency, money.scale),\n\t\tmoney,\n\t);\n}\n\n/**\n * Binds the locale once and caches one `Intl.NumberFormat` per\n * currency/scale pair; constructing formatters is expensive, so use\n * this over `formatMoney` anywhere hot (lists, tables, exports).\n */\nexport function createMoneyFormatter(locale: string): (money: Money) => string {\n\tconst formatters = new Map<string, Intl.NumberFormat>();\n\treturn (money) => {\n\t\tassertMoney(money);\n\t\tconst key = `${money.currency}@${money.scale}`;\n\t\tlet formatter = formatters.get(key);\n\t\tif (!formatter) {\n\t\t\tformatter = formatterFor(locale, money.currency, money.scale);\n\t\t\tif (formatters.size < CACHE_LIMIT) formatters.set(key, formatter);\n\t\t}\n\t\treturn formatDecimal(formatter, money);\n\t};\n}\n","import { describeValue, InvalidMoneyError } from \"./errors\";\nimport {\n\tassertMoney,\n\ttype CurrencyCode,\n\ttype Money,\n\tmoneyOfMinor,\n} from \"./money\";\n\n/**\n * The currency part of {@link MoneySnapshotLike}: either a bare code or\n * a currency object as calculation libraries model it (code, numeric\n * base, exponent). Structural on purpose; the kit depends on no\n * calculation library.\n */\nexport type MoneySnapshotCurrencyLike =\n\t| string\n\t| {\n\t\t\treadonly code: string;\n\t\t\treadonly base?: number | bigint | ReadonlyArray<number | bigint>;\n\t\t\treadonly exponent?: number | bigint;\n\t };\n\n/**\n * The `{ amount, currency, scale }` shape that calculation libraries\n * expose when serializing their money objects (a `toJSON()` result,\n * typically). `scale` falls back to the currency's `exponent` when\n * absent; number and bigint calculators are both accepted.\n */\nexport interface MoneySnapshotLike {\n\treadonly amount: number | bigint;\n\treadonly currency: MoneySnapshotCurrencyLike;\n\treadonly scale?: number | bigint;\n}\n\n/**\n * The canonical snapshot {@link moneyToSnapshot} emits: number-based\n * with an explicit base-10 currency object, which is exactly what the\n * common calculation-library constructors accept.\n */\nexport interface MoneySnapshot {\n\treadonly amount: number;\n\treadonly currency: {\n\t\treadonly code: CurrencyCode;\n\t\treadonly base: 10;\n\t\treadonly exponent: number;\n\t};\n\treadonly scale: number;\n}\n\nfunction toScaleNumber(value: unknown, what: string): number {\n\tconst scale = typeof value === \"bigint\" ? Number(value) : value;\n\tif (typeof scale !== \"number\" || !Number.isSafeInteger(scale)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`snapshot ${what} must be an integer; got ${describeValue(value)}`,\n\t\t);\n\t}\n\treturn scale;\n}\n\n/**\n * Converts a calculation-library snapshot into canonical {@link Money}.\n * The anti-corruption checks live here so they run exactly once, at\n * the boundary:\n *\n * - non-decimal currencies are rejected (`base` other than 10; some\n * library currency packages model MGA/MRU with base 5, and pre-1971\n * GBP with a base array): their minor units do not map onto a\n * power-of-ten scale\n * - number amounts must be safe integers; fractional or beyond-2^53\n * amounts are rejected instead of silently corrupted\n * - bigint amounts pass through exactly\n *\n * Takes `unknown` on purpose: this is the trust boundary for foreign\n * library data, so callers never cast before validating (the\n * {@link MoneySnapshotLike} type documents the expected shape).\n */\nexport function moneyFromSnapshot(snapshot: unknown): Money {\n\tif (snapshot === null || typeof snapshot !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money snapshot must be an object; got ${snapshot === null ? \"null\" : typeof snapshot}`,\n\t\t);\n\t}\n\tconst { amount, currency, scale } = snapshot as Partial<\n\t\tRecord<keyof MoneySnapshotLike, unknown>\n\t>;\n\tconst currencyObject = (\n\t\ttypeof currency === \"string\" ? { code: currency } : currency\n\t) as\n\t\t| Partial<Record<\"code\" | \"base\" | \"exponent\", unknown>>\n\t\t| null\n\t\t| undefined;\n\tif (currencyObject === null || typeof currencyObject !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`snapshot currency must be a code or a currency object; got ${typeof currency}`,\n\t\t);\n\t}\n\tconst { base } = currencyObject;\n\tif (base !== undefined && base !== 10 && base !== 10n) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`only base-10 currencies map onto Money; got base ${describeValue(base)} for ${describeValue(currencyObject.code)}`,\n\t\t);\n\t}\n\tconst scaleSource = scale ?? currencyObject.exponent;\n\tif (scaleSource === undefined) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"money snapshot carries neither a scale nor a currency exponent\",\n\t\t);\n\t}\n\t// moneyOfMinor validates the currency code.\n\treturn moneyOfMinor(\n\t\ttoBigIntAmount(amount),\n\t\tcurrencyObject.code as CurrencyCode,\n\t\ttoScaleNumber(scaleSource, \"scale\"),\n\t);\n}\n\nfunction toBigIntAmount(amount: unknown): bigint {\n\tif (typeof amount === \"bigint\") return amount;\n\tif (typeof amount === \"number\" && Number.isSafeInteger(amount)) {\n\t\treturn BigInt(amount);\n\t}\n\tthrow new InvalidMoneyError(\n\t\t`snapshot amount must be a bigint or a safe integer; got ${describeValue(amount)}`,\n\t);\n}\n\n/**\n * Converts {@link Money} into the snapshot shape the common\n * calculation-library constructors accept. Number-based by design (the\n * libraries' default calculators are), so amounts past\n * `Number.MAX_SAFE_INTEGER` are rejected loudly; wire such amounts\n * into a bigint calculator directly from `money.amountMinor` instead.\n */\nexport function moneyToSnapshot(money: Money): MoneySnapshot {\n\tassertMoney(money);\n\tconst amount = Number(money.amountMinor);\n\tif (!Number.isSafeInteger(amount)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`amountMinor ${money.amountMinor} exceeds Number.MAX_SAFE_INTEGER; use your library's bigint calculator and pass amountMinor directly`,\n\t\t);\n\t}\n\treturn {\n\t\tamount,\n\t\tcurrency: { code: money.currency, base: 10, exponent: money.scale },\n\t\tscale: money.scale,\n\t};\n}\n","import { err, ok, type Result } from \"@shirudo/result\";\nimport { InvalidMoneyError, MoneyPrecisionLossError } from \"./errors\";\nimport {\n\tassertValidCurrency,\n\tassertValidScale,\n\ttype Money,\n\tmoneyFromDto,\n} from \"./money\";\nimport { type ParseMoneyInputOptions, parseMoneyInput } from \"./parse\";\nimport { moneyFromSnapshot } from \"./snapshot\";\n\n/**\n * Result-returning counterparts to the three money boundary parsers,\n * for call sites that process many candidate values in one pass: a\n * CSV import, a batch migration, a message replay. Per-row try/catch\n * reads badly and, hand-rolled, usually catches too much; these\n * wrappers apply the `result-vs-throw` guide's discipline instead.\n *\n * The contract, mirroring `voValidated`: only the parser's DOCUMENTED\n * rejections of the INPUT become `Err`. Anything else, an assertion\n * firing, a typo-ed helper, a genuine bug, keeps propagating as a\n * throw, because a bug wrapped in `Err` is a bug silently counted as\n * a bad input row. That includes the parse OPTIONS: a broken scale\n * resolver is a bug in the caller's wiring, so it is validated\n * eagerly and throws instead of marking every row bad. The `Err`\n * types are exact per parser: the wire parsers reject only with\n * `InvalidMoneyError`, while decimal-string parsing can additionally\n * refuse over-precise input with `MoneyPrecisionLossError`.\n *\n * `Result`, `ok`, and `err` come from `@shirudo/result` (already a\n * peer dependency); import them from there to work with the branches.\n */\n\n// Deliberately NOT @shirudo/result's `fromThrowable`: its error\n// mapper is typed as a total unknown -> E conversion, and rethrowing\n// from it would lean on an undocumented implementation detail. Here\n// the rethrow of unexpected errors is a first-class branch, and the\n// type-guard parameter yields the exact per-parser Err union.\nfunction tryCatching<T, E extends Error>(\n\tparse: () => T,\n\tisExpected: (error: unknown) => error is E,\n): Result<T, E> {\n\ttry {\n\t\treturn ok(parse());\n\t} catch (error) {\n\t\tif (isExpected(error)) return err(error);\n\t\tthrow error;\n\t}\n}\n\n/**\n * {@link parseMoneyInput} as a `Result`: `Err` for the documented\n * rejections (malformed input as `InvalidMoneyError`, over-precise\n * input as `MoneyPrecisionLossError`), a throw for everything else.\n */\nexport function tryParseMoneyInput(\n\tinput: unknown,\n\toptions: ParseMoneyInputOptions,\n): Result<Money, InvalidMoneyError | MoneyPrecisionLossError> {\n\t// Options are wiring, not data: validated OUTSIDE the wrapper so a\n\t// bad scale or currency throws instead of becoming a per-row Err.\n\tassertValidScale(options.scale);\n\tassertValidCurrency(options.currency);\n\treturn tryCatching(\n\t\t() => parseMoneyInput(input, options),\n\t\t(error): error is InvalidMoneyError | MoneyPrecisionLossError =>\n\t\t\terror instanceof InvalidMoneyError ||\n\t\t\terror instanceof MoneyPrecisionLossError,\n\t);\n}\n\n/**\n * {@link moneyFromDto} as a `Result`: `Err` for the documented\n * rejection (`InvalidMoneyError`), a throw for everything else.\n */\nexport function tryMoneyFromDto(\n\tdto: unknown,\n): Result<Money, InvalidMoneyError> {\n\treturn tryCatching(\n\t\t() => moneyFromDto(dto),\n\t\t(error): error is InvalidMoneyError => error instanceof InvalidMoneyError,\n\t);\n}\n\n/**\n * {@link moneyFromSnapshot} as a `Result`: `Err` for the documented\n * rejection (`InvalidMoneyError`), a throw for everything else.\n */\nexport function tryMoneyFromSnapshot(\n\tsnapshot: unknown,\n): Result<Money, InvalidMoneyError> {\n\treturn tryCatching(\n\t\t() => moneyFromSnapshot(snapshot),\n\t\t(error): error is InvalidMoneyError => error instanceof InvalidMoneyError,\n\t);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"money.js","names":["CACHE_LIMIT"],"sources":["../src/domain/value-object/money/errors.ts","../src/domain/value-object/money/money.ts","../src/domain/value-object/money/arithmetic.ts","../src/domain/value-object/money/parse.ts","../src/domain/value-object/money/factory.ts","../src/domain/value-object/money/format.ts","../src/domain/value-object/money/snapshot.ts","../src/domain/value-object/money/try-parse.ts"],"sourcesContent":["import { DomainError } from \"../../../errors/kit-errors\";\n\n/**\n * Renders a value for a diagnostic message with a hard size ceiling:\n * error messages must never carry attacker-sized payloads into logs.\n * Total: never throws, whatever the value (JSON.stringify alone would\n * crash on bigint and circular input, replacing the documented kit\n * error with a raw TypeError). Module-internal, not part of the\n * package entry.\n */\nexport function describeValue(value: unknown): string {\n\tif (typeof value === \"bigint\") {\n\t\tconst digits = value.toString();\n\t\treturn digits.length > 48\n\t\t\t? `${digits.slice(0, 48)}... (truncated bigint)`\n\t\t\t: `${digits}n`;\n\t}\n\tif (typeof value === \"string\" && value.length > 48) {\n\t\treturn `${JSON.stringify(value.slice(0, 48))} (truncated, ${value.length} chars)`;\n\t}\n\tlet rendered: string | undefined;\n\ttry {\n\t\trendered = JSON.stringify(value);\n\t} catch {\n\t\trendered = undefined;\n\t}\n\tif (rendered === undefined) {\n\t\ttry {\n\t\t\trendered = String(value);\n\t\t} catch {\n\t\t\trendered = `[unserializable ${typeof value}]`;\n\t\t}\n\t}\n\tif (rendered.length > 64) {\n\t\treturn `${rendered.slice(0, 64)} (truncated)`;\n\t}\n\treturn rendered;\n}\n\nexport class InvalidMoneyError extends DomainError<\"INVALID_MONEY\"> {\n\tconstructor(message: string) {\n\t\tsuper({ code: \"INVALID_MONEY\", message });\n\t}\n}\n\nexport class MoneyCurrencyMismatchError extends DomainError<\"MONEY_CURRENCY_MISMATCH\"> {\n\tconstructor(left: string, right: string) {\n\t\tsuper({\n\t\t\tcode: \"MONEY_CURRENCY_MISMATCH\",\n\t\t\tmessage: `money operations require the same currency; got ${describeValue(left)} and ${describeValue(right)}`,\n\t\t});\n\t}\n}\n\nexport class MoneyScaleMismatchError extends DomainError<\"MONEY_SCALE_MISMATCH\"> {\n\tconstructor(left: number, right: number) {\n\t\tsuper({\n\t\t\tcode: \"MONEY_SCALE_MISMATCH\",\n\t\t\tmessage: `money operations require the same scale; got ${left} and ${right} (rescaleMoney is the explicit conversion)`,\n\t\t});\n\t}\n}\n\nexport class MoneyPrecisionLossError extends DomainError<\"MONEY_PRECISION_LOSS\"> {\n\tconstructor(message: string) {\n\t\tsuper({ code: \"MONEY_PRECISION_LOSS\", message });\n\t}\n}\n\nexport class UnknownCurrencyError extends DomainError<\"UNKNOWN_CURRENCY\"> {\n\tconstructor(currency: string) {\n\t\tsuper({\n\t\t\tcode: \"UNKNOWN_CURRENCY\",\n\t\t\tmessage: `The currency scale resolver has no entry for ${describeValue(currency)}`,\n\t\t});\n\t}\n}\n","import { describeValue, InvalidMoneyError } from \"./errors\";\n\n/**\n * Currency identifier for {@link Money}. The kit deliberately ships no\n * currency table: which codes exist and which scale they use is the\n * consumer's decision (see `createMoneyFactory` for wiring a resolver\n * once). ISO 4217 alpha-3 codes are the recommended convention.\n */\nexport type CurrencyCode = string;\n\n/**\n * The canonical money representation for domain state, domain events,\n * and snapshots: an exact integer amount in minor units plus the\n * explicit scale that maps minor to major units.\n *\n * Plain data by design: values created by `moneyOfMinor` are frozen,\n * carry no methods or library internals, and survive `structuredClone`,\n * deep-freeze, and the kit's state diffing. The kit ships exact\n * operations only (lossless `addMoney`/`subtractMoney`/`negateMoney`\n * and lossless `rescaleMoney`); everything that carries a rounding or\n * distribution policy (multiplication, ratios, fees, division, lossy\n * rescaling, allocation, FX) is domain policy executed by your\n * calculation library at the use-case boundary (see\n * `moneyFromSnapshot` / `moneyToSnapshot`).\n *\n * Never `number`, never a decimal string, never `amountCents`: `10.99`\n * EUR is `{ amountMinor: 1099n, currency: \"EUR\", scale: 2 }`, and JPY\n * has scale 0, not an assumed 2.\n */\nexport interface Money {\n\t/**\n\t * Exact integer amount in minor units AT THIS VALUE'S SCALE. The\n\t * scale may deliberately differ from the currency's default\n\t * exponent (intermediate precision; the snapshot interop honors\n\t * the same distinction), so the reference unit is always\n\t * `this.scale`, never an assumed per-currency constant.\n\t */\n\treadonly amountMinor: bigint;\n\t/** Required currency identifier; operations never mix currencies. */\n\treadonly currency: CurrencyCode;\n\t/** Number of minor-unit digits per major unit (EUR 2, JPY 0). */\n\treadonly scale: number;\n\t/**\n\t * Type-level brand: a NON-EXPORTED unique symbol, never present at\n\t * runtime. A string key (\"__brand\") could be spelled out by any\n\t * caller; the module-private symbol cannot, so only the module's\n\t * constructors create the type. Convert foreign plain shapes with\n\t * `moneyFromUnknown` (validates, copies, freezes) or `moneyFromDto`.\n\t */\n\treadonly [MONEY_BRAND]: true;\n}\n\ndeclare const MONEY_BRAND: unique symbol;\n\n/**\n * Wire shape for {@link Money}: `amountMinor` travels as a string\n * because JSON numbers are floats (silent precision loss past 2^53) and\n * `JSON.stringify` throws on bigint. Validate with `moneyFromDto`\n * immediately after deserialization; emit with `moneyToDto` right\n * before serialization.\n *\n * A type alias, not an interface, on purpose: only type aliases get the\n * implicit index signature that makes the DTO assignable to `JsonValue`,\n * which `PublishedCommand` payloads require.\n */\nexport type MoneyDto = {\n\t/** Integer string matching `/^-?\\d+$/`. */\n\treadonly amountMinor: string;\n\treadonly currency: string;\n\treadonly scale: number;\n};\n\nconst INTEGER_STRING = /^-?\\d+$/;\n\n/**\n * Hard bounds, enforced at the single construction door so hostile\n * input cannot buy unbounded CPU (BigInt conversion is superlinear in\n * digit count) or memory (cache keys, padded strings, log echoes):\n *\n * - amounts: fewer than 97 digits (uint256 is 78 digits; headroom)\n * - scale: at most 64 (ETH wei is 18)\n * - currency: at most 32 characters (ISO 4217 needs 3)\n *\n * Everything past a bound is `INVALID_MONEY` by construction, and the\n * DTO/parse boundaries reject oversized strings before converting them.\n */\nconst MONEY_AMOUNT_LIMIT = 10n ** 96n;\nconst MAX_MONEY_SCALE = 64;\nconst MAX_CURRENCY_LENGTH = 32;\nconst MAX_DTO_AMOUNT_LENGTH = 97; // sign + 96 digits\n\nfunction isValidAmount(amountMinor: unknown): amountMinor is bigint {\n\treturn (\n\t\ttypeof amountMinor === \"bigint\" &&\n\t\tamountMinor < MONEY_AMOUNT_LIMIT &&\n\t\tamountMinor > -MONEY_AMOUNT_LIMIT\n\t);\n}\n\nfunction isValidCurrency(currency: unknown): currency is string {\n\treturn (\n\t\ttypeof currency === \"string\" &&\n\t\tcurrency.length > 0 &&\n\t\tcurrency.length <= MAX_CURRENCY_LENGTH &&\n\t\t!/\\s/.test(currency)\n\t);\n}\n\n/**\n * Scale guard for operations that must validate a TARGET scale before\n * computing with it (10^scale on an unchecked value is an attack\n * surface). Module-internal export, not part of the package entry.\n */\nexport function assertValidScale(scale: number): void {\n\tif (!isValidScale(scale)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`scale must be an integer between 0 and ${MAX_MONEY_SCALE}; got ${describeValue(scale)}`,\n\t\t);\n\t}\n}\n\n/**\n * Currency counterpart to {@link assertValidScale}, for call sites\n * that must reject a wiring-provided currency before any input work.\n * Module-internal export, not part of the package entry.\n */\nexport function assertValidCurrency(currency: unknown): void {\n\tif (!isValidCurrency(currency)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`currency must be a non-empty string without whitespace, at most ${MAX_CURRENCY_LENGTH} characters; got ${describeValue(currency)}`,\n\t\t);\n\t}\n}\n\n/**\n * Validates an unknown plain shape and creates a FRESH, frozen\n * {@link Money} from it: the result shares no reference with the\n * input, so later mutation of the input cannot reach domain state.\n * The door for re-hydrating foreign minor-units data (rows already\n * mapped by an ORM, caches, deserialized snapshots); wire strings go\n * through `moneyFromDto` instead.\n */\nexport function moneyFromUnknown(value: unknown): Money {\n\tassertMoney(value);\n\treturn moneyOfMinor(value.amountMinor, value.currency, value.scale);\n}\n\nfunction isValidScale(scale: unknown): scale is number {\n\treturn (\n\t\ttypeof scale === \"number\" &&\n\t\tNumber.isSafeInteger(scale) &&\n\t\tscale >= 0 &&\n\t\tscale <= MAX_MONEY_SCALE\n\t);\n}\n\n/**\n * Constructs a frozen {@link Money} from an exact minor-unit amount.\n * The only door into the shape: rejects `number` amounts (floats have\n * no place in stored money), invalid scales, and empty or\n * whitespace-carrying currency codes with `InvalidMoneyError`.\n */\nexport function moneyOfMinor(\n\tamountMinor: bigint,\n\tcurrency: CurrencyCode,\n\tscale: number,\n): Money {\n\tif (typeof amountMinor !== \"bigint\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`amountMinor must be a bigint in minor units; got ${typeof amountMinor}`,\n\t\t);\n\t}\n\tif (!isValidAmount(amountMinor)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"amountMinor must stay below 97 digits (uint256 fits with headroom)\",\n\t\t);\n\t}\n\tassertValidCurrency(currency);\n\tassertValidScale(scale);\n\treturn Object.freeze({ amountMinor, currency, scale }) as Money;\n}\n\n/**\n * Validates a {@link MoneyDto} fresh off the wire and converts it to\n * {@link Money}. Takes `unknown` on purpose: this IS the trust\n * boundary, so callers never cast before validating. `amountMinor`\n * must be a plain integer string (`/^-?\\d+$/`); anything `Number()`\n * would tolerate but bigint arithmetic cannot represent exactly\n * (\"1e5\", \"10.99\", \"0x10\") is rejected with `InvalidMoneyError`.\n */\nexport function moneyFromDto(dto: unknown): Money {\n\tif (dto === null || typeof dto !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`MoneyDto must be an object; got ${dto === null ? \"null\" : typeof dto}`,\n\t\t);\n\t}\n\tconst { amountMinor, currency, scale } = dto as Partial<\n\t\tRecord<keyof MoneyDto, unknown>\n\t>;\n\t// The length ceiling runs BEFORE BigInt(): converting a hostile\n\t// multi-megabyte digit string is superlinear CPU.\n\tif (\n\t\ttypeof amountMinor !== \"string\" ||\n\t\tamountMinor.length > MAX_DTO_AMOUNT_LENGTH ||\n\t\t!INTEGER_STRING.test(amountMinor)\n\t) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`MoneyDto.amountMinor must be an integer string matching /^-?\\\\d+$/ with at most 96 digits; got ${describeValue(amountMinor)}`,\n\t\t);\n\t}\n\t// moneyOfMinor validates currency and scale.\n\treturn moneyOfMinor(\n\t\tBigInt(amountMinor),\n\t\tcurrency as CurrencyCode,\n\t\tscale as number,\n\t);\n}\n\n/**\n * Converts {@link Money} to its JSON-safe wire shape. Guards the input\n * so untyped callers cannot leak a number-amount object onto the wire.\n */\nexport function moneyToDto(money: Money): MoneyDto {\n\tassertMoney(money);\n\treturn {\n\t\tamountMinor: money.amountMinor.toString(),\n\t\tcurrency: money.currency,\n\t\tscale: money.scale,\n\t};\n}\n\n/**\n * Narrows an unknown value to the canonical {@link Money} shape. A\n * CHECK, not a door: narrowing neither copies nor freezes, so an\n * external alias can still mutate the underlying object after the\n * check. For anything entering domain state, create a fresh frozen\n * value with {@link moneyFromUnknown} instead.\n */\nexport function isMoney(value: unknown): value is Money {\n\tif (value === null || typeof value !== \"object\") return false;\n\tconst candidate = value as Partial<Money>;\n\treturn (\n\t\tisValidAmount(candidate.amountMinor) &&\n\t\tisValidCurrency(candidate.currency) &&\n\t\tisValidScale(candidate.scale)\n\t);\n}\n\n/**\n * Loud form of {@link isMoney} for boundary functions. Module-internal\n * export, not part of the package entry.\n */\nexport function assertMoney(value: unknown): asserts value is Money {\n\tif (!isMoney(value)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"expected a Money value ({ amountMinor: bigint, currency: string, scale: number })\",\n\t\t);\n\t}\n}\n\n/**\n * REPRESENTATION equality, deliberately: amount, currency, AND scale.\n * `10.0` EUR at scale 1 and `10.00` EUR at scale 2 denote the same\n * monetary value but are NOT equal here, because silently conflating\n * scales is how precision bugs hide. For monetary-value comparison,\n * align the scales explicitly first (lossless `rescaleMoney` upscales\n * the coarser side) and then compare.\n */\nexport function moneyEquals(a: Money, b: Money): boolean {\n\treturn (\n\t\ta.amountMinor === b.amountMinor &&\n\t\ta.currency === b.currency &&\n\t\ta.scale === b.scale\n\t);\n}\n\n/** True when the amount is exactly zero. */\nexport function isZeroMoney(money: Money): boolean {\n\treturn money.amountMinor === 0n;\n}\n\n/** True when the amount is strictly greater than zero. */\nexport function isPositiveMoney(money: Money): boolean {\n\treturn money.amountMinor > 0n;\n}\n\n/** True when the amount is strictly less than zero. */\nexport function isNegativeMoney(money: Money): boolean {\n\treturn money.amountMinor < 0n;\n}\n\n/**\n * Renders the exact decimal representation (\"10.99\", \"-0.05\", \"10\").\n * The inverse of `parseMoneyInput` and the precision-safe input for\n * display formatting; never feed the result back into arithmetic.\n * Guards its input like the wire emitters do: a non-Money value fails\n * loudly instead of rendering garbage.\n */\nexport function moneyToDecimalString(money: Money): string {\n\tassertMoney(money);\n\tconst negative = money.amountMinor < 0n;\n\tconst digits = (negative ? -money.amountMinor : money.amountMinor).toString();\n\tconst sign = negative ? \"-\" : \"\";\n\tif (money.scale === 0) return sign + digits;\n\tconst padded = digits.padStart(money.scale + 1, \"0\");\n\tconst whole = padded.slice(0, -money.scale);\n\tconst fraction = padded.slice(-money.scale);\n\treturn `${sign}${whole}.${fraction}`;\n}\n","import {\n\tMoneyCurrencyMismatchError,\n\tMoneyPrecisionLossError,\n\tMoneyScaleMismatchError,\n} from \"./errors\";\nimport { assertValidScale, type Money, moneyOfMinor } from \"./money\";\n\n/**\n * Exact operations only. Everything here is closed (Money in, Money\n * out) and cannot lose information, so no rounding decision exists to\n * make. Anything that WOULD need one (multiplication, ratios, fees,\n * division, lossy rescaling) plus everything distributive (allocation:\n * splitting 10.00 EUR three ways must hand out 3.34 + 3.33 + 3.33, not\n * round three times) and rate-based (FX) is deliberately NOT\n * implemented by the kit: rounding timing, order, and remainder policy\n * are domain policy, and a battle-tested calculation library should\n * execute them at the use-case boundary (see the snapshot bridge). The\n * kit NEVER rounds; even over-precise parse input is rejected instead\n * of rounded.\n */\nfunction assertSameUnit(a: Money, b: Money): void {\n\tif (a.currency !== b.currency) {\n\t\tthrow new MoneyCurrencyMismatchError(a.currency, b.currency);\n\t}\n\tif (a.scale !== b.scale) {\n\t\tthrow new MoneyScaleMismatchError(a.scale, b.scale);\n\t}\n}\n\n/**\n * Exact addition of same-currency, same-scale amounts. Mismatches\n * throw (`MONEY_CURRENCY_MISMATCH` / `MONEY_SCALE_MISMATCH`); there is\n * no implicit conversion of either. A result past the amount bound\n * fails as `INVALID_MONEY` instead of wrapping.\n */\nexport function addMoney(a: Money, b: Money): Money {\n\tassertSameUnit(a, b);\n\treturn moneyOfMinor(a.amountMinor + b.amountMinor, a.currency, a.scale);\n}\n\n/** Exact subtraction under the same guards as {@link addMoney}. */\nexport function subtractMoney(a: Money, b: Money): Money {\n\tassertSameUnit(a, b);\n\treturn moneyOfMinor(a.amountMinor - b.amountMinor, a.currency, a.scale);\n}\n\n/** Exact sign flip; useful for ledger reversals and refunds. */\nexport function negateMoney(money: Money): Money {\n\treturn moneyOfMinor(-money.amountMinor, money.currency, money.scale);\n}\n\n/**\n * Converts to another scale, LOSSLESSLY or not at all: upscaling and\n * exact downscaling succeed; a downscale that would drop non-zero\n * digits throws `MONEY_PRECISION_LOSS`. There is deliberately no\n * rounding parameter; lossy conversions carry a rounding policy and\n * belong to your calculation library. The intended use is aligning\n * mixed-scale amounts for `addMoney`/`subtractMoney` by upscaling the\n * coarser one.\n */\nexport function rescaleMoney(money: Money, scale: number): Money {\n\t// Validated BEFORE 10^(scale diff): an unchecked target scale is\n\t// an amplification vector.\n\tassertValidScale(scale);\n\tif (scale === money.scale) return money;\n\tif (scale > money.scale) {\n\t\treturn moneyOfMinor(\n\t\t\tmoney.amountMinor * 10n ** BigInt(scale - money.scale),\n\t\t\tmoney.currency,\n\t\t\tscale,\n\t\t);\n\t}\n\tconst factor = 10n ** BigInt(money.scale - scale);\n\tif (money.amountMinor % factor !== 0n) {\n\t\tthrow new MoneyPrecisionLossError(\n\t\t\t`rescaling from scale ${money.scale} to ${scale} loses precision; lossy conversions belong to your calculation library, where the rounding policy is explicit`,\n\t\t);\n\t}\n\treturn moneyOfMinor(money.amountMinor / factor, money.currency, scale);\n}\n","import {\n\tdescribeValue,\n\tInvalidMoneyError,\n\tMoneyPrecisionLossError,\n} from \"./errors\";\nimport {\n\tassertValidScale,\n\ttype CurrencyCode,\n\ttype Money,\n\tmoneyOfMinor,\n} from \"./money\";\n\n/** Options for {@link parseMoneyInput}. */\nexport interface ParseMoneyInputOptions {\n\treadonly currency: CurrencyCode;\n\t/** Target scale; the kit ships no currency table, so it is explicit. */\n\treadonly scale: number;\n}\n\nconst DECIMAL_INPUT = /^(-?)(\\d+)(?:\\.(\\d+))?$/;\n\n// Rejected before the regex and long before BigInt(): a hostile\n// multi-megabyte \"amount\" must cost O(1), not superlinear conversion\n// work. 96 amount digits + 64 fraction digits + sign + dot fit easily.\nconst MAX_INPUT_LENGTH = 256;\n\n/**\n * Parses a plain decimal string (\"10.99\") into exact minor units,\n * without ever touching floating point. This is the safe replacement\n * for the classic bugs `Number(input) * 100` and `parseFloat`: no\n * exponents, no `Infinity`, no locale separators, and NO rounding; the\n * kit never rounds.\n *\n * EXACT OR REJECTED: missing fraction digits pad losslessly (\"10.5\" at\n * scale 2 is 1050n) and all-zero excess digits are accepted (\"10.990\"\n * at scale 2 is 1099n), but input that cannot be represented exactly\n * at the target scale throws `MONEY_PRECISION_LOSS`. Whether \"10.999\"\n * should be rejected or become 11.00 is a BUSINESS decision, not a\n * parsing feature: put it in a domain-named policy function (a\n * `normalizeQuotedPrice`, a `calculateVat`) that rounds via your\n * calculation library and returns `Money`.\n *\n * The grammar is deliberately strict (`/^-?\\d+(\\.\\d+)?$/`). Locale\n * input (\"10,99\", grouping, currency signs) is a UI concern; normalize\n * it to this grammar before calling.\n *\n * Takes `unknown` on purpose: this is the trust boundary for raw\n * request values, so callers pass `req.body.amount` directly instead\n * of coercing (`String([...])` silently joins arrays) or casting.\n */\nexport function parseMoneyInput(\n\tinput: unknown,\n\toptions: ParseMoneyInputOptions,\n): Money {\n\tconst { currency, scale } = options;\n\t// Validated BEFORE padEnd/BigInt: an out-of-range scale (e.g. from\n\t// a buggy resolver) must fail fast as INVALID_MONEY, not buy\n\t// superlinear conversion work or a raw RangeError first.\n\tassertValidScale(scale);\n\tif (typeof input !== \"string\" || input.length > MAX_INPUT_LENGTH) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money input must be a decimal string of at most ${MAX_INPUT_LENGTH} characters; got ${describeValue(input)}`,\n\t\t);\n\t}\n\tconst match = DECIMAL_INPUT.exec(input);\n\tif (!match) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money input must be a plain decimal string matching /^-?\\\\d+(\\\\.\\\\d+)?$/; got ${describeValue(input)}`,\n\t\t);\n\t}\n\tconst sign = match[1] ?? \"\";\n\tconst whole = match[2] ?? \"\";\n\tconst fraction = match[3] ?? \"\";\n\tif (fraction.length <= scale) {\n\t\treturn moneyOfMinor(\n\t\t\tBigInt(sign + whole + fraction.padEnd(scale, \"0\")),\n\t\t\tcurrency,\n\t\t\tscale,\n\t\t);\n\t}\n\tconst excess = fraction.slice(scale);\n\tif (/^0+$/.test(excess)) {\n\t\treturn moneyOfMinor(\n\t\t\tBigInt(sign + whole + fraction.slice(0, scale)),\n\t\t\tcurrency,\n\t\t\tscale,\n\t\t);\n\t}\n\tthrow new MoneyPrecisionLossError(\n\t\t`parsing ${describeValue(input)} at scale ${scale} would lose precision; accepting over-precise input is a business decision, round it in a domain-named policy function via your calculation library`,\n\t);\n}\n","import { UnknownCurrencyError } from \"./errors\";\nimport { type CurrencyCode, type Money, moneyOfMinor } from \"./money\";\nimport { parseMoneyInput } from \"./parse\";\n\n/**\n * Resolves a currency to its scale, or `undefined` for currencies it\n * does not know. The kit ships no currency table; this is the seam\n * where the consumer provides one, once, at the composition root. Any\n * source works as a one-liner: a plain record, the runtime's own data\n * via {@link currencyScaleFromIntl}, or the currency package of a\n * calculation library (`(code) => currencies[code]?.exponent`).\n */\nexport type CurrencyScaleResolver = (\n\tcurrency: CurrencyCode,\n) => number | undefined;\n\n/**\n * Currency-aware construction helpers bound to one\n * {@link CurrencyScaleResolver}. Unknown currencies fail loudly with\n * `UNKNOWN_CURRENCY` instead of guessing a scale.\n */\nexport interface MoneyFactory {\n\t/** `moneyOfMinor` with the scale resolved from the currency. */\n\tofMinor(amountMinor: bigint, currency: CurrencyCode): Money;\n\t/** `parseMoneyInput` (exact-only) with the scale resolved from the currency. */\n\tparse(input: unknown, currency: CurrencyCode): Money;\n\t/** Zero in the given currency at its resolved scale. */\n\tzero(currency: CurrencyCode): Money;\n\t/** The resolved scale; throws `UNKNOWN_CURRENCY` when unresolved. */\n\tscaleOf(currency: CurrencyCode): number;\n}\n\n/** Options for {@link createMoneyFactory}. */\nexport interface CreateMoneyFactoryOptions {\n\treadonly scaleFor: CurrencyScaleResolver;\n}\n\n/**\n * Binds a {@link CurrencyScaleResolver} once and returns construction\n * helpers that no longer need an explicit scale per call.\n *\n * @example\n * ```ts\n * const money = createMoneyFactory({\n * scaleFor: currencyScaleFromRecord({ EUR: 2, JPY: 0 }),\n * });\n * money.parse(\"10.99\", \"EUR\"); // { amountMinor: 1099n, currency: \"EUR\", scale: 2 }\n * ```\n */\nexport function createMoneyFactory(\n\toptions: CreateMoneyFactoryOptions,\n): MoneyFactory {\n\tconst { scaleFor } = options;\n\tconst scaleOf = (currency: CurrencyCode): number => {\n\t\tconst scale = scaleFor(currency);\n\t\tif (scale === undefined) throw new UnknownCurrencyError(currency);\n\t\treturn scale;\n\t};\n\treturn Object.freeze({\n\t\tofMinor: (amountMinor: bigint, currency: CurrencyCode) =>\n\t\t\tmoneyOfMinor(amountMinor, currency, scaleOf(currency)),\n\t\tparse: (input: unknown, currency: CurrencyCode) =>\n\t\t\tparseMoneyInput(input, { currency, scale: scaleOf(currency) }),\n\t\tzero: (currency: CurrencyCode) =>\n\t\t\tmoneyOfMinor(0n, currency, scaleOf(currency)),\n\t\tscaleOf,\n\t});\n}\n\n/**\n * Resolver over a plain currency-to-scale record\n * (`{ EUR: 2, JPY: 0 }`). The record is copied into a `Map` at\n * creation, so later mutation of the input and hostile own keys have\n * no effect.\n */\nexport function currencyScaleFromRecord(\n\trecord: Readonly<Record<string, number>>,\n): CurrencyScaleResolver {\n\tconst scales = new Map(Object.entries(record));\n\treturn (currency) => scales.get(currency);\n}\n\nconst CANONICAL_ISO_CODE = /^[A-Z]{3}$/;\n\n/**\n * Resolver backed by the runtime's own currency data (ICU via\n * `Intl.NumberFormat`), so no currency table ships with the kit or the\n * consumer. Resolves ONLY canonical uppercase ISO 4217 codes: Intl\n * itself would accept \"eur\", but a silent alias would let \"eur\"-Money\n * and \"EUR\"-Money circulate side by side until an operation throws\n * `MONEY_CURRENCY_MISMATCH`; here \"eur\" resolves to `undefined` and\n * fails fast as `UNKNOWN_CURRENCY` at the factory.\n *\n * A CONVENIENCE, not an enterprise source of truth: ICU resolves\n * well-formed but UNASSIGNED codes to its default of 2 rather than\n * `undefined`, and the data shifts with the runtime's ICU version.\n * Production money paths should pin a closed, versioned currency map\n * (`currencyScaleFromRecord`) or a calculation library's versioned\n * currency package; use this resolver for demos, prototypes, and\n * internal tooling.\n */\nexport function currencyScaleFromIntl(): CurrencyScaleResolver {\n\tconst cache = new Map<string, number | undefined>();\n\treturn (currency) => {\n\t\tif (typeof currency !== \"string\" || !CANONICAL_ISO_CODE.test(currency)) {\n\t\t\treturn undefined;\n\t\t}\n\t\tif (cache.has(currency)) return cache.get(currency);\n\t\tlet scale: number | undefined;\n\t\ttry {\n\t\t\tscale = new Intl.NumberFormat(\"en\", {\n\t\t\t\tstyle: \"currency\",\n\t\t\t\tcurrency,\n\t\t\t}).resolvedOptions().maximumFractionDigits;\n\t\t} catch {\n\t\t\tscale = undefined;\n\t\t}\n\t\t// Bounded: attacker-influenced currency strings must not turn\n\t\t// the cache into a leak. The legitimate key population is tiny;\n\t\t// past the cap, misses stay correct and simply pay Intl again.\n\t\tif (cache.size < CACHE_LIMIT) cache.set(currency, scale);\n\t\treturn scale;\n\t};\n}\n\nconst CACHE_LIMIT = 1_000;\n","import { assertMoney, type Money, moneyToDecimalString } from \"./money\";\n\n// Bounded like the resolver cache: attacker-influenced currency/scale\n// pairs (any well-formed code formats) must not leak memory. Past the\n// cap, formatting stays correct and pays construction again.\nconst CACHE_LIMIT = 1_000;\n\nfunction formatterFor(\n\tlocale: string,\n\tcurrency: string,\n\tscale: number,\n): Intl.NumberFormat {\n\treturn new Intl.NumberFormat(locale, {\n\t\tstyle: \"currency\",\n\t\tcurrency,\n\t\tminimumFractionDigits: scale,\n\t\tmaximumFractionDigits: scale,\n\t});\n}\n\nfunction formatDecimal(formatter: Intl.NumberFormat, money: Money): string {\n\t// Intl accepts decimal strings since NumberFormat v3; the cast only\n\t// bridges TS lib types that predate it. A number round-trip instead\n\t// would silently lose precision past 2^53.\n\treturn formatter.format(moneyToDecimalString(money) as unknown as number);\n}\n\n/**\n * Formats for display via `Intl.NumberFormat`, feeding the exact\n * decimal string (never a float), with the money's own scale as the\n * fraction-digit count. Presentation only: the output is\n * locale-dependent text and must never flow back into parsing,\n * storage, or arithmetic. Non-Money input fails loudly with\n * `INVALID_MONEY` before Intl is touched.\n *\n * The currency must be well-formed for `Intl` (ISO alpha-3); for\n * non-ISO codes, format `moneyToDecimalString(money)` yourself.\n */\nexport function formatMoney(money: Money, locale: string): string {\n\tassertMoney(money);\n\treturn formatDecimal(\n\t\tformatterFor(locale, money.currency, money.scale),\n\t\tmoney,\n\t);\n}\n\n/**\n * Binds the locale once and caches one `Intl.NumberFormat` per\n * currency/scale pair; constructing formatters is expensive, so use\n * this over `formatMoney` anywhere hot (lists, tables, exports).\n */\nexport function createMoneyFormatter(locale: string): (money: Money) => string {\n\tconst formatters = new Map<string, Intl.NumberFormat>();\n\treturn (money) => {\n\t\tassertMoney(money);\n\t\tconst key = `${money.currency}@${money.scale}`;\n\t\tlet formatter = formatters.get(key);\n\t\tif (!formatter) {\n\t\t\tformatter = formatterFor(locale, money.currency, money.scale);\n\t\t\tif (formatters.size < CACHE_LIMIT) formatters.set(key, formatter);\n\t\t}\n\t\treturn formatDecimal(formatter, money);\n\t};\n}\n","import { describeValue, InvalidMoneyError } from \"./errors\";\nimport {\n\tassertMoney,\n\ttype CurrencyCode,\n\ttype Money,\n\tmoneyOfMinor,\n} from \"./money\";\n\n/**\n * The currency part of {@link MoneySnapshotLike}: either a bare code or\n * a currency object as calculation libraries model it (code, numeric\n * base, exponent). Structural on purpose; the kit depends on no\n * calculation library.\n */\nexport type MoneySnapshotCurrencyLike =\n\t| string\n\t| {\n\t\t\treadonly code: string;\n\t\t\treadonly base?: number | bigint | ReadonlyArray<number | bigint>;\n\t\t\treadonly exponent?: number | bigint;\n\t };\n\n/**\n * The `{ amount, currency, scale }` shape that calculation libraries\n * expose when serializing their money objects (a `toJSON()` result,\n * typically). `scale` falls back to the currency's `exponent` when\n * absent; number and bigint calculators are both accepted.\n */\nexport interface MoneySnapshotLike {\n\treadonly amount: number | bigint;\n\treadonly currency: MoneySnapshotCurrencyLike;\n\treadonly scale?: number | bigint;\n}\n\n/**\n * The canonical snapshot {@link moneyToSnapshot} emits: number-based\n * with an explicit base-10 currency object, which is exactly what the\n * common calculation-library constructors accept.\n */\nexport interface MoneySnapshot {\n\treadonly amount: number;\n\treadonly currency: {\n\t\treadonly code: CurrencyCode;\n\t\treadonly base: 10;\n\t\treadonly exponent: number;\n\t};\n\treadonly scale: number;\n}\n\nfunction toScaleNumber(value: unknown, what: string): number {\n\tconst scale = typeof value === \"bigint\" ? Number(value) : value;\n\tif (typeof scale !== \"number\" || !Number.isSafeInteger(scale)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`snapshot ${what} must be an integer; got ${describeValue(value)}`,\n\t\t);\n\t}\n\treturn scale;\n}\n\n/**\n * Converts a calculation-library snapshot into canonical {@link Money}.\n * The anti-corruption checks live here so they run exactly once, at\n * the boundary:\n *\n * - non-decimal currencies are rejected (`base` other than 10; some\n * library currency packages model MGA/MRU with base 5, and pre-1971\n * GBP with a base array): their minor units do not map onto a\n * power-of-ten scale\n * - number amounts must be safe integers; fractional or beyond-2^53\n * amounts are rejected instead of silently corrupted\n * - bigint amounts pass through exactly\n *\n * Takes `unknown` on purpose: this is the trust boundary for foreign\n * library data, so callers never cast before validating (the\n * {@link MoneySnapshotLike} type documents the expected shape).\n */\nexport function moneyFromSnapshot(snapshot: unknown): Money {\n\tif (snapshot === null || typeof snapshot !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`money snapshot must be an object; got ${snapshot === null ? \"null\" : typeof snapshot}`,\n\t\t);\n\t}\n\tconst { amount, currency, scale } = snapshot as Partial<\n\t\tRecord<keyof MoneySnapshotLike, unknown>\n\t>;\n\tconst currencyObject = (\n\t\ttypeof currency === \"string\" ? { code: currency } : currency\n\t) as\n\t\t| Partial<Record<\"code\" | \"base\" | \"exponent\", unknown>>\n\t\t| null\n\t\t| undefined;\n\tif (currencyObject === null || typeof currencyObject !== \"object\") {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`snapshot currency must be a code or a currency object; got ${typeof currency}`,\n\t\t);\n\t}\n\tconst { base } = currencyObject;\n\tif (base !== undefined && base !== 10 && base !== 10n) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`only base-10 currencies map onto Money; got base ${describeValue(base)} for ${describeValue(currencyObject.code)}`,\n\t\t);\n\t}\n\tconst scaleSource = scale ?? currencyObject.exponent;\n\tif (scaleSource === undefined) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t\"money snapshot carries neither a scale nor a currency exponent\",\n\t\t);\n\t}\n\t// moneyOfMinor validates the currency code.\n\treturn moneyOfMinor(\n\t\ttoBigIntAmount(amount),\n\t\tcurrencyObject.code as CurrencyCode,\n\t\ttoScaleNumber(scaleSource, \"scale\"),\n\t);\n}\n\nfunction toBigIntAmount(amount: unknown): bigint {\n\tif (typeof amount === \"bigint\") return amount;\n\tif (typeof amount === \"number\" && Number.isSafeInteger(amount)) {\n\t\treturn BigInt(amount);\n\t}\n\tthrow new InvalidMoneyError(\n\t\t`snapshot amount must be a bigint or a safe integer; got ${describeValue(amount)}`,\n\t);\n}\n\n/**\n * Converts {@link Money} into the snapshot shape the common\n * calculation-library constructors accept. Number-based by design (the\n * libraries' default calculators are), so amounts past\n * `Number.MAX_SAFE_INTEGER` are rejected loudly; wire such amounts\n * into a bigint calculator directly from `money.amountMinor` instead.\n */\nexport function moneyToSnapshot(money: Money): MoneySnapshot {\n\tassertMoney(money);\n\tconst amount = Number(money.amountMinor);\n\tif (!Number.isSafeInteger(amount)) {\n\t\tthrow new InvalidMoneyError(\n\t\t\t`amountMinor ${money.amountMinor} exceeds Number.MAX_SAFE_INTEGER; use your library's bigint calculator and pass amountMinor directly`,\n\t\t);\n\t}\n\treturn {\n\t\tamount,\n\t\tcurrency: { code: money.currency, base: 10, exponent: money.scale },\n\t\tscale: money.scale,\n\t};\n}\n","import { err, ok, type Result } from \"@shirudo/result\";\nimport { InvalidMoneyError, MoneyPrecisionLossError } from \"./errors\";\nimport {\n\tassertValidCurrency,\n\tassertValidScale,\n\ttype Money,\n\tmoneyFromDto,\n} from \"./money\";\nimport { type ParseMoneyInputOptions, parseMoneyInput } from \"./parse\";\nimport { moneyFromSnapshot } from \"./snapshot\";\n\n/**\n * Result-returning counterparts to the three money boundary parsers,\n * for call sites that process many candidate values in one pass: a\n * CSV import, a batch migration, a message replay. Per-row try/catch\n * reads badly and, hand-rolled, usually catches too much; these\n * wrappers apply the `result-vs-throw` guide's discipline instead.\n *\n * The contract, mirroring `valueObjectOrIssues`: only the parser's DOCUMENTED\n * rejections of the INPUT become `Err`. Anything else, an assertion\n * firing, a typo-ed helper, a genuine bug, keeps propagating as a\n * throw, because a bug wrapped in `Err` is a bug silently counted as\n * a bad input row. That includes the parse OPTIONS: a broken scale\n * resolver is a bug in the caller's wiring, so it is validated\n * eagerly and throws instead of marking every row bad. The `Err`\n * types are exact per parser: the wire parsers reject only with\n * `InvalidMoneyError`, while decimal-string parsing can additionally\n * refuse over-precise input with `MoneyPrecisionLossError`.\n *\n * `Result`, `ok`, and `err` come from `@shirudo/result` (already a\n * peer dependency); import them from there to work with the branches.\n */\n\n// Deliberately NOT @shirudo/result's `fromThrowable`: its error\n// mapper is typed as a total unknown -> E conversion, and rethrowing\n// from it would lean on an undocumented implementation detail. Here\n// the rethrow of unexpected errors is a first-class branch, and the\n// type-guard parameter yields the exact per-parser Err union.\nfunction tryCatching<T, E extends Error>(\n\tparse: () => T,\n\tisExpected: (error: unknown) => error is E,\n): Result<T, E> {\n\ttry {\n\t\treturn ok(parse());\n\t} catch (error) {\n\t\tif (isExpected(error)) return err(error);\n\t\tthrow error;\n\t}\n}\n\n/**\n * {@link parseMoneyInput} as a `Result`: `Err` for the documented\n * rejections (malformed input as `InvalidMoneyError`, over-precise\n * input as `MoneyPrecisionLossError`), a throw for everything else.\n */\nexport function tryParseMoneyInput(\n\tinput: unknown,\n\toptions: ParseMoneyInputOptions,\n): Result<Money, InvalidMoneyError | MoneyPrecisionLossError> {\n\t// Options are wiring, not data: validated OUTSIDE the wrapper so a\n\t// bad scale or currency throws instead of becoming a per-row Err.\n\tassertValidScale(options.scale);\n\tassertValidCurrency(options.currency);\n\treturn tryCatching(\n\t\t() => parseMoneyInput(input, options),\n\t\t(error): error is InvalidMoneyError | MoneyPrecisionLossError =>\n\t\t\terror instanceof InvalidMoneyError ||\n\t\t\terror instanceof MoneyPrecisionLossError,\n\t);\n}\n\n/**\n * {@link moneyFromDto} as a `Result`: `Err` for the documented\n * rejection (`InvalidMoneyError`), a throw for everything else.\n */\nexport function tryMoneyFromDto(\n\tdto: unknown,\n): Result<Money, InvalidMoneyError> {\n\treturn tryCatching(\n\t\t() => moneyFromDto(dto),\n\t\t(error): error is InvalidMoneyError => error instanceof InvalidMoneyError,\n\t);\n}\n\n/**\n * {@link moneyFromSnapshot} as a `Result`: `Err` for the documented\n * rejection (`InvalidMoneyError`), a throw for everything else.\n */\nexport function tryMoneyFromSnapshot(\n\tsnapshot: unknown,\n): Result<Money, InvalidMoneyError> {\n\treturn tryCatching(\n\t\t() => moneyFromSnapshot(snapshot),\n\t\t(error): error is InvalidMoneyError => error instanceof InvalidMoneyError,\n\t);\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { G as KitWiringError, f as DomainError } from "./chunks/kit-errors.js";
|
|
2
|
+
//#region src/domain/state-machine/contracts.d.ts
|
|
3
|
+
/** Base shape for every input accepted by a domain state machine. */
|
|
4
|
+
type DomainMachineInput = {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
};
|
|
7
|
+
/** Recursively readonly view of data accepted by the domain state machine. */
|
|
8
|
+
type DomainMachineReadonly<TValue> = TValue extends bigint | boolean | null | number | string | symbol | undefined ? TValue : TValue extends ((...args: never[]) => unknown) ? TValue : TValue extends readonly unknown[] ? { readonly [TKey in keyof TValue]: DomainMachineReadonly<TValue[TKey]>; } : TValue extends object ? { readonly [TKey in keyof TValue]: DomainMachineReadonly<TValue[TKey]>; } : TValue;
|
|
9
|
+
/** Immutable state and context value suitable for persistence. */
|
|
10
|
+
type DomainMachineSnapshot<TState extends string, TContext> = {
|
|
11
|
+
readonly state: TState;
|
|
12
|
+
readonly context: DomainMachineReadonly<TContext>;
|
|
13
|
+
};
|
|
14
|
+
/** Optional context replacement and requested external work from a reducer. */
|
|
15
|
+
type DomainTransitionResult<TContext, TOutput> = {
|
|
16
|
+
readonly context?: TContext;
|
|
17
|
+
readonly outputs?: readonly TOutput[];
|
|
18
|
+
};
|
|
19
|
+
/** A guard either allows, generically rejects, or returns a domain-specific rejection. */
|
|
20
|
+
type DomainTransitionGuardResult = boolean | DomainError;
|
|
21
|
+
/** A guarded state change and its optional pure context reducer. */
|
|
22
|
+
type DomainTransition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput, TSource extends TState = TState> = {
|
|
23
|
+
readonly target: TState;
|
|
24
|
+
/**
|
|
25
|
+
* Must be synchronous, deterministic, and side-effect-free. Return a
|
|
26
|
+
* `DomainError` to provide a typed rejection that `can()` treats as false and
|
|
27
|
+
* `dispatch()` throws unchanged.
|
|
28
|
+
*/
|
|
29
|
+
readonly guard?: (input: {
|
|
30
|
+
readonly state: TSource;
|
|
31
|
+
readonly context: DomainMachineReadonly<TContext>;
|
|
32
|
+
readonly input: DomainMachineReadonly<TInput>;
|
|
33
|
+
}) => DomainTransitionGuardResult;
|
|
34
|
+
/** Must be synchronous, deterministic, and side-effect-free. */
|
|
35
|
+
readonly reduce?: (input: {
|
|
36
|
+
readonly state: TSource;
|
|
37
|
+
readonly context: DomainMachineReadonly<TContext>;
|
|
38
|
+
readonly input: DomainMachineReadonly<TInput>;
|
|
39
|
+
}) => DomainTransitionResult<TContext, TOutput> | undefined;
|
|
40
|
+
};
|
|
41
|
+
/** Configuration for one named control state. */
|
|
42
|
+
type DomainStateNode<TState extends string, TContext, TInput extends DomainMachineInput, TOutput, TName extends TState = TState> = {
|
|
43
|
+
readonly terminal?: boolean;
|
|
44
|
+
/** State-specific context invariant. Must be synchronous, deterministic, and side-effect-free. */
|
|
45
|
+
readonly validateContext?: (input: {
|
|
46
|
+
readonly state: TName;
|
|
47
|
+
readonly context: DomainMachineReadonly<TContext>;
|
|
48
|
+
}) => boolean;
|
|
49
|
+
readonly on?: { readonly [TType in TInput["type"]]?: DomainTransition<TState, TContext, Extract<TInput, {
|
|
50
|
+
readonly type: TType;
|
|
51
|
+
}>, TOutput, TName>; };
|
|
52
|
+
};
|
|
53
|
+
/** Complete, finite domain lifecycle definition. */
|
|
54
|
+
type DomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput = never> = {
|
|
55
|
+
readonly initial: TState;
|
|
56
|
+
readonly initialContext: () => TContext;
|
|
57
|
+
/** Must be synchronous, deterministic, and side-effect-free. */
|
|
58
|
+
readonly validateSnapshot?: (snapshot: DomainMachineSnapshot<TState, TContext>) => boolean;
|
|
59
|
+
readonly states: { readonly [TName in TState]: DomainStateNode<TState, TContext, TInput, TOutput, TName>; };
|
|
60
|
+
};
|
|
61
|
+
/** Result of a successful transition. */
|
|
62
|
+
type DomainTransitionOutcome<TState extends string, TContext, TOutput> = {
|
|
63
|
+
readonly from: TState;
|
|
64
|
+
readonly to: TState;
|
|
65
|
+
readonly snapshot: DomainMachineSnapshot<TState, TContext>;
|
|
66
|
+
readonly outputs: readonly DomainMachineReadonly<TOutput>[];
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region src/domain/state-machine/analyzer.d.ts
|
|
70
|
+
type DomainMachineDefinitionDiagnostic<TState extends string> = {
|
|
71
|
+
readonly code: "unreachable-state";
|
|
72
|
+
readonly state: TState;
|
|
73
|
+
} | {
|
|
74
|
+
readonly code: "structural-dead-end";
|
|
75
|
+
readonly state: TState;
|
|
76
|
+
} | {
|
|
77
|
+
readonly code: "no-terminal-path";
|
|
78
|
+
readonly state: TState;
|
|
79
|
+
};
|
|
80
|
+
type DomainMachineTransitionDescription<TState extends string, TInputType extends string> = {
|
|
81
|
+
readonly state: TState;
|
|
82
|
+
readonly inputType: TInputType;
|
|
83
|
+
readonly target: TState;
|
|
84
|
+
readonly guarded: boolean;
|
|
85
|
+
};
|
|
86
|
+
type DomainMachineDefinitionAnalysis<TState extends string, TInputType extends string> = {
|
|
87
|
+
readonly diagnostics: readonly DomainMachineDefinitionDiagnostic<TState>[];
|
|
88
|
+
readonly transitions: readonly DomainMachineTransitionDescription<TState, TInputType>[];
|
|
89
|
+
/** States reachable when every guard is assumed to allow its transition. */
|
|
90
|
+
readonly structurallyReachableStates: readonly TState[];
|
|
91
|
+
/** States with a graph path to a terminal state when every guard is assumed to allow it. */
|
|
92
|
+
readonly statesWithTerminalPath: readonly TState[];
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* Inspects the declarative transition graph without executing definition callbacks.
|
|
96
|
+
* Guarded edges are treated as possible edges, so diagnostics never claim more
|
|
97
|
+
* runtime reachability than the static graph can prove.
|
|
98
|
+
*/
|
|
99
|
+
declare function analyzeDomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>): DomainMachineDefinitionAnalysis<TState, TInput["type"]>;
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/domain/state-machine/definition.d.ts
|
|
102
|
+
declare const preparedDefinitionBrand: unique symbol;
|
|
103
|
+
/**
|
|
104
|
+
* A machine definition that `prepareDomainMachineDefinition` has
|
|
105
|
+
* validated, defensively copied, and deeply frozen. Assignable wherever
|
|
106
|
+
* a plain `DomainMachineDefinition` is accepted; the reverse does NOT
|
|
107
|
+
* hold (the brand is required), so an API that demands a prepared
|
|
108
|
+
* definition rejects raw ones at compile time. The pure functions and
|
|
109
|
+
* the `DomainStateMachine` constructor recognize prepared definitions
|
|
110
|
+
* at runtime and skip their per-call validate-and-copy.
|
|
111
|
+
*/
|
|
112
|
+
type PreparedDomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput = never> = DomainMachineDefinition<TState, TContext, TInput, TOutput> & {
|
|
113
|
+
readonly [preparedDefinitionBrand]: true;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* Validates and stabilizes a machine definition ONCE, for repeated use
|
|
117
|
+
* with the pure functions. Without it, `transitionDomainState` and
|
|
118
|
+
* `canTransitionDomainState` re-validate and defensively re-copy the
|
|
119
|
+
* WHOLE definition on every call (the documented safety of the raw
|
|
120
|
+
* path); on a hot dispatch path that is avoidable O(definition) work.
|
|
121
|
+
* The `DomainStateMachine` class does the equivalent once in its
|
|
122
|
+
* constructor; this export brings the same amortization to pure-API
|
|
123
|
+
* users:
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* const prepared = prepareDomainMachineDefinition(orderLifecycle);
|
|
127
|
+
* // per dispatch: no re-validation, no definition copy
|
|
128
|
+
* const outcome = transitionDomainState(prepared, snapshot, input);
|
|
129
|
+
* ```
|
|
130
|
+
*
|
|
131
|
+
* The returned definition is a deeply frozen copy, isolated from later
|
|
132
|
+
* mutation of the input object. Preparing an already-prepared
|
|
133
|
+
* definition returns it unchanged.
|
|
134
|
+
*/
|
|
135
|
+
declare function prepareDomainMachineDefinition<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>): PreparedDomainMachineDefinition<TState, TContext, TInput, TOutput>;
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/domain/state-machine/errors.d.ts
|
|
138
|
+
/** No transition is defined for the input in the current state. */
|
|
139
|
+
declare class InvalidDomainTransitionError extends DomainError<"INVALID_DOMAIN_TRANSITION"> {
|
|
140
|
+
readonly state: string;
|
|
141
|
+
readonly inputType: string;
|
|
142
|
+
constructor(state: string, inputType: string);
|
|
143
|
+
}
|
|
144
|
+
/** A defined transition was rejected by its domain guard. */
|
|
145
|
+
declare class DomainTransitionGuardRejectedError extends DomainError<"DOMAIN_TRANSITION_GUARD_REJECTED"> {
|
|
146
|
+
readonly state: string;
|
|
147
|
+
readonly inputType: string;
|
|
148
|
+
constructor(state: string, inputType: string);
|
|
149
|
+
}
|
|
150
|
+
/** The machine definition violates its runtime contract. */
|
|
151
|
+
declare class InvalidDomainMachineDefinitionError extends KitWiringError<"INVALID_DOMAIN_MACHINE_DEFINITION"> {
|
|
152
|
+
constructor(message: string, cause?: unknown);
|
|
153
|
+
}
|
|
154
|
+
/** Context contains unsupported or unsafe runtime data. */
|
|
155
|
+
declare class InvalidDomainMachineContextError extends KitWiringError<"INVALID_DOMAIN_MACHINE_CONTEXT"> {
|
|
156
|
+
constructor(message: string, cause?: unknown);
|
|
157
|
+
}
|
|
158
|
+
/** A supplied or produced snapshot is malformed or violates invariants. */
|
|
159
|
+
declare class InvalidDomainMachineSnapshotError extends KitWiringError<"INVALID_DOMAIN_MACHINE_SNAPSHOT"> {
|
|
160
|
+
constructor(message: string, cause?: unknown);
|
|
161
|
+
}
|
|
162
|
+
/** An input is malformed or contains unsupported runtime data. */
|
|
163
|
+
declare class InvalidDomainMachineInputError extends KitWiringError<"INVALID_DOMAIN_MACHINE_INPUT"> {
|
|
164
|
+
constructor(message: string, cause?: unknown);
|
|
165
|
+
}
|
|
166
|
+
/** A guard returned a value other than `boolean` or `DomainError`. */
|
|
167
|
+
declare class InvalidDomainTransitionGuardResultError extends KitWiringError<"INVALID_DOMAIN_TRANSITION_GUARD_RESULT"> {
|
|
168
|
+
constructor(message: string, cause?: unknown);
|
|
169
|
+
}
|
|
170
|
+
/** A reducer returned a malformed result or unsupported output data. */
|
|
171
|
+
declare class InvalidDomainTransitionResultError extends KitWiringError<"INVALID_DOMAIN_TRANSITION_RESULT"> {
|
|
172
|
+
constructor(message: string, cause?: unknown);
|
|
173
|
+
}
|
|
174
|
+
/** A callback attempted to evaluate the same stateful machine recursively. */
|
|
175
|
+
declare class ReentrantDomainStateMachineEvaluationError extends KitWiringError<"REENTRANT_DOMAIN_STATE_MACHINE_EVALUATION"> {
|
|
176
|
+
constructor();
|
|
177
|
+
}
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/domain/state-machine/transition.d.ts
|
|
180
|
+
/** Creates and validates a fresh initial snapshot from a machine definition. */
|
|
181
|
+
declare function createInitialDomainMachineSnapshot<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>): DomainMachineSnapshot<TState, TContext>;
|
|
182
|
+
/**
|
|
183
|
+
* Checks whether an input currently has an allowed transition.
|
|
184
|
+
*
|
|
185
|
+
* Returns `false` for missing transitions, terminal states, rejected guards,
|
|
186
|
+
* and inputs without an own string `type` property. Invalid payload data for a
|
|
187
|
+
* matching transition and broken guard code still throw structured errors.
|
|
188
|
+
*/
|
|
189
|
+
declare function canTransitionDomainState<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>, snapshot: DomainMachineSnapshot<TState, TContext>, input: TInput): boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Applies one input without mutating the input definition or snapshot.
|
|
192
|
+
*
|
|
193
|
+
* @throws {@link InvalidDomainTransitionError} when no transition is defined.
|
|
194
|
+
* @throws {@link DomainTransitionGuardRejectedError} when its guard rejects.
|
|
195
|
+
* @throws A concrete `DomainError` returned by a rejecting guard.
|
|
196
|
+
*/
|
|
197
|
+
declare function transitionDomainState<TState extends string, TContext, TInput extends DomainMachineInput, TOutput>(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>, snapshot: DomainMachineSnapshot<TState, TContext>, input: TInput): DomainTransitionOutcome<TState, TContext, TOutput>;
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/domain/state-machine/domain-state-machine.d.ts
|
|
200
|
+
/**
|
|
201
|
+
* Stateful convenience wrapper around the pure domain transition functions.
|
|
202
|
+
*
|
|
203
|
+
* Persist {@link snapshot}, not the machine instance. Pass a restored snapshot
|
|
204
|
+
* to the second constructor overload to validate and reconstitute a machine.
|
|
205
|
+
*/
|
|
206
|
+
declare class DomainStateMachine<TState extends string, TContext, TInput extends DomainMachineInput, TOutput = never> {
|
|
207
|
+
#private;
|
|
208
|
+
private readonly definition;
|
|
209
|
+
constructor(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>);
|
|
210
|
+
constructor(definition: DomainMachineDefinition<TState, TContext, TInput, TOutput>, snapshot: DomainMachineSnapshot<TState, TContext> | undefined);
|
|
211
|
+
/** Returns a defensive, deeply frozen copy of the current snapshot. */
|
|
212
|
+
get snapshot(): DomainMachineSnapshot<TState, TContext>;
|
|
213
|
+
/** Current named control state. */
|
|
214
|
+
get state(): TState;
|
|
215
|
+
/** Current deeply readonly context. */
|
|
216
|
+
get context(): DomainMachineReadonly<TContext>;
|
|
217
|
+
/** Whether the current state permanently forbids outgoing transitions. */
|
|
218
|
+
isTerminal(): boolean;
|
|
219
|
+
/** Checks a transition without changing the current snapshot. */
|
|
220
|
+
can(input: TInput): boolean;
|
|
221
|
+
/** Applies an input and advances the current snapshot on success. */
|
|
222
|
+
dispatch(input: TInput): DomainTransitionOutcome<TState, TContext, TOutput>;
|
|
223
|
+
private evaluate;
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
export { type DomainMachineDefinition, type DomainMachineDefinitionAnalysis, type DomainMachineDefinitionDiagnostic, type DomainMachineInput, type DomainMachineReadonly, type DomainMachineSnapshot, type DomainMachineTransitionDescription, DomainStateMachine, type DomainStateNode, type DomainTransition, DomainTransitionGuardRejectedError, type DomainTransitionGuardResult, type DomainTransitionOutcome, type DomainTransitionResult, InvalidDomainMachineContextError, InvalidDomainMachineDefinitionError, InvalidDomainMachineInputError, InvalidDomainMachineSnapshotError, InvalidDomainTransitionError, InvalidDomainTransitionGuardResultError, InvalidDomainTransitionResultError, type PreparedDomainMachineDefinition, ReentrantDomainStateMachineEvaluationError, analyzeDomainMachineDefinition, canTransitionDomainState, createInitialDomainMachineSnapshot, prepareDomainMachineDefinition, transitionDomainState };
|
|
227
|
+
//# sourceMappingURL=state-machine.d.ts.map
|