@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.
- package/LICENSE +21 -0
- package/README.md +140 -0
- package/dist/index.d.ts +3703 -0
- package/dist/index.js +1006 -0
- package/dist/index.js.map +1 -0
- package/package.json +103 -0
- package/src/ArrayX/ArrayX.ts +818 -0
- package/src/ArrayX/index.ts +1 -0
- package/src/BigIntX/BigIntX.ts +35 -0
- package/src/BigIntX/index.ts +1 -0
- package/src/BooleanX/BooleanX.ts +24 -0
- package/src/BooleanX/index.ts +1 -0
- package/src/DurationX/DurationX.ts +178 -0
- package/src/DurationX/index.ts +1 -0
- package/src/EffectX/EffectX.ts +183 -0
- package/src/EffectX/index.ts +1 -0
- package/src/FormDataX/FormDataX.ts +57 -0
- package/src/FormDataX/index.ts +1 -0
- package/src/MapX/MapX.ts +54 -0
- package/src/MapX/index.ts +1 -0
- package/src/NonNullableX/NonNullableX.ts +290 -0
- package/src/NonNullableX/index.ts +2 -0
- package/src/NumberX/NumberX.ts +282 -0
- package/src/NumberX/index.ts +1 -0
- package/src/OptionX/OptionX.ts +234 -0
- package/src/OptionX/index.ts +1 -0
- package/src/OrderX/OrderX.ts +35 -0
- package/src/OrderX/index.ts +1 -0
- package/src/PredicateX/PredicateX.ts +98 -0
- package/src/PredicateX/index.ts +1 -0
- package/src/PromiseX/PromiseX.ts +32 -0
- package/src/PromiseX/index.ts +1 -0
- package/src/RecordX/RecordX.ts +478 -0
- package/src/RecordX/index.ts +1 -0
- package/src/ResultX/ResultX.ts +53 -0
- package/src/ResultX/index.ts +1 -0
- package/src/SchemaX/SchemaX.ts +324 -0
- package/src/SchemaX/index.ts +1 -0
- package/src/SetX/SetX.ts +160 -0
- package/src/SetX/index.ts +1 -0
- package/src/StringX/StringX.ts +97 -0
- package/src/StringX/index.ts +1 -0
- package/src/StructX/StructX.ts +310 -0
- package/src/StructX/index.ts +1 -0
- package/src/These/These.ts +1173 -0
- package/src/These/index.ts +1 -0
- 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";
|
package/src/MapX/MapX.ts
ADDED
|
@@ -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";
|