@zelgadis87/utils-core 5.4.4 → 5.4.5

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.
@@ -1145,6 +1145,26 @@ declare const Operation: {
1145
1145
  * @returns A tuple of [successes, failures]
1146
1146
  */
1147
1147
  partition: <T, E = Error>(results: TReadableArray<TOperation<T, E>>) => [TOperationSuccess<T>[], TOperationFailure<E>[]];
1148
+ /**
1149
+ * Transforms the success value of an operation using the provided function.
1150
+ * If the operation is a failure, it is returned unchanged (same reference).
1151
+ * The transformation function is never called on a failure.
1152
+ *
1153
+ * @template T - The input success data type
1154
+ * @template U - The output success data type
1155
+ * @template E - The error type (preserved exactly)
1156
+ * @param op - The operation result to transform
1157
+ * @param fn - Pure function applied to the success value
1158
+ * @returns A new success with the transformed value, or the original failure
1159
+ *
1160
+ * @example
1161
+ * Operation.map(Operation.ok(1), x => x * 2);
1162
+ * // => { success: true, data: 2 }
1163
+ *
1164
+ * Operation.map(Operation.ko('err'), x => x * 2);
1165
+ * // => { success: false, error: 'err' }
1166
+ */
1167
+ map: <T, U, E>(op: TOperation<T, E>, fn: (data: T) => U) => TOperation<U, E>;
1148
1168
  combine: typeof combine;
1149
1169
  };
1150
1170
 
@@ -1583,10 +1603,168 @@ type TPossibleVersion<XStar extends TUpgradable> = XStar["$version"] & number;
1583
1603
  type TPossibleFromVersion<XStar extends TUpgradable, XLatest extends XStar> = Exclude<TPossibleVersion<XStar>, XLatest["$version"]>;
1584
1604
  type TUpgradeFunction<XStar extends TUpgradable, XFrom extends XStar, XTo extends XStar> = TFunction<Readonly<TVersionMap<XStar>[XFrom["$version"]]>, TPromisable<TVersionMap<XStar>[XTo["$version"]]>>;
1585
1605
 
1606
+ type TTransitionRecord = readonly [from: number, to: number];
1607
+ type TContainsTransition<T extends readonly TTransitionRecord[], From extends number, To extends number> = T extends readonly [infer First, ...infer Rest] ? First extends readonly [From, To] ? true : TContainsTransition<Rest & readonly TTransitionRecord[], From, To> : false;
1608
+ type TAddTransition<T extends readonly TTransitionRecord[], From extends number, To extends number> = TContainsTransition<T, From, To> extends true ? never : readonly [...T, readonly [From, To]];
1609
+ /**
1610
+ * Builder for creating type-safe DataUpgrader instances with compile-time safety.
1611
+ *
1612
+ * Prevents at compile-time:
1613
+ * - Missing upgrade paths (incomplete version coverage)
1614
+ * - Duplicate transition definitions
1615
+ * - Backward transitions (e.g., version 2 → 1)
1616
+ *
1617
+ * @example
1618
+ * ```typescript
1619
+ * // Step 1: Define all historical versions with $version discriminator
1620
+ * type V1 = { name: string; $version: 1 };
1621
+ * type V2 = { name: string; age: number; $version: 2 };
1622
+ * type V3 = { name: string; age: number; email: string; $version: 3 };
1623
+ *
1624
+ * // Step 2: Build the upgrader with type-safe transitions
1625
+ * const upgrader = DataUpgraderBuilder.start<V1>()
1626
+ * .addTransition<V1, V2>(1, 2, async (data) => ({
1627
+ * ...data,
1628
+ * age: 0,
1629
+ * $version: 2
1630
+ * }))
1631
+ * .addTransition<V2, V3>(2, 3, async (data) => ({
1632
+ * ...data,
1633
+ * email: "",
1634
+ * $version: 3
1635
+ * }))
1636
+ * .addShortcut<V1, V3>(1, 3, async (data) => ({
1637
+ * // Optional: Direct path for efficiency
1638
+ * ...data,
1639
+ * age: 0,
1640
+ * email: "",
1641
+ * $version: 3
1642
+ * }))
1643
+ * .build<V3>(3);
1644
+ *
1645
+ * // Step 3: Use the upgrader
1646
+ * async function loadData(json: string): Promise<V3> {
1647
+ * const data = JSON.parse(json);
1648
+ * return await upgrader.upgrade(data);
1649
+ * }
1650
+ *
1651
+ * // Works with any version:
1652
+ * const v1 = await loadData('{"name":"Alice","$version":1}'); // → V3
1653
+ * const v2 = await loadData('{"name":"Bob","age":25,"$version":2}'); // → V3
1654
+ * const v3 = await loadData('{"name":"Carol","age":30,"email":"c@example.com","$version":3}'); // → V3
1655
+ * ```
1656
+ */
1657
+ declare class DataUpgraderBuilder<VLowest extends TUpgradable, VUnion extends TUpgradable = VLowest, TTransitions extends readonly TTransitionRecord[] = readonly []> {
1658
+ private transitions;
1659
+ private constructor();
1660
+ /** Starts building a new DataUpgrader from the lowest version. */
1661
+ static start<V1 extends TUpgradable>(): DataUpgraderBuilder<V1, V1, readonly []>;
1662
+ /**
1663
+ * Adds a sequential transition from the current version to the next version.
1664
+ * Prevents backward transitions and duplicates at compile-time.
1665
+ *
1666
+ * @example
1667
+ * ```typescript
1668
+ * builder.addTransition<V1, V2>(1, 2, async (d) => ({ ...d, extra: 0, $version: 2 }))
1669
+ * ```
1670
+ */
1671
+ addTransition<VNext extends TUpgradable, VCurrent extends TUpgradable & VUnion, NewUnion extends TUpgradable = VUnion | VNext, Current extends number = VCurrent["$version"], Next extends number = VNext["$version"], NewTransitions extends readonly TTransitionRecord[] = TAddTransition<TTransitions, Current, Next>>(fromVersion: Current, toVersion: Next, apply: (data: VCurrent) => Promise<VNext> | VNext): DataUpgraderBuilder<VLowest, NewUnion, NewTransitions>;
1672
+ /**
1673
+ * Adds a shortcut transition between arbitrary versions.
1674
+ * Both versions must already be in the accumulated union type.
1675
+ *
1676
+ * @example
1677
+ * ```typescript
1678
+ * builder.addShortcut<V1, V3>(1, 3, async (d) => ({ ...d, extra: 0, flag: false, $version: 3 }))
1679
+ * ```
1680
+ */
1681
+ addShortcut<VFrom extends TUpgradable & VUnion, VTo extends TUpgradable & VUnion, From extends number = VFrom["$version"], To extends number = VTo["$version"], NewTransitions extends readonly TTransitionRecord[] = TAddTransition<TTransitions, From, To>>(fromVersion: From, toVersion: To, apply: (data: VFrom) => Promise<VTo> | VTo): DataUpgraderBuilder<VLowest, VUnion, NewTransitions>;
1682
+ /**
1683
+ * Builds the DataUpgrader with the specified latest version.
1684
+ * The latest version must be in the accumulated union type.
1685
+ *
1686
+ * @example
1687
+ * ```typescript
1688
+ * const upgrader = builder.build<V3>(3);
1689
+ * ```
1690
+ */
1691
+ build<VLatest extends TUpgradable & VUnion>(latestVersion: VLatest["$version"]): DataUpgrader<VUnion, VLatest>;
1692
+ /** @internal */
1693
+ getTransitions(): Record<number, Record<number, {
1694
+ from: number;
1695
+ to: number;
1696
+ apply: (data: unknown) => Promise<unknown>;
1697
+ }>>;
1698
+ }
1699
+
1586
1700
  declare const VERSION_FIELD = "$version";
