@nunofyobiz/effect-extras 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/index.d.ts +3703 -0
  4. package/dist/index.js +1006 -0
  5. package/dist/index.js.map +1 -0
  6. package/package.json +103 -0
  7. package/src/ArrayX/ArrayX.ts +818 -0
  8. package/src/ArrayX/index.ts +1 -0
  9. package/src/BigIntX/BigIntX.ts +35 -0
  10. package/src/BigIntX/index.ts +1 -0
  11. package/src/BooleanX/BooleanX.ts +24 -0
  12. package/src/BooleanX/index.ts +1 -0
  13. package/src/DurationX/DurationX.ts +178 -0
  14. package/src/DurationX/index.ts +1 -0
  15. package/src/EffectX/EffectX.ts +183 -0
  16. package/src/EffectX/index.ts +1 -0
  17. package/src/FormDataX/FormDataX.ts +57 -0
  18. package/src/FormDataX/index.ts +1 -0
  19. package/src/MapX/MapX.ts +54 -0
  20. package/src/MapX/index.ts +1 -0
  21. package/src/NonNullableX/NonNullableX.ts +290 -0
  22. package/src/NonNullableX/index.ts +2 -0
  23. package/src/NumberX/NumberX.ts +282 -0
  24. package/src/NumberX/index.ts +1 -0
  25. package/src/OptionX/OptionX.ts +234 -0
  26. package/src/OptionX/index.ts +1 -0
  27. package/src/OrderX/OrderX.ts +35 -0
  28. package/src/OrderX/index.ts +1 -0
  29. package/src/PredicateX/PredicateX.ts +98 -0
  30. package/src/PredicateX/index.ts +1 -0
  31. package/src/PromiseX/PromiseX.ts +32 -0
  32. package/src/PromiseX/index.ts +1 -0
  33. package/src/RecordX/RecordX.ts +478 -0
  34. package/src/RecordX/index.ts +1 -0
  35. package/src/ResultX/ResultX.ts +53 -0
  36. package/src/ResultX/index.ts +1 -0
  37. package/src/SchemaX/SchemaX.ts +324 -0
  38. package/src/SchemaX/index.ts +1 -0
  39. package/src/SetX/SetX.ts +160 -0
  40. package/src/SetX/index.ts +1 -0
  41. package/src/StringX/StringX.ts +97 -0
  42. package/src/StringX/index.ts +1 -0
  43. package/src/StructX/StructX.ts +310 -0
  44. package/src/StructX/index.ts +1 -0
  45. package/src/These/These.ts +1173 -0
  46. package/src/These/index.ts +1 -0
  47. package/src/index.ts +20 -0
