@typed/fx 2.0.0-beta.0 → 2.0.0-beta.2

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 (139) hide show
  1. package/README.md +24 -1
  2. package/dist/Fx/combinators/additive.d.ts +94 -0
  3. package/dist/Fx/combinators/additive.d.ts.map +1 -0
  4. package/dist/Fx/combinators/additive.js +92 -0
  5. package/dist/Fx/combinators/catch.d.ts +61 -0
  6. package/dist/Fx/combinators/catch.d.ts.map +1 -1
  7. package/dist/Fx/combinators/catch.js +54 -0
  8. package/dist/Fx/combinators/changesWithEffect.d.ts +20 -0
  9. package/dist/Fx/combinators/changesWithEffect.d.ts.map +1 -0
  10. package/dist/Fx/combinators/changesWithEffect.js +28 -0
  11. package/dist/Fx/combinators/dropUntil.d.ts +29 -0
  12. package/dist/Fx/combinators/dropUntil.d.ts.map +1 -0
  13. package/dist/Fx/combinators/dropUntil.js +23 -0
  14. package/dist/Fx/combinators/flatMapConcurrently.d.ts.map +1 -1
  15. package/dist/Fx/combinators/flatMapConcurrently.js +3 -2
  16. package/dist/Fx/combinators/index.d.ts +10 -0
  17. package/dist/Fx/combinators/index.d.ts.map +1 -1
  18. package/dist/Fx/combinators/index.js +10 -0
  19. package/dist/Fx/combinators/keyed.d.ts +1 -1
  20. package/dist/Fx/combinators/keyed.d.ts.map +1 -1
  21. package/dist/Fx/combinators/mapBoth.d.ts +21 -0
  22. package/dist/Fx/combinators/mapBoth.d.ts.map +1 -0
  23. package/dist/Fx/combinators/mapBoth.js +14 -0
  24. package/dist/Fx/combinators/mapError.d.ts +17 -0
  25. package/dist/Fx/combinators/mapError.d.ts.map +1 -0
  26. package/dist/Fx/combinators/mapError.js +16 -0
  27. package/dist/Fx/combinators/provide.d.ts +34 -1
  28. package/dist/Fx/combinators/provide.d.ts.map +1 -1
  29. package/dist/Fx/combinators/provide.js +27 -0
  30. package/dist/Fx/combinators/result.d.ts +23 -0
  31. package/dist/Fx/combinators/result.d.ts.map +1 -0
  32. package/dist/Fx/combinators/result.js +32 -0
  33. package/dist/Fx/combinators/scan.d.ts +33 -0
  34. package/dist/Fx/combinators/scan.d.ts.map +1 -0
  35. package/dist/Fx/combinators/scan.js +38 -0
  36. package/dist/Fx/combinators/skip.d.ts +13 -0
  37. package/dist/Fx/combinators/skip.d.ts.map +1 -1
  38. package/dist/Fx/combinators/skip.js +11 -0
  39. package/dist/Fx/combinators/skipWhile.d.ts +49 -0
  40. package/dist/Fx/combinators/skipWhile.d.ts.map +1 -0
  41. package/dist/Fx/combinators/skipWhile.js +66 -0
  42. package/dist/Fx/combinators/slice.d.ts +13 -0
  43. package/dist/Fx/combinators/slice.d.ts.map +1 -1
  44. package/dist/Fx/combinators/slice.js +11 -0
  45. package/dist/Fx/combinators/take.d.ts +13 -0
  46. package/dist/Fx/combinators/take.d.ts.map +1 -1
  47. package/dist/Fx/combinators/take.js +11 -0
  48. package/dist/Fx/combinators/takeUntil.d.ts +14 -0
  49. package/dist/Fx/combinators/takeUntil.d.ts.map +1 -1
  50. package/dist/Fx/combinators/takeUntil.js +14 -0
  51. package/dist/Fx/combinators/takeWhile.d.ts +29 -0
  52. package/dist/Fx/combinators/takeWhile.d.ts.map +1 -0
  53. package/dist/Fx/combinators/takeWhile.js +23 -0
  54. package/dist/Fx/combinators/zip.d.ts +75 -0
  55. package/dist/Fx/combinators/zip.d.ts.map +1 -0
  56. package/dist/Fx/combinators/zip.js +100 -0
  57. package/dist/Fx/constructors/at.d.ts +2 -2
  58. package/dist/Fx/constructors/at.d.ts.map +1 -1
  59. package/dist/Fx/constructors/periodic.d.ts +1 -1
  60. package/dist/Fx/constructors/periodic.d.ts.map +1 -1
  61. package/dist/Push/Push.d.ts +64 -1
  62. package/dist/Push/Push.d.ts.map +1 -1
  63. package/dist/Push/Push.js +57 -0
  64. package/dist/RefSubject/RefArray.d.ts.map +1 -1
  65. package/dist/RefSubject/RefArray.js +2 -1
  66. package/dist/RefSubject/RefChunk.d.ts.map +1 -1
  67. package/dist/RefSubject/RefChunk.js +2 -1
  68. package/dist/RefSubject/RefDateTime.d.ts +4 -4
  69. package/dist/RefSubject/RefDateTime.d.ts.map +1 -1
  70. package/dist/RefSubject/RefHashMap.d.ts.map +1 -1
  71. package/dist/RefSubject/RefHashMap.js +5 -1
  72. package/dist/RefSubject/RefIterable.d.ts +1 -1
  73. package/dist/RefSubject/RefIterable.d.ts.map +1 -1
  74. package/dist/RefSubject/RefIterable.js +6 -1
  75. package/dist/RefSubject/RefRecord.d.ts.map +1 -1
  76. package/dist/RefSubject/RefRecord.js +3 -2
  77. package/dist/RefSubject/RefSubject.d.ts +48 -1
  78. package/dist/RefSubject/RefSubject.d.ts.map +1 -1
  79. package/dist/RefSubject/RefSubject.js +80 -1
  80. package/dist/RefSubject/RefTrie.d.ts +7 -7
  81. package/dist/RefSubject/RefTrie.d.ts.map +1 -1
  82. package/dist/RefSubject/RefTrie.js +8 -3
  83. package/dist/Sink/combinators.d.ts +57 -0
  84. package/dist/Sink/combinators.d.ts.map +1 -1
  85. package/dist/Sink/combinators.js +104 -1
  86. package/dist/Versioned/Versioned.d.ts +30 -0
  87. package/dist/Versioned/Versioned.d.ts.map +1 -1
  88. package/dist/Versioned/Versioned.js +18 -0
  89. package/package.json +10 -6
  90. package/src/Fx/combinators/additive.ts +142 -0
  91. package/src/Fx/combinators/catch.ts +256 -0
  92. package/src/Fx/combinators/changesWithEffect.ts +66 -0
  93. package/src/Fx/combinators/dropUntil.ts +47 -0
  94. package/src/Fx/combinators/flatMapConcurrently.ts +5 -2
  95. package/src/Fx/combinators/index.ts +10 -0
  96. package/src/Fx/combinators/keyed.ts +2 -2
  97. package/src/Fx/combinators/mapBoth.ts +40 -0
  98. package/src/Fx/combinators/mapError.ts +28 -0
  99. package/src/Fx/combinators/provide.ts +63 -1
  100. package/src/Fx/combinators/result.ts +39 -0
  101. package/src/Fx/combinators/scan.ts +82 -0
  102. package/src/Fx/combinators/skip.ts +21 -0
  103. package/src/Fx/combinators/skipWhile.ts +100 -0
  104. package/src/Fx/combinators/slice.ts +23 -0
  105. package/src/Fx/combinators/take.ts +21 -0
  106. package/src/Fx/combinators/takeUntil.ts +38 -0
  107. package/src/Fx/combinators/takeWhile.ts +47 -0
  108. package/src/Fx/combinators/zip.ts +175 -0
  109. package/src/Fx/constructors/at.ts +3 -3
  110. package/src/Fx/constructors/periodic.ts +1 -1
  111. package/src/Fx.additive-combinators.test.ts +126 -0
  112. package/src/Fx.catch-additive.test.ts +206 -0
  113. package/src/Fx.catch.test.ts +1 -2
  114. package/src/Fx.dropUntil.test.ts +61 -0
  115. package/src/Fx.lifecycle.test.ts +1 -2
  116. package/src/Fx.mapError-mapBoth.test.ts +101 -0
  117. package/src/Fx.provide-combinators.test.ts +94 -0
  118. package/src/Fx.result-changesWithEffect.test.ts +112 -0
  119. package/src/Fx.scan.test.ts +73 -0
  120. package/src/Fx.takeWhile-skipWhile.test.ts +84 -0
  121. package/src/Fx.zip-merge-additive.test.ts +171 -0
  122. package/src/Fx.zip.test.ts +133 -0
  123. package/src/Push/Push.ts +170 -1
  124. package/src/Push.additive.test.ts +256 -0
  125. package/src/RefSubject/RefArray.ts +4 -1
  126. package/src/RefSubject/RefChunk.ts +2 -1
  127. package/src/RefSubject/RefDateTime.ts +6 -6
  128. package/src/RefSubject/RefHashMap.ts +10 -1
  129. package/src/RefSubject/RefIterable.ts +11 -2
  130. package/src/RefSubject/RefRecord.ts +9 -2
  131. package/src/RefSubject/RefSubject.ts +108 -9
  132. package/src/RefSubject/RefTrie.ts +19 -10
  133. package/src/RefSubject.additive-parity.test.ts +101 -0
  134. package/src/Sink/combinators.ts +123 -1
  135. package/src/Sink.combinators.test.ts +88 -0
  136. package/src/Sink.reduce-collect-head-last.test.ts +107 -0
  137. package/src/Versioned/Versioned.ts +76 -0
  138. package/src/Versioned.filterMap.test.ts +91 -0
  139. package/tsconfig.json +0 -6