1587
1701
  interface IDataUpgrader<XStar extends TUpgradable, XLatest extends XStar> {
1702
+ /**
1703
+ * Upgrades data from any version to the latest version.
1704
+ *
1705
+ * @param data - Data object from any version (must contain valid $version field)
1706
+ * @returns Promise resolving to data at the latest version
1707
+ */
1588
1708
  upgrade(data: XStar): Promise<XLatest>;
1589
1709
  }
1710
+ /**
1711
+ * Versioned data migration system for transparently upgrading serialized data to the latest version.
1712
+ *
1713
+ * DataUpgrader manages the migration of data structures across multiple versions, automatically
1714
+ * finding the shortest upgrade path and applying transformations. This is particularly useful for
1715
+ * handling persisted data that may be in any historical format.
1716
+ *
1717
+ * **Key Use Case:** Reading serialized data of unknown version and migrating it transparently.
1718
+ *
1719
+ * @example
1720
+ * ```typescript
1721
+ * // Step 1: Define all historical versions with $version discriminator
1722
+ * type TSerializedData_V1 = { name: string; $version: 1 };
1723
+ * type TSerializedData_V2 = { name: string; age: number; $version: 2 };
1724
+ * type TSerializedData_V3 = { name: string; age: number; email: string; $version: 3 };
1725
+ *
1726
+ * // Step 2: Create union type and current version type
1727
+ * type TSerializedData_Vstar = TSerializedData_V1 | TSerializedData_V2 | TSerializedData_V3;
1728
+ * type TSerializedData = TSerializedData_V3;
1729
+ *
1730
+ * // Step 3: Create upgrader with transition functions
1731
+ * const upgrader = DataUpgrader.create<TSerializedData_Vstar, TSerializedData>(3)
1732
+ * .addTransition(1, 2, async (data: TSerializedData_V1) => ({
1733
+ * ...data,
1734
+ * age: 0,
1735
+ * $version: 2
1736
+ * }))
1737
+ * .addTransition(2, 3, async (data: TSerializedData_V2) => ({
1738
+ * ...data,
1739
+ * email: "",
1740
+ * $version: 3
1741
+ * }))
1742
+ * .addTransition(1, 3, async (data: TSerializedData_V1) => ({
1743
+ * // Optional: Direct path for efficiency
1744
+ * ...data,
1745
+ * age: 0,
1746
+ * email: "",
1747
+ * $version: 3
1748
+ * }));
1749
+ *
1750
+ * // Step 4: Use in data loading - handles any version transparently
1751
+ * async function loadData(json: string): Promise<TSerializedData> {
1752
+ * const data = parseJson<TSerializedData_Vstar>(json);
1753
+ * return await upgrader.upgrade(data);
1754
+ * }
1755
+ *
1756
+ * // Works with any version:
1757
+ * const v1 = await loadData('{"name":"Alice","$version":1}'); // → V3
1758
+ * const v2 = await loadData('{"name":"Bob","age":25,"$version":2}'); // → V3
1759
+ * const v3 = await loadData('{"name":"Carol","age":30,"email":"c@example.com","$version":3}'); // → V3
1760
+ * ```
1761
+ *
1762
+ * @typeParam X - Union type of all possible data versions (must extend TUpgradable)
1763
+ * @typeParam XLatest - The latest version type (must extend X)
1764
+ *
1765
+ * @see {@link isUpgradable | Type guard for checking if data is upgradable}
1766
+ * @see {@link DataUpgrader.Errors | Error types for upgrade failures}
1767
+ */
1590
1768
  declare class DataUpgrader<X extends TUpgradable, XLatest extends X> implements IDataUpgrader<X, XLatest> {
1591
1769
  private latestVersion;
1592
1770
  private transitions;
@@ -1595,6 +1773,11 @@ declare class DataUpgrader<X extends TUpgradable, XLatest extends X> implements
1595
1773
  upgrade(data: X): Promise<XLatest>;
1596
1774
  isLatestVersion(data: X): data is XLatest;
1597
1775
  static create<X extends TUpgradable, XLatestVersion extends X>(latest: XLatestVersion[typeof VERSION_FIELD]): DataUpgrader<X, XLatestVersion>;
1776
+ /**
1777
+ * Creates a DataUpgrader from a DataUpgraderBuilder instance.
1778
+ * @internal
1779
+ */
1780
+ static fromBuilder<X extends TUpgradable, XLatestVersion extends X>(builder: DataUpgraderBuilder<any, X, any>, latestVersion: XLatestVersion[typeof VERSION_FIELD]): DataUpgrader<X, XLatestVersion>;
1598
1781
  static Errors: {
1599
1782
  readonly EmptyUpgrade: typeof EmptyUpgradeError;
1600
1783
  readonly UnavailableUpgrade: typeof UnavailableUpgradeError;
@@ -1602,5 +1785,5 @@ declare class DataUpgrader<X extends TUpgradable, XLatest extends X> implements
1602
1785
  }
1603
1786
  declare function isUpgradable(obj: TJsonSerializable): obj is TUpgradable;
1604
1787
 
1605
- export { Cached, DataUpgrader, Deferred, DeferredCanceledError, ErrorCannotInstantiatePresentOptionalWithEmptyValue, ErrorGetEmptyOptional, ErrorSetEmptyOptional, Lazy, LazyAsync, LazyDictionary, Logger, NEVER, NonExhaustiveSwitchError, Operation, OperationAggregateError, Optional, PredicateBuilder, RandomTimeDuration, RateThrottler, Semaphore, Sorter, StringParts, TimeDuration, TimeFrequency, TimeInstant, TimeRange, TimeUnit, TimeoutError, alwaysFalse, alwaysTrue, and, arrayGet, arrayIncludes, asError, asPromise, average, averageBy, awaitAtMost, capitalizeWord, clamp, clampInt0_100, constant, constantFalse, constantNull, constantOne, constantTrue, constantUndefined, constantZero, cssDeclarationRulesDictionaryToCss, decrement, decrementBy, delayPromise, dictToEntries, dictToList, divideBy, ellipsis, ensureArray, ensureDefined, ensureNegativeNumber, ensureNonNegativeNumber, ensureNonPositiveNumber, ensurePositiveNumber, ensureReadableArray, entriesToDict, entriesToEntries, entriesToList, extendArray, extendArrayWith, fill, fillWith, filterMap, filterMapReduce, filterWithTypePredicate, findInArray, findIndexInArray, first, flatMapTruthys, getCauseMessageFromError, getCauseStackFromError, getMessageFromError, getStackFromError, groupByBoolean, groupByBooleanWith, groupByNumber, groupByNumberWith, groupByString, groupByStringWith, groupBySymbol, groupBySymbolWith, hashCode, havingMaxBy, havingMinBy, head, identity, ifDefined, ifNullOrUndefined, iff, includes, increment, incrementBy, indexByNumber, indexByNumberWith, indexByString, indexByStringWith, indexBySymbol, indexBySymbolWith, indexByWith, isAllowedTimeDuration, isArray, isDefined, isEmpty, isError, isFalse, isFunction, isNegativeNumber, isNullOrUndefined, isNullOrUndefinedOrEmpty, isNumber, isPositiveNumber, isString, isTimeInstant, isTrue, isUpgradable, isZero, jsonCloneDeep, last, listToDict, mapDefined, mapEntries, mapFirstTruthy, mapTruthys, max, maxBy, min, minBy, multiplyBy, noop, not, omitFromJsonObject, or, pad, padLeft, padRight, parseJson, parseTimeInstantBasicComponents, parseTimeInstantComponents, partition, pick, pipedInvoke, pipedInvokeFromArray, pluralize, promiseSequence, randomDecimalInInterval, randomId, randomIntegerInInterval, randomNumberInInterval, randomPick, randomPicks, range, repeat, reverse, round, roundAwayFromZero, roundToLower, roundToNearest, roundToUpper, roundTowardsZero, shallowArrayEquals, shallowRecordEquals, sortedArray, splitWords, stringToNumber, stringifyJson, sum, sumBy, tail, throttle, throwIfNullOrUndefined, transformCssDictionary, tryToParseJson, tryToParseNumber, uniq, uniqBy, uniqByKey, unzip, upsert, withTryCatch, withTryCatchAsync, wrapWithString, xor, zip };
1788
+ export { Cached, DataUpgrader, DataUpgraderBuilder, Deferred, DeferredCanceledError, ErrorCannotInstantiatePresentOptionalWithEmptyValue, ErrorGetEmptyOptional, ErrorSetEmptyOptional, Lazy, LazyAsync, LazyDictionary, Logger, NEVER, NonExhaustiveSwitchError, Operation, OperationAggregateError, Optional, PredicateBuilder, RandomTimeDuration, RateThrottler, Semaphore, Sorter, StringParts, TimeDuration, TimeFrequency, TimeInstant, TimeRange, TimeUnit, TimeoutError, alwaysFalse, alwaysTrue, and, arrayGet, arrayIncludes, asError, asPromise, average, averageBy, awaitAtMost, capitalizeWord, clamp, clampInt0_100, constant, constantFalse, constantNull, constantOne, constantTrue, constantUndefined, constantZero, cssDeclarationRulesDictionaryToCss, decrement, decrementBy, delayPromise, dictToEntries, dictToList, divideBy, ellipsis, ensureArray, ensureDefined, ensureNegativeNumber, ensureNonNegativeNumber, ensureNonPositiveNumber, ensurePositiveNumber, ensureReadableArray, entriesToDict, entriesToEntries, entriesToList, extendArray, extendArrayWith, fill, fillWith, filterMap, filterMapReduce, filterWithTypePredicate, findInArray, findIndexInArray, first, flatMapTruthys, getCauseMessageFromError, getCauseStackFromError, getMessageFromError, getStackFromError, groupByBoolean, groupByBooleanWith, groupByNumber, groupByNumberWith, groupByString, groupByStringWith, groupBySymbol, groupBySymbolWith, hashCode, havingMaxBy, havingMinBy, head, identity, ifDefined, ifNullOrUndefined, iff, includes, increment, incrementBy, indexByNumber, indexByNumberWith, indexByString, indexByStringWith, indexBySymbol, indexBySymbolWith, indexByWith, isAllowedTimeDuration, isArray, isDefined, isEmpty, isError, isFalse, isFunction, isNegativeNumber, isNullOrUndefined, isNullOrUndefinedOrEmpty, isNumber, isPositiveNumber, isString, isTimeInstant, isTrue, isUpgradable, isZero, jsonCloneDeep, last, listToDict, mapDefined, mapEntries, mapFirstTruthy, mapTruthys, max, maxBy, min, minBy, multiplyBy, noop, not, omitFromJsonObject, or, pad, padLeft, padRight, parseJson, parseTimeInstantBasicComponents, parseTimeInstantComponents, partition, pick, pipedInvoke, pipedInvokeFromArray, pluralize, promiseSequence, randomDecimalInInterval, randomId, randomIntegerInInterval, randomNumberInInterval, randomPick, randomPicks, range, repeat, reverse, round, roundAwayFromZero, roundToLower, roundToNearest, roundToUpper, roundTowardsZero, shallowArrayEquals, shallowRecordEquals, sortedArray, splitWords, stringToNumber, stringifyJson, sum, sumBy, tail, throttle, throwIfNullOrUndefined, transformCssDictionary, tryToParseJson, tryToParseNumber, uniq, uniqBy, uniqByKey, unzip, upsert, withTryCatch, withTryCatchAsync, wrapWithString, xor, zip };
1606
1789
  export type { ICancelable, ICancelablePromise, IDataUpgrader, TAccumulator, TAllKeysOptional, TAnyFunction, TArrayable, TAsyncAnyFunction, TAsyncBiConsumer, TAsyncBiFunction, TAsyncBiPredicate, TAsyncConsumer, TAsyncFunction, TAsyncOperation, TAsyncOperationTuple, TAsyncPredicate, TAsyncProducer, TAsyncValidation, TAsyncVoidFunction, TBasicTimePattern, TBiConsumer, TBiFunction, TBiPredicate, TComparisonDirection, TComparisonFunction, TComparisonResult, TConditionalOptionalType, TConditionalParameter, TConditionalParameterOptions, TConsumer, TCssDeclarationRulesDictionary, TCssSelectorDeclarationRulesDictionary, TDayOfMonth, TDayOfWeek, TDigit, TDigit1_9, TEmpty, TEmptyArray, TEmptyObject, TEmptyOptional, TFourDigits, TFourDigitsMillisecond, TFourDigitsYear, TFunction, THasNever, THourOfDay, THtmlString, TIdentityFunction, TIntervalHandle, TIsEmptyObject, TIso8601DateString, TIso8601DateUtcString, TJsonArray, TJsonObject, TJsonPrimitive, TJsonSerializable, TKeysOfType, TLoggerOpts, TMaybe, TMillisecondOfSecond, TMinuteOfHour, TMonth, TMonthName, TNegativeNumber, TNumber0_10, TNumber0_100, TNumber0_1000, TNumber0_15, TNumber0_20, TNumber0_5, TNumber1_10, TNumericFloatingPointString, TNumericString, TOneDigit, TOperation, TOperationFailure, TOperationSuccess, TOperationTuple, TOptional, TOptionalKeysForType, TOptionsWithoutDefaults, TParseInt, TParseableInt, TPositiveNumber, TPredefinedTimeDuration, TPredicate, TPresentOptional, TPrettify, TPrimitive, TProducer, TPromisable, TReadableArray, TRelativeUrl, TReplaceType, TRequiredKeysForType, TSecondOfMinute, TSorter, TSorterBuilder, TStrictComparisonResult, TThreeDigits, TThreeDigitsMillisecond, TTimeInUnits, TTimeoutHandle, TTransformer, TTwoDigits, TTwoDigitsDate, TTwoDigitsHour, TTwoDigitsMinute, TTwoDigitsMonth, TTwoDigitsSecond, TTypePredicate, TUpToFourDigits, TUpToThreeDigits, TUpToTwoDigits, TUpgradable, TUrl, TValidTimeDuration, TValidation, TVoidFunction, TWeekNumber, TWithExtras, TWithRequiredProperties, TWithRequiredProperty, TYear, TZero };
package/.rollup/index.mjs CHANGED
@@ -1359,6 +1359,31 @@ const Operation = {
1359
1359
  partition: (results) => {
1360
1360
  return partition(results, r => r.success);
1361
1361
  },
1362
+ /**
1363
+ * Transforms the success value of an operation using the provided function.
1364
+ * If the operation is a failure, it is returned unchanged (same reference).
1365
+ * The transformation function is never called on a failure.
1366
+ *
1367
+ * @template T - The input success data type
1368
+ * @template U - The output success data type
1369
+ * @template E - The error type (preserved exactly)
1370
+ * @param op - The operation result to transform
1371
+ * @param fn - Pure function applied to the success value
1372
+ * @returns A new success with the transformed value, or the original failure
1373
+ *
1374
+ * @example
1375
+ * Operation.map(Operation.ok(1), x => x * 2);
1376
+ * // => { success: true, data: 2 }
1377
+ *
1378
+ * Operation.map(Operation.ko('err'), x => x * 2);
1379
+ * // => { success: false, error: 'err' }
1380
+ */
1381
+ map: (op, fn) => {
1382
+ if (op.success) {
1383
+ return { success: true, data: fn(op.data) };
1384
+ }
1385
+ return op;
1386
+ },
1362
1387
  combine: combine$1,
1363
1388
  };
1364
1389
 
@@ -3689,11 +3714,70 @@ function printTransitions(transitions) {
3689
3714
  }
3690
3715
 
3691
3716
  const VERSION_FIELD = "$version";
3717
+ /**
3718
+ * Versioned data migration system for transparently upgrading serialized data to the latest version.
3719
+ *
3720
+ * DataUpgrader manages the migration of data structures across multiple versions, automatically
3721
+ * finding the shortest upgrade path and applying transformations. This is particularly useful for
3722
+ * handling persisted data that may be in any historical format.
3723
+ *
3724
+ * **Key Use Case:** Reading serialized data of unknown version and migrating it transparently.
3725
+ *
3726
+ * @example
3727
+ * ```typescript
3728
+ * // Step 1: Define all historical versions with $version discriminator
3729
+ * type TSerializedData_V1 = { name: string; $version: 1 };
3730
+ * type TSerializedData_V2 = { name: string; age: number; $version: 2 };
3731
+ * type TSerializedData_V3 = { name: string; age: number; email: string; $version: 3 };
3732
+ *
3733
+ * // Step 2: Create union type and current version type
3734
+ * type TSerializedData_Vstar = TSerializedData_V1 | TSerializedData_V2 | TSerializedData_V3;
3735
+ * type TSerializedData = TSerializedData_V3;
3736
+ *
3737
+ * // Step 3: Create upgrader with transition functions
3738
+ * const upgrader = DataUpgrader.create<TSerializedData_Vstar, TSerializedData>(3)
3739
+ * .addTransition(1, 2, async (data: TSerializedData_V1) => ({
3740
+ * ...data,
3741
+ * age: 0,
3742
+ * $version: 2
3743
+ * }))
3744
+ * .addTransition(2, 3, async (data: TSerializedData_V2) => ({
3745
+ * ...data,
3746
+ * email: "",
3747
+ * $version: 3
3748
+ * }))
3749
+ * .addTransition(1, 3, async (data: TSerializedData_V1) => ({
3750
+ * // Optional: Direct path for efficiency
3751
+ * ...data,
3752
+ * age: 0,
3753
+ * email: "",
3754
+ * $version: 3
3755
+ * }));
3756
+ *
3757
+ * // Step 4: Use in data loading - handles any version transparently
3758
+ * async function loadData(json: string): Promise<TSerializedData> {
3759
+ * const data = parseJson<TSerializedData_Vstar>(json);
3760
+ * return await upgrader.upgrade(data);
3761
+ * }
3762
+ *
3763
+ * // Works with any version:
3764
+ * const v1 = await loadData('{"name":"Alice","$version":1}'); // → V3
3765
+ * const v2 = await loadData('{"name":"Bob","age":25,"$version":2}'); // → V3
3766
+ * const v3 = await loadData('{"name":"Carol","age":30,"email":"c@example.com","$version":3}'); // → V3
3767
+ * ```
3768
+ *
3769
+ * @typeParam X - Union type of all possible data versions (must extend TUpgradable)
3770
+ * @typeParam XLatest - The latest version type (must extend X)
3771
+ *
3772
+ * @see {@link isUpgradable | Type guard for checking if data is upgradable}
3773
+ * @see {@link DataUpgrader.Errors | Error types for upgrade failures}
3774
+ */
3692
3775
  class DataUpgrader {
3693
3776
  latestVersion;
3694
- transitions = {};
3695
- constructor(latestVersion) {
3777
+ transitions;
3778
+ constructor(latestVersion, transitions = {}) {
3696
3779
  this.latestVersion = latestVersion;
3780
+ this.transitions = transitions;
3697
3781
  }
3698
3782
  addTransition(from, to, apply) {
3699
3783
  if (from === undefined || from < 0)
@@ -3740,6 +3824,17 @@ class DataUpgrader {
3740
3824
  static create(latest) {
3741
3825
  return new DataUpgrader(latest);
3742
3826
  }
3827
+ /**
3828
+ * Creates a DataUpgrader from a DataUpgraderBuilder instance.
3829
+ * @internal
3830
+ */
3831
+ static fromBuilder(builder, latestVersion) {
3832
+ // Extract transitions from builder and convert to strongly-typed TTransitionMatrix
3833
+ // This type assertion is safe because the builder has validated all transitions
3834
+ // at compile-time via its type parameters
3835
+ const transitions = builder.getTransitions();
3836
+ return new DataUpgrader(latestVersion, transitions);
3837
+ }
3743
3838
  static Errors = {
3744
3839
  EmptyUpgrade: EmptyUpgradeError,
3745
3840
  UnavailableUpgrade: UnavailableUpgradeError
@@ -3748,6 +3843,141 @@ class DataUpgrader {
3748
3843
  function isUpgradable(obj) {
3749
3844
  return isDefined(obj) && typeof obj === "object" && VERSION_FIELD in obj && isNumber(obj.$version) && isPositiveNumber(obj.$version);
3750
3845
  }
3846
+ /**
3847
+ * The current system is unable to catch the following problems at compile-time:
3848
+ * type A_V1 = { a: number, $version: 1 }
3849
+ * type A_V2 = { a: number, b: number, $version: 2 }
3850
+ * type A_V3 = { a: number, b: number, c: string, $version: 3 }
3851
+ * type A_Vstar = A_V1 | A_V2 | A_V3;
3852
+ * type A = A_V3;
3853
+ *
3854
+ * const DU1 = DataUpgrader.create<A_Vstar, A>( 3 );
3855
+ *
3856
+ * const DU2 = DataUpgrader.create<A_Vstar, A>( 3 )
3857
+ * .addTransition( 1, 3, x => ( { ...x, b: 2, c: "foo", $version: 3 } ) )
3858
+ * .addTransition( 1, 3, x => ( { ...x, b: 3, c: "bar", $version: 3 } ) )
3859
+ * ;
3860
+ *
3861
+ * const DU3 = DataUpgrader.create<A_Vstar, A>( 3 )
3862
+ * .addTransition( 1, 2, x => ( { ...x, b: 2, $version: 2 } ) )
3863
+ * .addTransition( 2, 1, x => ( { ...x, $version: 1 } ) )
3864
+ * ;
3865
+ */
3866
+
3867
+ /**
3868
+ * Builder for creating type-safe DataUpgrader instances with compile-time safety.
3869
+ *
3870
+ * Prevents at compile-time:
3871
+ * - Missing upgrade paths (incomplete version coverage)
3872
+ * - Duplicate transition definitions
3873
+ * - Backward transitions (e.g., version 2 → 1)
3874
+ *
3875
+ * @example
3876
+ * ```typescript
3877
+ * // Step 1: Define all historical versions with $version discriminator
3878
+ * type V1 = { name: string; $version: 1 };
3879
+ * type V2 = { name: string; age: number; $version: 2 };
3880
+ * type V3 = { name: string; age: number; email: string; $version: 3 };
3881
+ *
3882
+ * // Step 2: Build the upgrader with type-safe transitions
3883
+ * const upgrader = DataUpgraderBuilder.start<V1>()
3884
+ * .addTransition<V1, V2>(1, 2, async (data) => ({
3885
+ * ...data,
3886
+ * age: 0,
3887
+ * $version: 2
3888
+ * }))
3889
+ * .addTransition<V2, V3>(2, 3, async (data) => ({
3890
+ * ...data,
3891
+ * email: "",
3892
+ * $version: 3
3893
+ * }))
3894
+ * .addShortcut<V1, V3>(1, 3, async (data) => ({
3895
+ * // Optional: Direct path for efficiency
3896
+ * ...data,
3897
+ * age: 0,
3898
+ * email: "",
3899
+ * $version: 3
3900
+ * }))
3901
+ * .build<V3>(3);
3902
+ *
3903
+ * // Step 3: Use the upgrader
3904
+ * async function loadData(json: string): Promise<V3> {
3905
+ * const data = JSON.parse(json);
3906
+ * return await upgrader.upgrade(data);
3907
+ * }
3908
+ *
3909
+ * // Works with any version:
3910
+ * const v1 = await loadData('{"name":"Alice","$version":1}'); // → V3
3911
+ * const v2 = await loadData('{"name":"Bob","age":25,"$version":2}'); // → V3
3912
+ * const v3 = await loadData('{"name":"Carol","age":30,"email":"c@example.com","$version":3}'); // → V3
3913
+ * ```
3914
+ */
3915
+ class DataUpgraderBuilder {
3916
+ transitions;
3917
+ constructor(transitions = {}) {
3918
+ this.transitions = transitions;
3919
+ }
3920
+ /** Starts building a new DataUpgrader from the lowest version. */
3921
+ static start() {
3922
+ return new DataUpgraderBuilder();
3923
+ }
3924
+ /**
3925
+ * Adds a sequential transition from the current version to the next version.
3926
+ * Prevents backward transitions and duplicates at compile-time.
3927
+ *
3928
+ * @example
3929
+ * ```typescript
3930
+ * builder.addTransition<V1, V2>(1, 2, async (d) => ({ ...d, extra: 0, $version: 2 }))
3931
+ * ```
3932
+ */
3933
+ addTransition(fromVersion, toVersion, apply) {
3934
+ const newTransitions = {
3935
+ ...this.transitions,
3936
+ [toVersion]: {
3937
+ ...(this.transitions[toVersion] ?? {}),
3938
+ [fromVersion]: { from: fromVersion, to: toVersion, apply: async (d) => apply(d) }
3939
+ }
3940
+ };
3941
+ const builder = new DataUpgraderBuilder(newTransitions);
3942
+ return builder;
3943
+ }
3944
+ /**
3945
+ * Adds a shortcut transition between arbitrary versions.
3946
+ * Both versions must already be in the accumulated union type.
3947
+ *
3948
+ * @example
3949
+ * ```typescript
3950
+ * builder.addShortcut<V1, V3>(1, 3, async (d) => ({ ...d, extra: 0, flag: false, $version: 3 }))
3951
+ * ```
3952
+ */
3953
+ addShortcut(fromVersion, toVersion, apply) {
3954
+ const newTransitions = {
3955
+ ...this.transitions,
3956
+ [toVersion]: {
3957
+ ...(this.transitions[toVersion] ?? {}),
3958
+ [fromVersion]: { from: fromVersion, to: toVersion, apply: async (d) => apply(d) }
3959
+ }
3960
+ };
3961
+ const builder = new DataUpgraderBuilder(newTransitions);
3962
+ return builder;
3963
+ }
3964
+ /**
3965
+ * Builds the DataUpgrader with the specified latest version.
3966
+ * The latest version must be in the accumulated union type.
3967
+ *
3968
+ * @example
3969
+ * ```typescript
3970
+ * const upgrader = builder.build<V3>(3);
3971
+ * ```
3972
+ */
3973
+ build(latestVersion) {
3974
+ return DataUpgrader.fromBuilder(this, latestVersion);
3975
+ }
3976
+ /** @internal */
3977
+ getTransitions() {
3978
+ return this.transitions;
3979
+ }
3980
+ }
3751
3981
 
3752
- export { Cached, DataUpgrader, Deferred, DeferredCanceledError, ErrorCannotInstantiatePresentOptionalWithEmptyValue, ErrorGetEmptyOptional, ErrorSetEmptyOptional, Lazy, LazyAsync, LazyDictionary, Logger, NEVER, NonExhaustiveSwitchError, Operation, OperationAggregateError, Optional, PredicateBuilder, RandomTimeDuration, RateThrottler, Semaphore, Sorter, StringParts, TimeDuration, TimeFrequency, TimeInstant, TimeRange, TimeUnit, TimeoutError, alwaysFalse, alwaysTrue, and, arrayGet, arrayIncludes, asError, asPromise, average, averageBy, awaitAtMost, capitalizeWord, clamp, clampInt0_100, constant, constantFalse, constantNull, constantOne, constantTrue, constantUndefined, constantZero, cssDeclarationRulesDictionaryToCss, decrement, decrementBy, delayPromise, dictToEntries, dictToList, divideBy, ellipsis, ensureArray, ensureDefined, ensureNegativeNumber, ensureNonNegativeNumber, ensureNonPositiveNumber, ensurePositiveNumber, ensureReadableArray, entriesToDict, entriesToEntries, entriesToList, extendArray, extendArrayWith, fill, fillWith, filterMap, filterMapReduce, filterWithTypePredicate, findInArray, findIndexInArray, first$1 as first, flatMapTruthys, getCauseMessageFromError, getCauseStackFromError, getMessageFromError, getStackFromError, groupByBoolean, groupByBooleanWith, groupByNumber, groupByNumberWith, groupByString, groupByStringWith, groupBySymbol, groupBySymbolWith, hashCode, havingMaxBy, havingMinBy, head, identity, ifDefined, ifNullOrUndefined, iff, includes, increment, incrementBy, indexByNumber, indexByNumberWith, indexByString, indexByStringWith, indexBySymbol, indexBySymbolWith, indexByWith, isAllowedTimeDuration, isArray, isDefined, isEmpty, isError, isFalse, isFunction, isNegativeNumber, isNullOrUndefined, isNullOrUndefinedOrEmpty, isNumber, isPositiveNumber, isString, isTimeInstant, isTrue, isUpgradable, isZero, jsonCloneDeep, last$1 as last, listToDict, mapDefined, mapEntries, mapFirstTruthy, mapTruthys, max, maxBy, min, minBy, multiplyBy, noop, not, omitFromJsonObject, or, pad, padLeft, padRight, parseJson, parseTimeInstantBasicComponents, parseTimeInstantComponents, partition, pick, pipedInvoke, pipedInvokeFromArray, pluralize, promiseSequence, randomDecimalInInterval, randomId, randomIntegerInInterval, randomNumberInInterval, randomPick, randomPicks, range, repeat, reverse$1 as reverse, round, roundAwayFromZero, roundToLower, roundToNearest, roundToUpper, roundTowardsZero, shallowArrayEquals, shallowRecordEquals, sortedArray, splitWords, stringToNumber, stringifyJson, sum, sumBy, tail, throttle, throwIfNullOrUndefined, transformCssDictionary, tryToParseJson, tryToParseNumber, uniq, uniqBy, uniqByKey, unzip, upsert, withTryCatch, withTryCatchAsync, wrapWithString, xor, zip };
3982
+ export { Cached, DataUpgrader, DataUpgraderBuilder, Deferred, DeferredCanceledError, ErrorCannotInstantiatePresentOptionalWithEmptyValue, ErrorGetEmptyOptional, ErrorSetEmptyOptional, Lazy, LazyAsync, LazyDictionary, Logger, NEVER, NonExhaustiveSwitchError, Operation, OperationAggregateError, Optional, PredicateBuilder, RandomTimeDuration, RateThrottler, Semaphore, Sorter, StringParts, TimeDuration, TimeFrequency, TimeInstant, TimeRange, TimeUnit, TimeoutError, alwaysFalse, alwaysTrue, and, arrayGet, arrayIncludes, asError, asPromise, average, averageBy, awaitAtMost, capitalizeWord, clamp, clampInt0_100, constant, constantFalse, constantNull, constantOne, constantTrue, constantUndefined, constantZero, cssDeclarationRulesDictionaryToCss, decrement, decrementBy, delayPromise, dictToEntries, dictToList, divideBy, ellipsis, ensureArray, ensureDefined, ensureNegativeNumber, ensureNonNegativeNumber, ensureNonPositiveNumber, ensurePositiveNumber, ensureReadableArray, entriesToDict, entriesToEntries, entriesToList, extendArray, extendArrayWith, fill, fillWith, filterMap, filterMapReduce, filterWithTypePredicate, findInArray, findIndexInArray, first$1 as first, flatMapTruthys, getCauseMessageFromError, getCauseStackFromError, getMessageFromError, getStackFromError, groupByBoolean, groupByBooleanWith, groupByNumber, groupByNumberWith, groupByString, groupByStringWith, groupBySymbol, groupBySymbolWith, hashCode, havingMaxBy, havingMinBy, head, identity, ifDefined, ifNullOrUndefined, iff, includes, increment, incrementBy, indexByNumber, indexByNumberWith, indexByString, indexByStringWith, indexBySymbol, indexBySymbolWith, indexByWith, isAllowedTimeDuration, isArray, isDefined, isEmpty, isError, isFalse, isFunction, isNegativeNumber, isNullOrUndefined, isNullOrUndefinedOrEmpty, isNumber, isPositiveNumber, isString, isTimeInstant, isTrue, isUpgradable, isZero, jsonCloneDeep, last$1 as last, listToDict, mapDefined, mapEntries, mapFirstTruthy, mapTruthys, max, maxBy, min, minBy, multiplyBy, noop, not, omitFromJsonObject, or, pad, padLeft, padRight, parseJson, parseTimeInstantBasicComponents, parseTimeInstantComponents, partition, pick, pipedInvoke, pipedInvokeFromArray, pluralize, promiseSequence, randomDecimalInInterval, randomId, randomIntegerInInterval, randomNumberInInterval, randomPick, randomPicks, range, repeat, reverse$1 as reverse, round, roundAwayFromZero, roundToLower, roundToNearest, roundToUpper, roundTowardsZero, shallowArrayEquals, shallowRecordEquals, sortedArray, splitWords, stringToNumber, stringifyJson, sum, sumBy, tail, throttle, throwIfNullOrUndefined, transformCssDictionary, tryToParseJson, tryToParseNumber, uniq, uniqBy, uniqByKey, unzip, upsert, withTryCatch, withTryCatchAsync, wrapWithString, xor, zip };
3753
3983
  //# sourceMappingURL=index.mjs.map