@@ -0,0 +1 @@
1
+ export * as ArrayX from "./ArrayX";
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `BigInt` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { BigInt, Option } from "effect";
7
+
8
+ /**
9
+ * Converts a `bigint` to a `number`, throwing when the value cannot be
10
+ * represented exactly.
11
+ *
12
+ * Delegates to Effect's `BigInt.toNumber`, which returns `None` once the
13
+ * `bigint` falls outside the safe integer range (`Number.MAX_SAFE_INTEGER`).
14
+ * This unwraps that `Option`, throwing instead of silently losing precision —
15
+ * use it only when the value is known to fit.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * import { BigIntX } from "@nunofyobiz/effect-extras"
20
+ *
21
+ * assert.deepStrictEqual(BigIntX.toNumberOrThrow(42n), 42)
22
+ *
23
+ * // throws when outside the safe integer range
24
+ * assert.throws(() => BigIntX.toNumberOrThrow(9007199254740993n))
25
+ * ```
26
+ *
27
+ * @category unsafe
28
+ * @since 0.0.0
29
+ */
30
+ export const toNumberOrThrow = (value: bigint): number =>
31
+ BigInt.toNumber(value).pipe(
32
+ Option.getOrThrowWith(
33
+ () => new Error(`Value ${value} is outside safe integer range`),
34
+ ),
35
+ );
@@ -0,0 +1 @@
1
+ export * as BigIntX from "./BigIntX";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Boolean` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ /**
7
+ * Converts a `boolean` to its binary digit: `1` for `true`, `0` for `false`.
8
+ *
9
+ * Useful when a numeric flag is required — summing booleans to count how many
10
+ * predicates hold, or feeding a bit into bitwise math or an external API that
11
+ * expects `0`/`1` rather than `false`/`true`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { BooleanX } from "@nunofyobiz/effect-extras"
16
+ *
17
+ * assert.deepStrictEqual(BooleanX.toBinary(true), 1)
18
+ * assert.deepStrictEqual(BooleanX.toBinary(false), 0)
19
+ * ```
20
+ *
21
+ * @category conversions
22
+ * @since 0.0.0
23
+ */
24
+ export const toBinary = (value: boolean): 0 | 1 => (value ? 1 : 0);
@@ -0,0 +1 @@
1
+ export * as BooleanX from "./BooleanX";
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Duration` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { DateTime, Duration, Match, Option } from "effect";
7
+ import { dual, pipe } from "effect/Function";
8
+ import { BigIntX } from "../BigIntX";
9
+
10
+ // Some private constants for conversion
11
+ const MICROS_PER_MILLI = 1000;
12
+
13
+ /**
14
+ * Computes the elapsed `Duration` from `that` to `self`, clamped at zero.
15
+ *
16
+ * Represents the time that has passed since the reference instant `that`. When
17
+ * `that` lies in the future relative to `self`, the elapsed time is `zero`
18
+ * rather than a negative duration.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { DateTime, Duration, pipe } from "effect"
23
+ * import { DurationX } from "@nunofyobiz/effect-extras"
24
+ *
25
+ * const earlier = DateTime.makeUnsafe(1000)
26
+ * const later = DateTime.makeUnsafe(4000)
27
+ *
28
+ * // data-first
29
+ * assert.deepStrictEqual(DurationX.diff(later, earlier), Duration.seconds(3))
30
+ *
31
+ * // future reference clamps to zero
32
+ * assert.deepStrictEqual(DurationX.diff(earlier, later), Duration.zero)
33
+ *
34
+ * // data-last (piped)
35
+ * assert.deepStrictEqual(
36
+ * pipe(later, DurationX.diff(earlier)),
37
+ * Duration.seconds(3),
38
+ * )
39
+ * ```
40
+ *
41
+ * @category combinators
42
+ * @since 0.0.0
43
+ */
44
+ export const diff = dual<
45
+ // Data-last typing
46
+ (that: DateTime.DateTime) => (self: DateTime.DateTime) => Duration.Duration,
47
+ // Data-first typing
48
+ (self: DateTime.DateTime, that: DateTime.DateTime) => Duration.Duration
49
+ >(
50
+ 2,
51
+ (self: DateTime.DateTime, that: DateTime.DateTime): Duration.Duration =>
52
+ Duration.millis(
53
+ // Clamp at 0 — "diff" represents elapsed time since `that`; if `that`
54
+ // is in the future relative to `self`, the elapsed time is zero
55
+ // (not negative).
56
+ Math.max(0, DateTime.toEpochMillis(self) - DateTime.toEpochMillis(that)),
57
+ ),
58
+ );
59
+
60
+ // Internal — used by mapAsUnit.
61
+ const toUnit = dual<
62
+ // Data-last typing
63
+ (unit: Duration.Unit) => (duration: Duration.Duration) => number,
64
+ // Data-first typing
65
+ (duration: Duration.Duration, unit: Duration.Unit) => number
66
+ >(2, (duration: Duration.Duration, unit: Duration.Unit): number =>
67
+ Match.value(unit).pipe(
68
+ Match.whenOr("week", "weeks", () => Duration.toWeeks(duration)),
69
+ Match.whenOr("day", "days", () => Duration.toDays(duration)),
70
+ Match.whenOr("hour", "hours", () => Duration.toHours(duration)),
71
+ Match.whenOr("minute", "minutes", () => Duration.toMinutes(duration)),
72
+ Match.whenOr("second", "seconds", () => Duration.toSeconds(duration)),
73
+ Match.whenOr("milli", "millis", () => Duration.toMillis(duration)),
74
+ Match.whenOr(
75
+ "micro",
76
+ "micros",
77
+ () => Duration.toMillis(duration) * MICROS_PER_MILLI,
78
+ ),
79
+ Match.whenOr("nano", "nanos", () =>
80
+ pipe(
81
+ Duration.toNanos(duration),
82
+ Option.getOrThrowWith(
83
+ () => new Error("Duration.toNanos returned None"),
84
+ ),
85
+ BigIntX.toNumberOrThrow,
86
+ ),
87
+ ),
88
+ Match.exhaustive,
89
+ ),
90
+ );
91
+
92
+ // Internal — used by mapAsUnit.
93
+ const fromUnit = dual<
94
+ // Data-last typing
95
+ (unit: Duration.Unit) => (value: number) => Duration.Duration,
96
+ // Data-first typing
97
+ (value: number, unit: Duration.Unit) => Duration.Duration
98
+ >(
99
+ 2,
100
+ (value: number, unit: Duration.Unit): Duration.Duration =>
101
+ Match.value(unit).pipe(
102
+ Match.whenOr("week", "weeks", () => Duration.weeks(value)),
103
+ Match.whenOr("day", "days", () => Duration.days(value)),
104
+ Match.whenOr("hour", "hours", () => Duration.hours(value)),
105
+ Match.whenOr("minute", "minutes", () => Duration.minutes(value)),
106
+ Match.whenOr("second", "seconds", () => Duration.seconds(value)),
107
+ Match.whenOr("milli", "millis", () => Duration.millis(value)),
108
+ Match.whenOr("micro", "micros", () => Duration.micros(BigInt(value))),
109
+ Match.whenOr("nano", "nanos", () => Duration.nanos(BigInt(value))),
110
+ Match.exhaustive,
111
+ ),
112
+ );
113
+
114
+ /**
115
+ * Transforms a `Duration` by converting it to a numeric `unit`, applying `map`,
116
+ * then converting back.
117
+ *
118
+ * Lets you operate on a duration in whatever unit is convenient — round it to
119
+ * whole minutes, halve its seconds, floor its days — without juggling
120
+ * conversions by hand. The `map` callback receives the duration expressed as a
121
+ * `number` of `unit`s and returns the new count.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * import { Duration, Number, pipe } from "effect"
126
+ * import { DurationX } from "@nunofyobiz/effect-extras"
127
+ *
128
+ * // data-first: halve a 4-second duration
129
+ * assert.deepStrictEqual(
130
+ * DurationX.mapAsUnit(Duration.seconds(4), "second", Number.divideUnsafe(2)),
131
+ * Duration.seconds(2),
132
+ * )
133
+ *
134
+ * // data-last (piped)
135
+ * assert.deepStrictEqual(
136
+ * pipe(
137
+ * Duration.minutes(10),
138
+ * DurationX.mapAsUnit("minute", (minutes) => minutes + 5),
139
+ * ),
140
+ * Duration.minutes(15),
141
+ * )
142
+ * ```
143
+ *
144
+ * @category combinators
145
+ * @since 0.0.0
146
+ */
147
+ export const mapAsUnit = dual<
148
+ // Data-last typing
149
+ (
150
+ unit: Duration.Unit,
151
+ map: (numberOfUnits: number) => number,
152
+ ) => (duration: Duration.Duration) => Duration.Duration,
153
+ // Data-first typing
154
+ (
155
+ duration: Duration.Duration,
156
+ unit: Duration.Unit,
157
+ map: (numberOfUnits: number) => number,
158
+ ) => Duration.Duration
159
+ >(
160
+ 3,
161
+ (
162
+ duration: Duration.Duration,
163
+ unit: Duration.Unit,
164
+ map: (numberOfUnits: number) => number,
165
+ ): Duration.Duration =>
166
+ pipe(
167
+ duration,
168
+
169
+ // Convert to that unit
170
+ toUnit(unit),
171
+
172
+ // Truncate to the requested number of digits
173
+ map,
174
+
175
+ // Convert back to a duration
176
+ fromUnit(unit),
177
+ ),
178
+ );
@@ -0,0 +1 @@
1
+ export * as DurationX from "./DurationX";
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions to Effect's `Effect` module.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Cause, Duration, Effect, Option, Predicate, pipe } from "effect";
7
+ import { dual } from "effect/Function";
8
+
9
+ /**
10
+ * The duration of time a user registers something as "instant" — used as the
11
+ * default poll interval for {@link tryUntil}. 200ms is the generally agreed
12
+ * value. See https://psychology.stackexchange.com/a/1680
13
+ */
14
+ const USER_INSTANT_DURATION = Duration.millis(200);
15
+
16
+ /**
17
+ * Flattens an `Effect` that succeeds with an `Option` into an `Effect` that
18
+ * fails with `onNone()` when the `Option` is `None`.
19
+ *
20
+ * When the wrapped `Option` is `Some(value)` the effect succeeds with `value`;
21
+ * when it is `None` the effect fails with the error produced by the `onNone`
22
+ * thunk. An existing failure of the source effect is preserved untouched, so the
23
+ * result's error channel is the union of the original error and the `None`
24
+ * error.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * import { Effect, Option, Result } from "effect"
29
+ * import { EffectX } from "@nunofyobiz/effect-extras"
30
+ *
31
+ * const some = EffectX.flattenOption(
32
+ * Effect.succeed(Option.some(1)),
33
+ * () => "missing",
34
+ * )
35
+ * assert.deepStrictEqual(Effect.runSync(Effect.result(some)), Result.succeed(1))
36
+ *
37
+ * const none = EffectX.flattenOption(
38
+ * Effect.succeed(Option.none<number>()),
39
+ * () => "missing",
40
+ * )
41
+ * assert.deepStrictEqual(
42
+ * Effect.runSync(Effect.result(none)),
43
+ * Result.fail("missing"),
44
+ * )
45
+ * ```
46
+ *
47
+ * @category sequencing
48
+ * @since 0.0.0
49
+ */
50
+ export const flattenOption = dual<
51
+ <A, E1, E2, R>(
52
+ onNone: () => E2,
53
+ ) => (
54
+ effect: Effect.Effect<Option.Option<A>, E1, R>,
55
+ ) => Effect.Effect<A, E1 | E2, R>,
56
+ <A, E1, E2, R>(
57
+ effect: Effect.Effect<Option.Option<A>, E1, R>,
58
+ onNone: () => E2,
59
+ ) => Effect.Effect<A, E1 | E2, R>
60
+ >(
61
+ 2,
62
+ <A, E1, E2, R>(
63
+ effect: Effect.Effect<Option.Option<A>, E1, R>,
64
+ onNone: () => E2,
65
+ ): Effect.Effect<A, E1 | E2, R> =>
66
+ Effect.flatMap(effect, (option) =>
67
+ pipe(Effect.fromOption(option), Effect.mapError(onNone)),
68
+ ),
69
+ );
70
+
71
+ /**
72
+ * Converts an `Option` to an `Effect`, mapping the `None` case to a caller-chosen
73
+ * error via the `onNone` thunk.
74
+ *
75
+ * Equivalent to `Effect.mapError(Effect.fromOption(option), onNone)`: it bridges
76
+ * the `NoSuchElementError` that `Effect.fromOption` produces to the caller's own
77
+ * error type, so callers never have to handle `NoSuchElementError`. This fills
78
+ * the v4 gap where `Effect.mapError` no longer accepts an `Option` directly —
79
+ * instead of
80
+ * `pipe(option, Effect.fromOption, Effect.mapError(() => new MyError()))`, write
81
+ * `pipe(option, EffectX.fromOptionOrElse(() => new MyError()))`. The `onNone`
82
+ * thunk runs only when the `Option` is `None`.
83
+ *
84
+ * @example
85
+ * ```ts
86
+ * import { Effect, Option, Result, pipe } from "effect"
87
+ * import { EffectX } from "@nunofyobiz/effect-extras"
88
+ *
89
+ * // data-first
90
+ * const some = EffectX.fromOptionOrElse(Option.some(42), () => "missing")
91
+ * assert.deepStrictEqual(Effect.runSync(Effect.result(some)), Result.succeed(42))
92
+ *
93
+ * // data-last (piped) — None maps to the chosen error
94
+ * const none = pipe(
95
+ * Option.none<number>(),
96
+ * EffectX.fromOptionOrElse(() => "missing"),
97
+ * )
98
+ * assert.deepStrictEqual(
99
+ * Effect.runSync(Effect.result(none)),
100
+ * Result.fail("missing"),
101
+ * )
102
+ * ```
103
+ *
104
+ * @category conversions
105
+ * @since 0.0.0
106
+ */
107
+ export const fromOptionOrElse: {
108
+ <E>(onNone: () => E): <A>(option: Option.Option<A>) => Effect.Effect<A, E>;
109
+ <A, E>(option: Option.Option<A>, onNone: () => E): Effect.Effect<A, E>;
110
+ } = dual(
111
+ 2,
112
+ <A, E>(option: Option.Option<A>, onNone: () => E): Effect.Effect<A, E> =>
113
+ pipe(Effect.fromOption(option), Effect.mapError(onNone)),
114
+ );
115
+
116
+ /**
117
+ * Repeatedly calls a synchronous `try` thunk until its result satisfies the
118
+ * `until` refinement, sleeping `sleepDuration` between attempts and failing with
119
+ * a `TimeoutError` once `maxDuration` elapses.
120
+ *
121
+ * The thunk is evaluated immediately; if its first result already passes the
122
+ * refinement the effect succeeds without any delay. Otherwise it polls on the
123
+ * `sleepDuration` interval (defaulting to 200ms — the threshold below which a
124
+ * delay reads as "instant" to a user) until either the predicate holds (the
125
+ * effect succeeds with the narrowed `B` value) or `maxDuration` is exceeded (the
126
+ * effect fails with a `Cause.TimeoutError`). Use it to await an external,
127
+ * non-effectful condition such as a flag flipped by a callback.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * import { Duration, Effect } from "effect"
132
+ * import { EffectX } from "@nunofyobiz/effect-extras"
133
+ *
134
+ * // First attempt already matches, so it resolves immediately.
135
+ * const effect = EffectX.tryUntil({
136
+ * try: () => 1,
137
+ * until: (value: number): value is number => value === 1,
138
+ * sleepDuration: Duration.millis(100),
139
+ * maxDuration: Duration.seconds(1),
140
+ * })
141
+ *
142
+ * assert.deepStrictEqual(Effect.runSync(effect), 1)
143
+ * ```
144
+ *
145
+ * @category sequencing
146
+ * @since 0.0.0
147
+ */
148
+ export const tryUntil = <A, B extends A>({
149
+ try: doTry,
150
+ until: isDone,
151
+ sleepDuration = USER_INSTANT_DURATION,
152
+ maxDuration,
153
+ }: {
154
+ try: () => A;
155
+ until: Predicate.Refinement<A, B>;
156
+ sleepDuration?: Duration.Duration;
157
+ maxDuration: Duration.Duration;
158
+ }): Effect.Effect<B, Cause.TimeoutError, never> => {
159
+ const immediateValue = doTry();
160
+ if (isDone(immediateValue)) {
161
+ return Effect.succeed(immediateValue);
162
+ }
163
+
164
+ // Continually call it again on a schedule with a delay
165
+ return Effect.sync(doTry).pipe(
166
+ // Sleep in between each attempt
167
+ Effect.delay(sleepDuration),
168
+
169
+ // Keep doing this until the predicate passes
170
+ Effect.repeat({ until: isDone }),
171
+
172
+ // Until a timeout occurs. In v4, `Effect.timeout` raises `TimeoutError`
173
+ // on its own — no separate `timeoutFail` overload is needed.
174
+ Effect.timeout(maxDuration),
175
+ Effect.catchTag("TimeoutError", () =>
176
+ Effect.fail(
177
+ new Cause.TimeoutError(
178
+ `Timed out after ${Duration.format(maxDuration)} waiting for value to pass predicate`,
179
+ ),
180
+ ),
181
+ ),
182
+ );
183
+ };
@@ -0,0 +1 @@
1
+ export * as EffectX from "./EffectX";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Helpers for decoding `FormData` with Effect `Schema`.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Function, Schema } from "effect";
7
+
8
+ /**
9
+ * Decodes a `FormData` into a typed value using a `Schema`, throwing on
10
+ * validation failure.
11
+ *
12
+ * Built on v4's native `Schema.fromFormData`, which first parses the `FormData`
13
+ * entries into a nested tree record (bracket-path notation is supported) and then
14
+ * decodes that tree with the provided inner schema. For schemas with non-string
15
+ * fields (for example `Schema.Int`), wrap the inner schema in
16
+ * `Schema.toCodecStringTree(schema, { keepDeclarations: true })` so the
17
+ * string → number/boolean coercion happens. Usable only with schemas that have
18
+ * no decoding services — this is enforced at the type level via
19
+ * `S["DecodingServices"] = never` — and it throws synchronously when the input
20
+ * fails validation.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { Schema } from "effect"
25
+ * import { FormDataX } from "@nunofyobiz/effect-extras"
26
+ *
27
+ * const formData = new FormData()
28
+ * formData.append("name", "John")
29
+ * formData.append("age", "30")
30
+ *
31
+ * const result = FormDataX.decodeSync(
32
+ * formData,
33
+ * Schema.Struct({ name: Schema.String, age: Schema.NumberFromString }),
34
+ * )
35
+ *
36
+ * assert.deepStrictEqual(result, { name: "John", age: 30 })
37
+ * ```
38
+ *
39
+ * @category conversions
40
+ * @since 0.0.0
41
+ */
42
+ export const decodeSync: {
43
+ <S extends Schema.Top & { readonly DecodingServices: never }>(
44
+ formData: FormData,
45
+ schema: S,
46
+ ): S["Type"];
47
+ <S extends Schema.Top & { readonly DecodingServices: never }>(
48
+ schema: S,
49
+ ): (formData: FormData) => S["Type"];
50
+ } = Function.dual(
51
+ 2,
52
+ <S extends Schema.Top & { readonly DecodingServices: never }>(
53
+ formData: FormData,
54
+ schema: S,
55
+ ): S["Type"] =>
56
+ Schema.decodeUnknownSync(Schema.fromFormData(schema))(formData),
57
+ );
@@ -0,0 +1 @@
1
+ export * as FormDataX from "./FormDataX";
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Generic, framework-agnostic extensions for working with `Map`.
3
+ *
4
+ * @since 0.0.0
5
+ */
6
+ import { Predicate } from "effect";
7
+ import { dual } from "effect/Function";
8
+
9
+ /**
10
+ * Like `Map.prototype.get`, but when the key is absent it stores the computed
11
+ * fallback at that key and returns it. Mutates the input map in place.
12
+ *
13
+ * Use it for memoization-style caches where a miss should both populate the map
14
+ * and yield the value in one step. Throws if the resolved value is nullish (only
15
+ * possible when `fallbackIfNotFound` returns `null`/`undefined` for a value type
16
+ * that admits them — treated as a programmer error). Supports both data-first and
17
+ * data-last (pipeable) call styles.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { MapX } from "@nunofyobiz/effect-extras"
22
+ * import { pipe } from "effect"
23
+ *
24
+ * // Miss: stores the fallback and returns it (data-first)
25
+ * const map = new Map<string, number>()
26
+ * assert.deepStrictEqual(MapX.getOrElseSetGet(map, "a", () => 1), 1)
27
+ * assert.deepStrictEqual(map.get("a"), 1)
28
+ *
29
+ * // Hit: returns the existing value, fallback is ignored (data-last)
30
+ * assert.deepStrictEqual(
31
+ * pipe(map, MapX.getOrElseSetGet("a", () => 99)),
32
+ * 1
33
+ * )
34
+ * ```
35
+ *
36
+ * @category combinators
37
+ * @since 0.0.0
38
+ */
39
+ export const getOrElseSetGet = dual<
40
+ <K, V>(key: K, fallbackIfNotFound: () => V) => (map: Map<K, V>) => V,
41
+ <K, V>(map: Map<K, V>, key: K, fallbackIfNotFound: () => V) => V
42
+ >(3, <K, V>(map: Map<K, V>, key: K, fallbackIfNotFound: () => V): V => {
43
+ if (!map.has(key)) {
44
+ map.set(key, fallbackIfNotFound());
45
+ return fallbackIfNotFound();
46
+ }
47
+
48
+ const existingValue = map.get(key);
49
+ if (Predicate.isNullish(existingValue)) {
50
+ throw new Error(`Value is nullable: ${String(key)}`);
51
+ }
52
+
53
+ return existingValue;
54
+ });
@@ -0,0 +1 @@
1
+ export * as MapX from "./MapX";