@@ -4,6 +4,7 @@
4
4
  import * as Array from "effect/Array";
5
5
  import type * as Cause from "effect/Cause";
6
6
  import * as Effect from "effect/Effect";
7
+ import * as Semaphore from "effect/Semaphore";
7
8
  import { equals } from "effect/Equal";
8
9
  import type { Equivalence } from "effect/Equivalence";
9
10
  import * as Exit from "effect/Exit";
@@ -459,14 +460,14 @@ class RefSubjectCore<A, E, R, R2> {
459
460
  readonly services: ServiceMap.ServiceMap<R2>;
460
461
  readonly scope: Scope.Closeable;
461
462
  readonly deferredRef: DeferredRef.DeferredRef<E, A>;
462
- readonly semaphore: Effect.Semaphore;
463
+ readonly semaphore: Semaphore.Semaphore;
463
464
  constructor(
464
465
  initial: Effect.Effect<A, E, R>,
465
466
  subject: Subject.HoldSubjectImpl<A, E>,
466
467
  services: ServiceMap.ServiceMap<R2>,
467
468
  scope: Scope.Closeable,
468
469
  deferredRef: DeferredRef.DeferredRef<E, A>,
469
- semaphore: Effect.Semaphore,
470
+ semaphore: Semaphore.Semaphore,
470
471
  ) {
471
472
  this.initial = initial;
472
473
  this.subject = subject;
@@ -693,6 +694,66 @@ export function fromStream<A, E, R>(
693
694
  });
694
695
  }
695
696
 
697
+ /**
698
+ * Creates a `RefSubject` from an `Option` value.
699
+ *
700
+ * @example
701
+ * ```ts
702
+ * import { Effect, Option } from "effect"
703
+ * import * as RefSubject from "@typed/fx/RefSubject"
704
+ *
705
+ * const program = Effect.gen(function* () {
706
+ * const ref = yield* RefSubject.fromOption(Option.some(42))
707
+ * const value = yield* ref
708
+ * console.log(Option.isSome(value)) // true
709
+ * })
710
+ * ```
711
+ *
712
+ * @since 1.0.0
713
+ * @category constructors
714
+ */
715
+ export function fromOption<A>(
716
+ option: Option.Option<A>,
717
+ options?: RefSubjectOptions<Option.Option<A>>,
718
+ ): Effect.Effect<RefSubject<Option.Option<A>>, never, Scope.Scope> {
719
+ return make(option, options);
720
+ }
721
+
722
+ /**
723
+ * Creates a `RefSubject` from a nullable value (null/undefined become `Option.none()`).
724
+ *
725
+ * @example
726
+ * ```ts
727
+ * import { Effect, Option } from "effect"
728
+ * import * as RefSubject from "@typed/fx/RefSubject"
729
+ *
730
+ * const program = Effect.gen(function* () {
731
+ * const ref = yield* RefSubject.fromNullable("hello")
732
+ * const value = yield* ref
733
+ * console.log(Option.isSome(value)) // true
734
+ *
735
+ * const empty = yield* RefSubject.fromNullable(null)
736
+ * const none = yield* empty
737
+ * console.log(Option.isNone(none)) // true
738
+ * })
739
+ * ```
740
+ *
741
+ * @since 1.0.0
742
+ * @category constructors
743
+ */
744
+ function optionFromNullable<A>(value: A | null | undefined): Option.Option<NonNullable<A>> {
745
+ return value === null || value === undefined
746
+ ? Option.none()
747
+ : Option.some(value as NonNullable<A>);
748
+ }
749
+
750
+ export function fromNullable<A>(
751
+ value: A | null | undefined,
752
+ options?: RefSubjectOptions<Option.Option<NonNullable<A>>>,
753
+ ): Effect.Effect<RefSubject<Option.Option<NonNullable<A>>>, never, Scope.Scope> {
754
+ return make(optionFromNullable(value), options);
755
+ }
756
+
696
757
  function redirectCause<A, E, R>(core: RefSubjectCore<A, E, R, R | Scope.Scope>) {
697
758
  return Stream.catchCause((cause: Cause.Cause<E>) =>
698
759
  Stream.unwrap(Effect.as(onFailureCore(core, cause), Stream.empty)),
@@ -716,7 +777,7 @@ function makeCore<A, E, R>(
716
777
  scope,
717
778
  deferredRef ??
718
779
  DeferredRef.unsafeMake(id, getExitEquivalence(options?.eq ?? equals), subject.lastValue),
719
- Effect.makeSemaphoreUnsafe(1),
780
+ Semaphore.makeUnsafe(1),
720
781
  );
721
782
  yield* Scope.addFinalizer(scope, core.subject.interrupt);
722
783
  return core;
@@ -1179,12 +1240,10 @@ export const runUpdates: {
1179
1240
  <A, E, R, B, E2, R2, R3 = never, E3 = never, C = never>(
1180
1241
  ref: RefSubject<A, E, R>,
1181
1242
  f: (ref: GetSetDelete<A, E, R>) => Effect.Effect<B, E2, R2>,
1182
- options?:
1183
- | {
1184
- readonly onInterrupt: (value: A) => Effect.Effect<C, E3, R3>;
1185
- readonly value?: "initial" | "current";
1186
- }
1187
- | undefined,
1243
+ options?: {
1244
+ readonly onInterrupt: (value: A) => Effect.Effect<C, E3, R3>;
1245
+ readonly value?: "initial" | "current";
1246
+ },
1188
1247
  ): Effect.Effect<B, E | E2 | E3, R | R2 | R3>;
1189
1248
  } = dual(
1190
1249
  isRefSubjectDataFirst,
@@ -1741,6 +1800,46 @@ export const compact: {
1741
1800
  return new FilteredImpl(versioned, Effect.succeed);
1742
1801
  };
1743
1802
 
1803
+ /**
1804
+ * Returns a `Computed` that yields the value inside the `Option`, or the fallback when `None`.
1805
+ * Works with `Computed<Option<A>>` (e.g. from `fromOption` / `fromNullable`) and with `Filtered<A>`.
1806
+ *
1807
+ * @example
1808
+ * ```ts
1809
+ * import { Effect, Option } from "effect"
1810
+ * import * as RefSubject from "@typed/fx/RefSubject"
1811
+ *
1812
+ * const program = Effect.gen(function* () {
1813
+ * const ref = yield* RefSubject.fromOption(Option.some(42))
1814
+ * const withDefault = RefSubject.getOrElse(ref, () => 0)
1815
+ * expect(yield* withDefault).toBe(42)
1816
+ *
1817
+ * const empty = yield* RefSubject.fromNullable(null)
1818
+ * const fallback = RefSubject.getOrElse(empty, () => 99)
1819
+ * expect(yield* fallback).toBe(99)
1820
+ * })
1821
+ * ```
1822
+ *
1823
+ * @since 1.0.0
1824
+ * @category combinators
1825
+ */
1826
+ export const getOrElse: {
1827
+ <A>(
1828
+ fallback: () => A,
1829
+ ): <E, R>(ref: Computed<Option.Option<A>, E, R> | Filtered<A, E, R>) => Computed<A, E, R>;
1830
+ <A, E, R>(
1831
+ ref: Computed<Option.Option<A>, E, R> | Filtered<A, E, R>,
1832
+ fallback: () => A,
1833
+ ): Computed<A, E, R>;
1834
+ } = dual(2, function getOrElse<
1835
+ A,
1836
+ E,
1837
+ R,
1838
+ >(ref: Computed<Option.Option<A>, E, R> | Filtered<A, E, R>, fallback: () => A): Computed<A, E, R> {
1839
+ const computed = FilteredTypeId in ref ? (ref as Filtered<A, E, R>).asComputed() : ref;
1840
+ return map(computed, (opt) => Option.getOrElse(opt, fallback));
1841
+ });
1842
+
1744
1843
  class RefSubjectSimpleTransform<A, E, R, R2, R3>
1745
1844
  extends YieldableFx<A, E, R | R2 | Scope.Scope, A, E, R | R3>
1746
1845
  implements RefSubject<A, E, R | R2 | R3>
@@ -6,11 +6,12 @@
6
6
  import type * as Effect from "effect/Effect";
7
7
  import { equals } from "effect/Equal";
8
8
  import { dual } from "effect/Function";
9
- import type * as Option from "effect/Option";
9
+ import * as Option from "effect/Option";
10
10
  import type * as Scope from "effect/Scope";
11
11
  import * as Trie from "effect/Trie";
12
12
  import type * as Fx from "../Fx/index.js";
13
13
  import * as RefSubject from "./RefSubject.js";
14
+ import { Result } from "effect";
14
15
 
15
16
  /**
16
17
  * A RefTrie is a RefSubject specialized over a Trie.
@@ -122,13 +123,13 @@ export const clear = <V, E, R>(ref: RefTrie<V, E, R>): Effect.Effect<Trie.Trie<V
122
123
  export const map: {
123
124
  <V>(
124
125
  f: (value: V, key: string) => V,
125
- ): <E, R>(ref: RefTrie<V, E, R>) => Effect.Effect<Trie.Trie<V>, E, R>;
126
+ ): <E, R>(ref: RefTrie<V, E, R>) => RefSubject.Computed<Trie.Trie<V>, E, R>;
126
127
  <V, E, R>(
127
128
  ref: RefTrie<V, E, R>,
128
129
  f: (value: V, key: string) => V,
129
- ): Effect.Effect<Trie.Trie<V>, E, R>;
130
+ ): RefSubject.Computed<Trie.Trie<V>, E, R>;
130
131
  } = dual(2, function map<V, E, R>(ref: RefTrie<V, E, R>, f: (value: V, key: string) => V) {
131
- return RefSubject.update(ref, Trie.map(f));
132
+ return RefSubject.map(ref, Trie.map(f));
132
133
  });
133
134
 
134
135
  /**
@@ -139,17 +140,17 @@ export const map: {
139
140
  export const filter: {
140
141
  <V>(
141
142
  predicate: (value: V, key: string) => boolean,
142
- ): <E, R>(ref: RefTrie<V, E, R>) => Effect.Effect<Trie.Trie<V>, E, R>;
143
+ ): <E, R>(ref: RefTrie<V, E, R>) => RefSubject.Computed<Trie.Trie<V>, E, R>;
143
144
  <V, E, R>(
144
145
  ref: RefTrie<V, E, R>,
145
146
  predicate: (value: V, key: string) => boolean,
146
- ): Effect.Effect<Trie.Trie<V>, E, R>;
147
+ ): RefSubject.Computed<Trie.Trie<V>, E, R>;
147
148
  } = dual(2, function filter<
148
149
  V,
149
150
  E,
150
151
  R,
151
152
  >(ref: RefTrie<V, E, R>, predicate: (value: V, key: string) => boolean) {
152
- return RefSubject.update(ref, Trie.filter(predicate));
153
+ return RefSubject.map(ref, Trie.filter(predicate));
153
154
  });
154
155
 
155
156
  /**
@@ -160,17 +161,25 @@ export const filter: {
160
161
  export const filterMap: {
161
162
  <V>(
162
163
  f: (value: V, key: string) => Option.Option<V>,
163
- ): <E, R>(ref: RefTrie<V, E, R>) => Effect.Effect<Trie.Trie<V>, E, R>;
164
+ ): <E, R>(ref: RefTrie<V, E, R>) => RefSubject.Computed<Trie.Trie<V>, E, R>;
164
165
  <V, E, R>(
165
166
  ref: RefTrie<V, E, R>,
166
167
  f: (value: V, key: string) => Option.Option<V>,
167
- ): Effect.Effect<Trie.Trie<V>, E, R>;
168
+ ): RefSubject.Computed<Trie.Trie<V>, E, R>;
168
169
  } = dual(2, function filterMap<
169
170
  V,
170
171
  E,
171
172
  R,
172
173
  >(ref: RefTrie<V, E, R>, f: (value: V, key: string) => Option.Option<V>) {
173
- return RefSubject.update(ref, Trie.filterMap(f));
174
+ return RefSubject.map(
175
+ ref,
176
+ Trie.filterMap((value, key) =>
177
+ Option.match(f(value, key), {
178
+ onNone: () => Result.failVoid,
179
+ onSome: (b) => Result.succeed(b),
180
+ }),
181
+ ),
182
+ );
174
183
  });
175
184
 
176
185
  // ========================================
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Effect } from "effect";
3
+ import * as Option from "effect/Option";
4
+ import { RefSubject } from "./index.js";
5
+
6
+ const fromNullableOption = <A>(v: A | null | undefined): Option.Option<NonNullable<A>> =>
7
+ v === null || v === undefined ? Option.none() : Option.some(v as NonNullable<A>);
8
+
9
+ describe("RefSubject additive parity (fromOption, fromNullable, getOrElse)", () => {
10
+ describe("fromOption", () => {
11
+ it("creates RefSubject from Option.some", () =>
12
+ Effect.gen(function* () {
13
+ const ref = yield* RefSubject.fromOption(Option.some(42));
14
+ const value = yield* ref;
15
+ expect(Option.isSome(value)).toBe(true);
16
+ if (Option.isSome(value)) {
17
+ expect(value.value).toBe(42);
18
+ }
19
+ }).pipe(Effect.scoped, Effect.runPromise));
20
+
21
+ it("creates RefSubject from Option.none", () =>
22
+ Effect.gen(function* () {
23
+ const ref = yield* RefSubject.fromOption(Option.none<number>());
24
+ const value = yield* ref;
25
+ expect(Option.isNone(value)).toBe(true);
26
+ }).pipe(Effect.scoped, Effect.runPromise));
27
+
28
+ it("is writable (set/update)", () =>
29
+ Effect.gen(function* () {
30
+ const ref = yield* RefSubject.fromOption(Option.some(1));
31
+ yield* RefSubject.set(ref, Option.some(2));
32
+ const value = yield* ref;
33
+ expect(Option.isSome(value)).toBe(true);
34
+ if (Option.isSome(value)) {
35
+ expect(value.value).toBe(2);
36
+ }
37
+ }).pipe(Effect.scoped, Effect.runPromise));
38
+ });
39
+
40
+ describe("fromNullable", () => {
41
+ it("creates RefSubject from non-null value", () =>
42
+ Effect.gen(function* () {
43
+ const ref = yield* RefSubject.fromNullable("hello");
44
+ const value = yield* ref;
45
+ expect(Option.isSome(value)).toBe(true);
46
+ if (Option.isSome(value)) {
47
+ expect(value.value).toBe("hello");
48
+ }
49
+ }).pipe(Effect.scoped, Effect.runPromise));
50
+
51
+ it("creates RefSubject from null", () =>
52
+ Effect.gen(function* () {
53
+ const ref = yield* RefSubject.fromNullable<string>(null);
54
+ const value = yield* ref;
55
+ expect(Option.isNone(value)).toBe(true);
56
+ }).pipe(Effect.scoped, Effect.runPromise));
57
+
58
+ it("creates RefSubject from undefined", () =>
59
+ Effect.gen(function* () {
60
+ const ref = yield* RefSubject.fromNullable<number>(undefined);
61
+ const value = yield* ref;
62
+ expect(Option.isNone(value)).toBe(true);
63
+ }).pipe(Effect.scoped, Effect.runPromise));
64
+ });
65
+
66
+ describe("getOrElse", () => {
67
+ it("returns value for Computed<Option<A>> when Some", () =>
68
+ Effect.gen(function* () {
69
+ const ref = yield* RefSubject.fromOption(Option.some(42));
70
+ const withDefault = RefSubject.getOrElse(ref, () => 0);
71
+ expect(yield* withDefault).toBe(42);
72
+ }).pipe(Effect.scoped, Effect.runPromise));
73
+
74
+ it("returns fallback for Computed<Option<A>> when None", () =>
75
+ Effect.gen(function* () {
76
+ const ref = yield* RefSubject.fromNullable<number>(null);
77
+ const withDefault = RefSubject.getOrElse(ref, () => 99);
78
+ expect(yield* withDefault).toBe(99);
79
+ }).pipe(Effect.scoped, Effect.runPromise));
80
+
81
+ it("getOrElse on Filtered uses fallback when no value", () =>
82
+ Effect.gen(function* () {
83
+ const numbers = yield* RefSubject.make([1, 3, 5]);
84
+ const firstEven = RefSubject.filterMap(numbers, (arr) =>
85
+ fromNullableOption(arr.find((n) => n % 2 === 0)),
86
+ );
87
+ const withDefault = RefSubject.getOrElse(firstEven, () => -1);
88
+ expect(yield* withDefault).toBe(-1);
89
+ }).pipe(Effect.scoped, Effect.runPromise));
90
+
91
+ it("getOrElse on Filtered returns value when present", () =>
92
+ Effect.gen(function* () {
93
+ const numbers = yield* RefSubject.make([1, 2, 3]);
94
+ const firstEven = RefSubject.filterMap(numbers, (arr) =>
95
+ fromNullableOption(arr.find((n) => n % 2 === 0)),
96
+ );
97
+ const withDefault = RefSubject.getOrElse(firstEven, () => -1);
98
+ expect(yield* withDefault).toBe(2);
99
+ }).pipe(Effect.scoped, Effect.runPromise));
100
+ });
101
+ });
@@ -1,5 +1,6 @@
1
1
  import * as Cause from "effect/Cause";
2
2
  import * as Effect from "effect/Effect";
3
+ import * as Semaphore from "effect/Semaphore";
3
4
  import * as Exit from "effect/Exit";
4
5
  import { dual, flow, identity } from "effect/Function";
5
6
  import * as MutableRef from "effect/MutableRef";
@@ -72,6 +73,40 @@ export function map<A, E, R, B>(sink: Sink<A, E, R>, f: (b: B) => A): Sink<B, E,
72
73
  return MapSink.make(sink, f);
73
74
  }
74
75
 
76
+ /** Alias for `map`: transforms input values before they reach the sink. @since 1.0.0 @category combinators */
77
+ export const mapInput = map;
78
+
79
+ /**
80
+ * Maps the error channel of a sink using the provided function.
81
+ * Failures are mapped via `Cause.map`; defects and interrupts are preserved.
82
+ *
83
+ * @since 1.0.0
84
+ * @category combinators
85
+ */
86
+ export function mapError<A, E, E2, R>(sink: Sink<A, E2, R>, f: (e: E) => E2): Sink<A, E, R> {
87
+ return new MapErrorSink(sink, f);
88
+ }
89
+
90
+ class MapErrorSink<A, E, E2, R> implements Sink<A, E, R> {
91
+ readonly sink: Sink<A, E2, R>;
92
+ readonly f: (e: E) => E2;
93
+
94
+ constructor(sink: Sink<A, E2, R>, f: (e: E) => E2) {
95
+ this.sink = sink;
96
+ this.f = f;
97
+ this.onSuccess = this.onSuccess.bind(this);
98
+ this.onFailure = this.onFailure.bind(this);
99
+ }
100
+
101
+ onSuccess(value: A): Effect.Effect<unknown, never, R> {
102
+ return this.sink.onSuccess(value);
103
+ }
104
+
105
+ onFailure(cause: Cause.Cause<E>): Effect.Effect<unknown, never, R> {
106
+ return this.sink.onFailure(Cause.map(cause, this.f));
107
+ }
108
+ }
109
+
75
110
  class FilterMapSink<A, E, R, B> implements Sink<B, E, R> {
76
111
  readonly sink: Sink<A, E, R>;
77
112
  readonly f: (b: B) => Option.Option<A>;
@@ -236,7 +271,7 @@ export function withStateSemaphore<A, E, R, B, R2>(
236
271
  ) {
237
272
  return withEarlyExit(sink, (sink, params) => {
238
273
  const stateRef = MutableRef.make(state);
239
- const semaphore = Effect.makeSemaphoreUnsafe(1);
274
+ const semaphore = Semaphore.makeUnsafe(1);
240
275
  const lock = semaphore.withPermits(1);
241
276
  const modifyEffect = <C, E2, R2>(f: (state: B) => Effect.Effect<readonly [C, B], E2, R2>) =>
242
277
  Effect.suspend(() => f(MutableRef.get(stateRef))).pipe(
@@ -776,6 +811,9 @@ class MapEffectSink<A, E, R, B, E2, R2> implements Sink<B, E | E2, R | R2> {
776
811
  }
777
812
  }
778
813
 
814
+ /** Alias for `mapEffect`: transforms input with an effect before the sink. @since 1.0.0 @category combinators */
815
+ export const mapInputEffect = mapEffect;
816
+
779
817
  export const filterMapEffect: {
780
818
  <B, A, E2, R2>(
781
819
  f: (b: B) => Effect.Effect<Option.Option<A>, E2, R2>,
@@ -991,3 +1029,87 @@ export const skipInterrupt = <A, E, R>(sink: Sink<A, E, R>): Sink<A, E, R> => {
991
1029
  cause.reasons.every(Cause.isInterruptReason) ? Effect.void : sink.onFailure(cause),
992
1030
  };
993
1031
  };
1032
+
1033
+ // -----------------------------------------------------------------------------
1034
+ // Reducing / collecting combinators (additive)
1035
+ // -----------------------------------------------------------------------------
1036
+
1037
+ /**
1038
+ * Reduces values into a single result using a pure function. Pass a `Ref<B>`
1039
+ * (e.g. from `Ref.make(initial)`); after running, read the result with `Ref.get(ref)`.
1040
+ *
1041
+ * @since 1.0.0
1042
+ * @category combinators
1043
+ */
1044
+ export function reduce<A, B, E>(ref: Ref.Ref<B>, f: (b: B, a: A) => B): Sink<A, E, never> {
1045
+ return {
1046
+ onSuccess: (value) => Ref.update(ref, (b) => f(b, value)),
1047
+ onFailure: () => Effect.void,
1048
+ };
1049
+ }
1050
+
1051
+ /**
1052
+ * Reduces values into a single result using an effectful function. Pass a `Ref<B>`;
1053
+ * after running, read the result with `Ref.get(ref)`. If the reducer effect fails,
1054
+ * the ref is left unchanged (Sink onSuccess is typed as never failing).
1055
+ *
1056
+ * @since 1.0.0
1057
+ * @category combinators
1058
+ */
1059
+ export function reduceEffect<A, B, E, E2, R2>(
1060
+ ref: Ref.Ref<B>,
1061
+ f: (b: B, a: A) => Effect.Effect<B, E2, R2>,
1062
+ ): Sink<A, E | E2, R2> {
1063
+ return {
1064
+ onSuccess: (value) =>
1065
+ Effect.flatMap(Ref.get(ref), (b) =>
1066
+ Effect.matchCauseEffect(f(b, value), {
1067
+ onFailure: () => Effect.void,
1068
+ onSuccess: (next) => Ref.set(ref, next),
1069
+ }),
1070
+ ),
1071
+ onFailure: () => Effect.void,
1072
+ };
1073
+ }
1074
+
1075
+ /**
1076
+ * Collects all values into an array. Pass a `Ref<ReadonlyArray<A>>` (e.g. `Ref.make([])`);
1077
+ * after running, read the result with `Ref.get(ref)`.
1078
+ *
1079
+ * @since 1.0.0
1080
+ * @category combinators
1081
+ */
1082
+ export function collect<A, E>(ref: Ref.Ref<ReadonlyArray<A>>): Sink<A, E, never> {
1083
+ return {
1084
+ onSuccess: (value) => Ref.update(ref, (arr) => [...arr, value]),
1085
+ onFailure: () => Effect.void,
1086
+ };
1087
+ }
1088
+
1089
+ /**
1090
+ * Keeps only the first value. Pass a `Ref<Option.Option<A>>` (e.g. `Ref.make(Option.none())`);
1091
+ * after running, read the result with `Ref.get(ref)`.
1092
+ *
1093
+ * @since 1.0.0
1094
+ * @category combinators
1095
+ */
1096
+ export function head<A, E>(ref: Ref.Ref<Option.Option<A>>): Sink<A, E, never> {
1097
+ return {
1098
+ onSuccess: (value) => Ref.update(ref, (opt) => (Option.isNone(opt) ? Option.some(value) : opt)),
1099
+ onFailure: () => Effect.void,
1100
+ };
1101
+ }
1102
+
1103
+ /**
1104
+ * Keeps only the last value. Pass a `Ref<Option.Option<A>>` (e.g. `Ref.make(Option.none())`);
1105
+ * after running, read the result with `Ref.get(ref)`.
1106
+ *
1107
+ * @since 1.0.0
1108
+ * @category combinators
1109
+ */
1110
+ export function last<A, E>(ref: Ref.Ref<Option.Option<A>>): Sink<A, E, never> {
1111
+ return {
1112
+ onSuccess: (value) => Ref.set(ref, Option.some(value)),
1113
+ onFailure: () => Effect.void,
1114
+ };
1115
+ }
@@ -0,0 +1,88 @@
1
+ import { assert, describe, it } from "vitest";
2
+ import * as Cause from "effect/Cause";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Ref from "effect/Ref";
5
+ import { Fx, Sink } from "./index.js";
6
+
7
+ describe("Sink.mapError", () => {
8
+ it("maps failure cause before passing to inner sink", () =>
9
+ Effect.gen(function* () {
10
+ const failures = yield* Ref.make<Array<number>>([]);
11
+ const sink = Sink.make<unknown, number, never>(
12
+ (cause) =>
13
+ Ref.update(failures, (list) => {
14
+ const f = Cause.findFail(cause);
15
+ if (f._tag === "Success") list.push(f.success.error as number);
16
+ return list;
17
+ }),
18
+ () => Effect.void,
19
+ );
20
+ const mapped = Sink.mapError(sink, (s: string) => s.length);
21
+ yield* Fx.fail("err").run(mapped);
22
+ const list = yield* Ref.get(failures);
23
+ assert.deepStrictEqual(list, [3]);
24
+ }).pipe(Effect.scoped, Effect.runPromise));
25
+
26
+ it("preserves success path", () =>
27
+ Effect.gen(function* () {
28
+ const out = yield* Ref.make<number[]>([]);
29
+ const sink = Sink.make<number, string, never>(
30
+ (_cause) => Effect.void,
31
+ (n: number) => Ref.update(out, (arr) => [...arr, n]),
32
+ );
33
+ const mapped = Sink.mapError(sink, (e: string) => e);
34
+ yield* Fx.fromIterable([1, 2, 3]).run(mapped);
35
+ const list = yield* Ref.get(out);
36
+ assert.deepStrictEqual(list, [1, 2, 3]);
37
+ }).pipe(Effect.scoped, Effect.runPromise));
38
+ });
39
+
40
+ describe("Sink.mapInput", () => {
41
+ it("transforms input before sink (same as map)", () =>
42
+ Effect.gen(function* () {
43
+ const out = yield* Ref.make<number[]>([]);
44
+ const sink = Sink.make(
45
+ () => Effect.void,
46
+ (n: number) => Ref.update(out, (arr) => [...arr, n]),
47
+ );
48
+ const mapped = Sink.mapInput(sink, (s: string) => parseInt(s, 10));
49
+ yield* Fx.fromIterable(["1", "2", "3"]).run(mapped);
50
+ const list = yield* Ref.get(out);
51
+ assert.deepStrictEqual(list, [1, 2, 3]);
52
+ }).pipe(Effect.scoped, Effect.runPromise));
53
+ });
54
+
55
+ describe("Sink.mapInputEffect", () => {
56
+ it("transforms input with effect before sink (same as mapEffect)", () =>
57
+ Effect.gen(function* () {
58
+ const out = yield* Ref.make<number[]>([]);
59
+ const sink = Sink.make(
60
+ () => Effect.void,
61
+ (n: number) => Ref.update(out, (arr) => [...arr, n]),
62
+ );
63
+ const mapped = Sink.mapInputEffect(sink, (s: string) => Effect.succeed(parseInt(s, 10)));
64
+ yield* Fx.fromIterable(["1", "2", "3"]).run(mapped);
65
+ const list = yield* Ref.get(out);
66
+ assert.deepStrictEqual(list, [1, 2, 3]);
67
+ }).pipe(Effect.scoped, Effect.runPromise));
68
+
69
+ it("propagates effect failure to sink onFailure", () =>
70
+ Effect.gen(function* () {
71
+ const failures = yield* Ref.make<Array<string>>([]);
72
+ const sink = Sink.make<number, string, never>(
73
+ (cause) =>
74
+ Ref.update(failures, (list) => {
75
+ const f = Cause.findFail(cause);
76
+ if (f._tag === "Success") list.push(f.success.error as string);
77
+ return list;
78
+ }),
79
+ (_n: number) => Effect.void,
80
+ );
81
+ const mapped = Sink.mapInputEffect(sink, (s: string) =>
82
+ s === "bad" ? Effect.fail("parse failed") : Effect.succeed(parseInt(s, 10)),
83
+ );
84
+ yield* Fx.fromIterable(["1", "bad", "3"]).run(mapped);
85
+ const list = yield* Ref.get(failures);
86
+ assert.deepStrictEqual(list, ["parse failed"]);
87
+ }).pipe(Effect.scoped, Effect.runPromise));
88
+ });