git.iko.soy/rowtype-yoga/purescript-yoga-om-strom
on main help sign in
Extract yoga-om-strom from monorepo

๐Ÿค– Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>

commitdd20ac6d22bfbac8cb2d9176c90a308a8f06c6a9 authorMark <mark.eibes@gmail.com> dateThu 5 Feb 2026, 10:41 (8 months ago) parentb48d916
4 files changed+605โˆ’66
LICENSE+16 benchmark-results/node_20260205_102520.txt+191 spago.yaml+2โˆ’5 src/Yoga/Om/Strom.purs+396โˆ’61
LICENSEnew file+16
@@ -0,0 +1,16 @@
1+MIT No Attribution
2+
3+Copyright 2021 Mark Eibes, Jan Schulte
4+
5+Permission is hereby granted, free of charge, to any person obtaining a copy of this
6+software and associated documentation files (the "Software"), to deal in the Software
7+without restriction, including without limitation the rights to use, copy, modify,
8+merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
9+permit persons to whom the Software is furnished to do so.
10+
11+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
12+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
13+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
14+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
15+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
16+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
benchmark-results/node_20260205_102520.txtnew file+191
@@ -0,0 +1,191 @@
1+๐Ÿš€ Running benchmarks on Node.js v25.2.1
2+============================================================
3+๐Ÿ”ฅ Strom Performance Benchmark Suite
4+=====================================
5+
6+
7+๐Ÿƒ map-2M (runs: 10)
8+ min: 0.0 ms
9+ median: 0.0 ms
10+ mean: 0.0 ms
11+ max: 0.0 ms
12+
13+๐Ÿƒ filter-2M-50pct (runs: 10)
14+ min: 0.0 ms
15+ median: 0.0 ms
16+ mean: 0.0 ms
17+ max: 0.0 ms
18+
19+๐Ÿƒ map-chain-3x-2M (runs: 10)
20+ min: 0.0 ms
21+ median: 0.0 ms
22+ mean: 0.0 ms
23+ max: 0.0 ms
24+
25+๐Ÿƒ pipeline-map-filter-map-2M (runs: 10)
26+ min: 0.0 ms
27+ median: 0.0 ms
28+ mean: 0.0 ms
29+ max: 0.0 ms
30+
31+๐Ÿƒ fold-sum-1M (runs: 10)
32+ min: 0.0 ms
33+ median: 0.0 ms
34+ mean: 0.0 ms
35+ max: 0.0 ms
36+
37+๐Ÿƒ collect-50k (runs: 10)
38+ min: 0.0 ms
39+ median: 0.0 ms
40+ mean: 0.0 ms
41+ max: 0.0 ms
42+
43+๐Ÿƒ take-5k-from-2M (runs: 10)
44+ min: 0.0 ms
45+ median: 0.0 ms
46+ mean: 0.0 ms
47+ max: 0.0 ms
48+
49+๐Ÿƒ mergeND-2x1M-pure (runs: 10)
50+ min: 96.0 ms
51+ median: 128.0 ms
52+ mean: 137.1 ms
53+ max: 228.0 ms
54+
55+๐Ÿƒ mergeND-2x100-with-delay (runs: 10)
56+ min: 118.0 ms
57+ median: 176.0 ms
58+ mean: 166.5 ms
59+ max: 194.0 ms
60+
61+๐Ÿƒ mapPar-concurrency4-5k-pure (runs: 10)
62+ min: 279.0 ms
63+ median: 314.0 ms
64+ mean: 346.8 ms
65+ max: 500.0 ms
66+
67+๐Ÿƒ mapPar-concurrency4-100-with-delay (runs: 10)
68+ min: 287.0 ms
69+ median: 299.0 ms
70+ mean: 303.9 ms
71+ max: 334.0 ms
72+
73+๐Ÿƒ scan-running-sum-2M (runs: 10)
74+ min: 0.0 ms
75+ median: 0.0 ms
76+ mean: 0.0 ms
77+ max: 0.0 ms
78+
79+๐Ÿƒ unfold-10k (runs: 10)
80+ min: 0.0 ms
81+ median: 0.0 ms
82+ mean: 0.0 ms
83+ max: 0.0 ms
84+
85+๐Ÿƒ infinite-iterate-take-10k (runs: 10)
86+ min: 1.0 ms
87+ median: 2.0 ms
88+ mean: 2.1 ms
89+ max: 3.0 ms
90+
91+๐Ÿƒ bind-5kx10 (runs: 10)
92+ min: 0.0 ms
93+ median: 0.0 ms
94+ mean: 0.0 ms
95+ max: 0.0 ms
96+
97+๐Ÿƒ zip-2x500k (runs: 10)
98+ min: 0.0 ms
99+ median: 0.0 ms
100+ mean: 0.0 ms
101+ max: 0.0 ms
102+
103+๐Ÿƒ fromArray-1M (runs: 10)
104+ min: 0.0 ms
105+ median: 0.0 ms
106+ mean: 0.0 ms
107+ max: 0.0 ms
108+
109+๐Ÿƒ iterateStrom-10k (runs: 10)
110+ min: 0.0 ms
111+ median: 0.0 ms
112+ mean: 0.0 ms
113+ max: 0.0 ms
114+
115+๐Ÿƒ repeatStrom-10k (runs: 10)
116+ min: 0.0 ms
117+ median: 0.0 ms
118+ mean: 0.0 ms
119+ max: 0.0 ms
120+
121+๐Ÿƒ mapM-effect-50k (runs: 10)
122+ min: 0.0 ms
123+ median: 0.0 ms
124+ mean: 0.0 ms
125+ max: 0.0 ms
126+
127+๐Ÿƒ tap-50k (runs: 10)
128+ min: 0.0 ms
129+ median: 0.0 ms
130+ mean: 0.0 ms
131+ max: 0.0 ms
132+
133+๐Ÿƒ collect-filter-map-50k (runs: 10)
134+ min: 0.0 ms
135+ median: 0.0 ms
136+ mean: 0.0 ms
137+ max: 0.0 ms
138+
139+๐Ÿƒ changes-50k (runs: 10)
140+ min: 0.0 ms
141+ median: 0.0 ms
142+ mean: 0.0 ms
143+ max: 0.0 ms
144+
145+๐Ÿƒ takeWhile-50k (runs: 10)
146+ min: 0.0 ms
147+ median: 0.0 ms
148+ mean: 0.0 ms
149+ max: 0.0 ms
150+
151+๐Ÿƒ drop-5k-from-1M (runs: 10)
152+ min: 0.0 ms
153+ median: 0.0 ms
154+ mean: 0.0 ms
155+ max: 0.0 ms
156+
157+๐Ÿƒ dropWhile-half-1M (runs: 10)
158+ min: 0.0 ms
159+ median: 0.0 ms
160+ mean: 0.0 ms
161+ max: 0.0 ms
162+
163+๐Ÿƒ grouped-chunks-of-100-from-50k (runs: 10)
164+ min: 0.0 ms
165+ median: 0.0 ms
166+ mean: 0.1 ms
167+ max: 1.0 ms
168+
169+๐Ÿƒ partition-even-odd-50k (runs: 10)
170+ min: 0.0 ms
171+ median: 0.0 ms
172+ mean: 0.0 ms
173+ max: 0.0 ms
174+
175+๐Ÿƒ append-2x500k (runs: 10)
176+ min: 0.0 ms
177+ median: 0.0 ms
178+ mean: 0.0 ms
179+ max: 0.0 ms
180+
181+๐Ÿƒ concat-10x10k (runs: 10)
182+ min: 0.0 ms
183+ median: 0.0 ms
184+ mean: 0.0 ms
185+ max: 0.0 ms
186+
187+๐Ÿƒ zipWith-add-2x500k (runs: 10)
188+ min: 0.0 ms
189+ median: 0.0 ms
190+ mean: 0.0 ms
191+ max: 0.0 ms
spago.yaml+2โˆ’5
@@ -1,7 +1,3 @@
1โˆ’workspace:
2โˆ’ package_set:
3โˆ’ registry: 51.1.0
4โˆ’
51package:
62 name: yoga-om-strom
73 publish:
@@ -12,7 +8,8 @@ package:
128 - "src/Test/**"
139 location:
1410 githubOwner: rowtype-yoga
15โˆ’ githubRepo: purescript-yoga-om-strom
11+ githubRepo: purescript-yoga-om
12+ subdir: packages/yoga-om-strom
1613 dependencies:
1714 - aff: ">=7.1.0 <8.0.0"
1815 - arrays: ">=7.3.0 <8.0.0"
src/Yoga/Om/Strom.purs+396โˆ’61
@@ -74,16 +74,29 @@ module Yoga.Om.Strom
7474 , zipWithStrom
7575 , interleave
7676 , intersperse
77+ , HaltStrategy(..)
7778 , merge
7879 , mergeAll
80+ , mergeWith
81+ , mergeHaltEither
82+ , mergeHaltLeft
83+ , mergeHaltRight
7984 , mergeND
85+ , mergeNDWith
8086 , mergeAllND
87+ , mergeAllNDWith
8188 , race
8289 , raceAll
8390 -- Parallel Processing
8491 , mapPar
8592 , mapMPar
8693 , foreachPar
94+ , mapParUnordered
95+ , mapMParUnordered
96+ , foreachParUnordered
97+ -- Buffering
98+ , buffer
99+ , bufferChunks
87100 -- Error Handling
88101 , catchAll
89102 -- , catchSome -- TODO: Fix type constraints
@@ -115,7 +128,7 @@ import Data.Functor (map) as Functor
115128import Data.List as List
116129import Data.Maybe (Maybe(..))
117130import Data.Time.Duration (Milliseconds(..))
118โˆ’import Data.Traversable (traverse)
131+import Data.Traversable (traverse, traverse_)
119132import Data.Tuple (Tuple(..))
120133import Data.Variant (Variant)
121134import Effect.Aff (Aff, Error, delay, forkAff, joinFiber, killFiber)
@@ -123,7 +136,10 @@ import Effect.Aff.AVar as AVar
123136import Effect.Aff.Class (liftAff)
124137import Effect.Exception as Exception
125138import Effect.Now (now)
139+import Effect.Ref as Ref
126140import Effect.Class (liftEffect)
141+import Data.CatQueue as CatQueue
142+import Data.CatQueue (CatQueue)
127143import Data.DateTime.Instant (unInstant)
128144import Yoga.Om (Om)
129145import Yoga.Om as Om
@@ -175,6 +191,29 @@ data QueueItem a
175191
176192derive instance functorQueueItem :: Functor QueueItem
177193
194+-- | Messages for mergeAllND buffered queue
195+data MergeAllItem a
196+ = MergeChunk (Array a)
197+ | MergeDone
198+
199+derive instance functorMergeAllItem :: Functor MergeAllItem
200+
201+-- | Merge termination strategy (like ZStream HaltStrategy)
202+data HaltStrategy
203+ = HaltBoth
204+ | HaltEither
205+ | HaltLeft
206+ | HaltRight
207+
208+derive instance eqHaltStrategy :: Eq HaltStrategy
209+
210+-- | Messages for buffering queues
211+data BufferItem a
212+ = BufferChunk (Array a)
213+ | BufferDone
214+
215+derive instance functorBufferItem :: Functor BufferItem
216+
178217--------------------------------------------------------------------------------
179218-- Construction
180219--------------------------------------------------------------------------------
@@ -1097,41 +1136,187 @@ intersperse separator stream = intersperseHelper true stream
10971136 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
10981137 pure $ Loop $ Tuple (Just withSeparators) (intersperseHelper false next)
10991138
1139+--------------------------------------------------------------------------------
1140+-- Buffering
1141+--------------------------------------------------------------------------------
1142+
1143+-- | Buffer stream chunks with backpressure (preserves chunking)
1144+bufferChunks :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
1145+bufferChunks capacity stream
1146+ | capacity <= 0 = stream
1147+ | otherwise = mkStrom do
1148+ ctx <- Om.ask
1149+ liftAff do
1150+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (BufferItem a))
1151+ sizeRef <- liftEffect $ Ref.new 0
1152+ mutex <- AVar.new unit
1153+ notEmpty <- AVar.empty
1154+ notFull <- AVar.new unit
1155+
1156+ let
1157+ enqueue item = do
1158+ AVar.take notFull
1159+ AVar.take mutex
1160+ size <- liftEffect $ Ref.read sizeRef
1161+ let wasEmpty = size == 0
1162+ buf <- liftEffect $ Ref.read bufferRef
1163+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1164+ liftEffect $ Ref.write (size + 1) sizeRef
1165+ AVar.put unit mutex
1166+ when wasEmpty $ AVar.put unit notEmpty
1167+ when (size + 1 < capacity) $ AVar.put unit notFull
1168+
1169+ dequeue = do
1170+ AVar.take notEmpty
1171+ AVar.take mutex
1172+ buf <- liftEffect $ Ref.read bufferRef
1173+ size <- liftEffect $ Ref.read sizeRef
1174+ case CatQueue.uncons buf of
1175+ Nothing -> do
1176+ AVar.put unit mutex
1177+ dequeue
1178+ Just (Tuple head tail) -> do
1179+ liftEffect $ Ref.write tail bufferRef
1180+ liftEffect $ Ref.write (size - 1) sizeRef
1181+ let hasMore = (size - 1) > 0
1182+ let wasFull = size == capacity
1183+ AVar.put unit mutex
1184+ when hasMore $ AVar.put unit notEmpty
1185+ when wasFull $ AVar.put unit notFull
1186+ pure head
1187+
1188+ producer s = do
1189+ stepResult <- Om.runReader ctx (runStrom s)
1190+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1191+ case step of
1192+ Done Nothing -> enqueue BufferDone
1193+ Done (Just chunk) -> do
1194+ enqueue (BufferChunk chunk)
1195+ enqueue BufferDone
1196+ Loop (Tuple maybeChunk next) -> do
1197+ case maybeChunk of
1198+ Just chunk -> enqueue (BufferChunk chunk)
1199+ Nothing -> pure unit
1200+ producer next
1201+
1202+ fiber <- forkAff (producer stream)
1203+
1204+ let
1205+ consumer = mkStrom $ liftAff do
1206+ msg <- dequeue
1207+ case msg of
1208+ BufferDone -> do
1209+ killFiber (Exception.error "done") fiber
1210+ pure $ Done Nothing
1211+ BufferChunk chunk ->
1212+ pure $ Loop $ Tuple (Just chunk) consumer
1213+
1214+ stepResult <- Om.runReader ctx (runStrom consumer)
1215+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1216+
1217+-- | Buffer stream elements with backpressure (destroys chunking)
1218+buffer :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
1219+buffer capacity stream
1220+ | capacity <= 0 = stream
1221+ | otherwise = mkStrom do
1222+ ctx <- Om.ask
1223+ liftAff do
1224+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (BufferItem a))
1225+ sizeRef <- liftEffect $ Ref.new 0
1226+ mutex <- AVar.new unit
1227+ notEmpty <- AVar.empty
1228+ notFull <- AVar.new unit
1229+
1230+ let
1231+ enqueue item = do
1232+ AVar.take notFull
1233+ AVar.take mutex
1234+ size <- liftEffect $ Ref.read sizeRef
1235+ let wasEmpty = size == 0
1236+ buf <- liftEffect $ Ref.read bufferRef
1237+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1238+ liftEffect $ Ref.write (size + 1) sizeRef
1239+ AVar.put unit mutex
1240+ when wasEmpty $ AVar.put unit notEmpty
1241+ when (size + 1 < capacity) $ AVar.put unit notFull
1242+
1243+ dequeue = do
1244+ AVar.take notEmpty
1245+ AVar.take mutex
1246+ buf <- liftEffect $ Ref.read bufferRef
1247+ size <- liftEffect $ Ref.read sizeRef
1248+ case CatQueue.uncons buf of
1249+ Nothing -> do
1250+ AVar.put unit mutex
1251+ dequeue
1252+ Just (Tuple head tail) -> do
1253+ liftEffect $ Ref.write tail bufferRef
1254+ liftEffect $ Ref.write (size - 1) sizeRef
1255+ let hasMore = (size - 1) > 0
1256+ let wasFull = size == capacity
1257+ AVar.put unit mutex
1258+ when hasMore $ AVar.put unit notEmpty
1259+ when wasFull $ AVar.put unit notFull
1260+ pure head
1261+
1262+ producer s = do
1263+ stepResult <- Om.runReader ctx (runStrom s)
1264+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1265+ case step of
1266+ Done Nothing -> enqueue BufferDone
1267+ Done (Just chunk) -> do
1268+ traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1269+ enqueue BufferDone
1270+ Loop (Tuple maybeChunk next) -> do
1271+ case maybeChunk of
1272+ Just chunk -> traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1273+ Nothing -> pure unit
1274+ producer next
1275+
1276+ fiber <- forkAff (producer stream)
1277+
1278+ let
1279+ consumer = mkStrom $ liftAff do
1280+ msg <- dequeue
1281+ case msg of
1282+ BufferDone -> do
1283+ killFiber (Exception.error "done") fiber
1284+ pure $ Done Nothing
1285+ BufferChunk chunk ->
1286+ pure $ Loop $ Tuple (Just chunk) consumer
1287+
1288+ stepResult <- Om.runReader ctx (runStrom consumer)
1289+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1290+
11001291--------------------------------------------------------------------------------
11011292-- Concurrency
11021293--------------------------------------------------------------------------------
11031294
1104โˆ’-- | Merge two streams concurrently
1295+-- | Merge two streams concurrently (nondeterministic like ZStream merge)
11051296-- | Elements are emitted as soon as they're available from either stream
11061297merge :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1107โˆ’merge s1 s2 = mkStrom do
1108โˆ’ -- Pull from both streams concurrently
1109โˆ’ step1 <- runStrom s1
1110โˆ’ step2 <- runStrom s2
1111โˆ’ case step1, step2 of
1112โˆ’ Done Nothing, Done Nothing -> pure $ Done Nothing
1113โˆ’ Done Nothing, Done (Just chunk2) -> pure $ Done $ Just chunk2
1114โˆ’ Done (Just chunk1), Done Nothing -> pure $ Done $ Just chunk1
1115โˆ’ Done (Just chunk1), Done (Just chunk2) -> pure $ Done $ Just (chunk1 <> chunk2)
1116โˆ’ Done Nothing, Loop (Tuple maybeChunk2 next2) ->
1117โˆ’ pure $ Loop $ Tuple maybeChunk2 next2
1118โˆ’ Done (Just chunk1), Loop (Tuple maybeChunk2 next2) ->
1119โˆ’ case maybeChunk2 of
1120โˆ’ Nothing -> pure $ Loop $ Tuple (Just chunk1) next2
1121โˆ’ Just chunk2 -> pure $ Loop $ Tuple (Just $ chunk1 <> chunk2) next2
1122โˆ’ Loop (Tuple maybeChunk1 next1), Done Nothing ->
1123โˆ’ pure $ Loop $ Tuple maybeChunk1 next1
1124โˆ’ Loop (Tuple maybeChunk1 next1), Done (Just chunk2) ->
1125โˆ’ case maybeChunk1 of
1126โˆ’ Nothing -> pure $ Loop $ Tuple (Just chunk2) next1
1127โˆ’ Just chunk1 -> pure $ Loop $ Tuple (Just $ chunk1 <> chunk2) next1
1128โˆ’ Loop (Tuple maybeChunk1 next1), Loop (Tuple maybeChunk2 next2) ->
1129โˆ’ -- Both streams continue, merge chunks and continue with both
1130โˆ’ case maybeChunk1, maybeChunk2 of
1131โˆ’ Nothing, Nothing -> pure $ Loop $ Tuple Nothing (merge next1 next2)
1132โˆ’ Just c1, Nothing -> pure $ Loop $ Tuple (Just c1) (merge next1 next2)
1133โˆ’ Nothing, Just c2 -> pure $ Loop $ Tuple (Just c2) (merge next1 next2)
1134โˆ’ Just c1, Just c2 -> pure $ Loop $ Tuple (Just $ c1 <> c2) (merge next1 next2)
1298+merge = mergeNDWith 1024
1299+
1300+-- | Merge with halt strategy and mapping functions (ZStream-like)
1301+mergeWith
1302+ :: forall ctx err a b c
1303+ . HaltStrategy
1304+ -> (a -> c)
1305+ -> (b -> c)
1306+ -> Strom ctx err a
1307+ -> Strom ctx err b
1308+ -> Strom ctx err c
1309+mergeWith strategy f g s1 s2 =
1310+ mergeNDWithStrategy 1024 strategy (mapStrom f s1) (mapStrom g s2)
1311+
1312+mergeHaltEither :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1313+mergeHaltEither = mergeNDWithStrategy 1024 HaltEither
1314+
1315+mergeHaltLeft :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1316+mergeHaltLeft = mergeNDWithStrategy 1024 HaltLeft
1317+
1318+mergeHaltRight :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1319+mergeHaltRight = mergeNDWithStrategy 1024 HaltRight
11351320
11361321-- | Merge multiple streams concurrently
11371322mergeAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
@@ -1140,27 +1325,83 @@ mergeAll streams = Array.foldl merge empty streams
11401325-- | Race two streams - returns elements from whichever produces first
11411326-- | Uses Om.race for true concurrent racing
11421327race :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1143โˆ’race s1 s2 = mkStrom do
1144โˆ’ Om.race [ runStrom s1, runStrom s2 ]
1328+race = mergeHaltEither
11451329
11461330-- | Race multiple streams - returns elements from whichever produces first
11471331raceAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1148โˆ’raceAll streams = mkStrom do
1149โˆ’ Om.race (Functor.map runStrom streams)
1332+raceAll streams =
1333+ case Array.uncons streams of
1334+ Nothing -> empty
1335+ Just { head, tail } -> Array.foldl mergeHaltEither head tail
11501336
11511337-- | Non-deterministic merge: Merge two streams concurrently, emitting elements as soon as they're available
11521338-- | Unlike `merge`, this runs both streams in parallel and elements may arrive in any order
11531339-- | The stream that produces results faster will have its elements appear first
11541340-- | Optimized: Spawns producer fibers once and uses a queue for communication
11551341mergeND :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1156โˆ’mergeND stream1 stream2 = mkStrom do
1342+mergeND = mergeNDWith 1024
1343+
1344+-- | Non-deterministic merge with configurable buffer capacity (chunk queue)
1345+mergeNDWith :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1346+mergeNDWith bufferCapacity = mergeNDWithStrategy bufferCapacity HaltBoth
1347+
1348+mergeNDWithStrategy
1349+ :: forall ctx err a
1350+ . Int
1351+ -> HaltStrategy
1352+ -> Strom ctx err a
1353+ -> Strom ctx err a
1354+ -> Strom ctx err a
1355+mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
11571356 -- Capture context
11581357 ctx <- Om.ask
1159โˆ’
1358+
11601359 liftAff do
1161โˆ’ -- Create communication queue
1162โˆ’ queue <- AVar.empty
1163โˆ’
1360+ -- Create buffered queue with backpressure
1361+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (QueueItem a))
1362+ sizeRef <- liftEffect $ Ref.new 0
1363+ mutex <- AVar.new unit
1364+ notEmpty <- AVar.empty
1365+ notFull <- AVar.new unit
1366+
1367+ let
1368+ shouldStop doneLeft doneRight =
1369+ case strategy of
1370+ HaltBoth -> doneLeft && doneRight
1371+ HaltEither -> doneLeft || doneRight
1372+ HaltLeft -> doneLeft
1373+ HaltRight -> doneRight
1374+ enqueue item = do
1375+ AVar.take notFull
1376+ AVar.take mutex
1377+ size <- liftEffect $ Ref.read sizeRef
1378+ let wasEmpty = size == 0
1379+ buf <- liftEffect $ Ref.read bufferRef
1380+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1381+ liftEffect $ Ref.write (size + 1) sizeRef
1382+ AVar.put unit mutex
1383+ when wasEmpty $ AVar.put unit notEmpty
1384+ when (size + 1 < bufferCapacity) $ AVar.put unit notFull
1385+
1386+ dequeue = do
1387+ AVar.take notEmpty
1388+ AVar.take mutex
1389+ buf <- liftEffect $ Ref.read bufferRef
1390+ size <- liftEffect $ Ref.read sizeRef
1391+ case CatQueue.uncons buf of
1392+ Nothing -> do
1393+ AVar.put unit mutex
1394+ dequeue
1395+ Just (Tuple head tail) -> do
1396+ liftEffect $ Ref.write tail bufferRef
1397+ liftEffect $ Ref.write (size - 1) sizeRef
1398+ let hasMore = (size - 1) > 0
1399+ let wasFull = size == bufferCapacity
1400+ AVar.put unit mutex
1401+ when hasMore $ AVar.put unit notEmpty
1402+ when wasFull $ AVar.put unit notFull
1403+ pure head
1404+
11641405 -- Spawn producer fiber for stream1
11651406 fiber1 <- forkAff do
11661407 let
@@ -1168,18 +1409,18 @@ mergeND stream1 stream2 = mkStrom do
11681409 stepResult <- Om.runReader ctx (runStrom s)
11691410 step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
11701411 case step of
1171โˆ’ Done Nothing ->
1172โˆ’ AVar.put (Left StreamId1) queue -- Signal stream1 done
1412+ Done Nothing ->
1413+ enqueue (StreamDone StreamId1)
11731414 Done (Just chunk) -> do
1174โˆ’ AVar.put (Right chunk) queue -- Emit final chunk
1175โˆ’ AVar.put (Left StreamId1) queue -- Signal stream1 done
1415+ enqueue (DataChunk chunk StreamId1)
1416+ enqueue (StreamDone StreamId1)
11761417 Loop (Tuple maybeChunk next) -> do
11771418 case maybeChunk of
1178โˆ’ Just chunk -> AVar.put (Right chunk) queue
1419+ Just chunk -> enqueue (DataChunk chunk StreamId1)
11791420 Nothing -> pure unit
11801421 producer next
11811422 producer stream1
1182โˆ’
1423+
11831424 -- Spawn producer fiber for stream2
11841425 fiber2 <- forkAff do
11851426 let
@@ -1187,44 +1428,126 @@ mergeND stream1 stream2 = mkStrom do
11871428 stepResult <- Om.runReader ctx (runStrom s)
11881429 step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
11891430 case step of
1190โˆ’ Done Nothing ->
1191โˆ’ AVar.put (Left StreamId2) queue -- Signal stream2 done
1431+ Done Nothing ->
1432+ enqueue (StreamDone StreamId2)
11921433 Done (Just chunk) -> do
1193โˆ’ AVar.put (Right chunk) queue -- Emit final chunk
1194โˆ’ AVar.put (Left StreamId2) queue -- Signal stream2 done
1434+ enqueue (DataChunk chunk StreamId2)
1435+ enqueue (StreamDone StreamId2)
11951436 Loop (Tuple maybeChunk next) -> do
11961437 case maybeChunk of
1197โˆ’ Just chunk -> AVar.put (Right chunk) queue
1438+ Just chunk -> enqueue (DataChunk chunk StreamId2)
11981439 Nothing -> pure unit
11991440 producer next
12001441 producer stream2
1201โˆ’
1442+
12021443 -- Consumer: pull from queue
12031444 let
1204โˆ’ consumer doneCount = mkStrom $ liftAff do
1205โˆ’ if doneCount >= 2 then do
1445+ consumer doneLeft doneRight = mkStrom $ liftAff do
1446+ if shouldStop doneLeft doneRight then do
12061447 -- Both streams done, cleanup
12071448 killFiber (Exception.error "done") fiber1
12081449 killFiber (Exception.error "done") fiber2
12091450 pure $ Done Nothing
12101451 else do
1211โˆ’ msg <- AVar.take queue
1452+ msg <- dequeue
12121453 case msg of
1213โˆ’ Left _streamId -> do
1454+ StreamDone streamId -> do
12141455 -- One stream finished, recurse
1215โˆ’ stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
1456+ let
1457+ doneLeft' = doneLeft || streamId == StreamId1
1458+ doneRight' = doneRight || streamId == StreamId2
1459+ stepResult <- Om.runReader ctx (runStrom $ consumer doneLeft' doneRight')
12161460 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1217โˆ’ Right chunk ->
1461+ DataChunk chunk _streamId ->
12181462 -- Got a chunk
1219โˆ’ pure $ Loop $ Tuple (Just chunk) (consumer doneCount)
1220โˆ’
1221โˆ’ stepResult <- Om.runReader ctx (runStrom $ consumer 0)
1463+ pure $ Loop $ Tuple (Just chunk) (consumer doneLeft doneRight)
1464+
1465+ stepResult <- Om.runReader ctx (runStrom $ consumer false false)
12221466 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
12231467
12241468-- | Non-deterministic merge of multiple streams
12251469-- | All streams run concurrently and emit elements as soon as they're available
12261470mergeAllND :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1227โˆ’mergeAllND streams = Array.foldl mergeND empty streams
1471+mergeAllND = mergeAllNDWith 1024
1472+
1473+-- | Non-deterministic merge of multiple streams with configurable buffer capacity
1474+mergeAllNDWith :: forall ctx err a. Int -> Array (Strom ctx err a) -> Strom ctx err a
1475+mergeAllNDWith bufferCapacity streams
1476+ | Array.null streams = empty
1477+ | otherwise = mkStrom do
1478+ ctx <- Om.ask
1479+ liftAff do
1480+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (MergeAllItem a))
1481+ sizeRef <- liftEffect $ Ref.new 0
1482+ mutex <- AVar.new unit
1483+ notEmpty <- AVar.empty
1484+ notFull <- AVar.new unit
1485+
1486+ let
1487+ enqueue item = do
1488+ AVar.take notFull
1489+ AVar.take mutex
1490+ size <- liftEffect $ Ref.read sizeRef
1491+ let wasEmpty = size == 0
1492+ buf <- liftEffect $ Ref.read bufferRef
1493+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1494+ liftEffect $ Ref.write (size + 1) sizeRef
1495+ AVar.put unit mutex
1496+ when wasEmpty $ AVar.put unit notEmpty
1497+ when (size + 1 < bufferCapacity) $ AVar.put unit notFull
1498+
1499+ dequeue = do
1500+ AVar.take notEmpty
1501+ AVar.take mutex
1502+ buf <- liftEffect $ Ref.read bufferRef
1503+ size <- liftEffect $ Ref.read sizeRef
1504+ case CatQueue.uncons buf of
1505+ Nothing -> do
1506+ AVar.put unit mutex
1507+ dequeue
1508+ Just (Tuple head tail) -> do
1509+ liftEffect $ Ref.write tail bufferRef
1510+ liftEffect $ Ref.write (size - 1) sizeRef
1511+ let hasMore = (size - 1) > 0
1512+ let wasFull = size == bufferCapacity
1513+ AVar.put unit mutex
1514+ when hasMore $ AVar.put unit notEmpty
1515+ when wasFull $ AVar.put unit notFull
1516+ pure head
1517+
1518+ producer s = do
1519+ stepResult <- Om.runReader ctx (runStrom s)
1520+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1521+ case step of
1522+ Done Nothing -> enqueue MergeDone
1523+ Done (Just chunk) -> do
1524+ enqueue (MergeChunk chunk)
1525+ enqueue MergeDone
1526+ Loop (Tuple maybeChunk next) -> do
1527+ case maybeChunk of
1528+ Just chunk -> enqueue (MergeChunk chunk)
1529+ Nothing -> pure unit
1530+ producer next
1531+
1532+ fibers <- traverse (\s -> forkAff (producer s)) streams
1533+
1534+ let
1535+ totalStreams = Array.length streams
1536+ consumer doneCount = mkStrom $ liftAff do
1537+ if doneCount >= totalStreams then do
1538+ traverse_ (\fiber -> killFiber (Exception.error "done") fiber) fibers
1539+ pure $ Done Nothing
1540+ else do
1541+ msg <- dequeue
1542+ case msg of
1543+ MergeDone -> do
1544+ stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
1545+ either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1546+ MergeChunk chunk ->
1547+ pure $ Loop $ Tuple (Just chunk) (consumer doneCount)
1548+
1549+ stepResult <- Om.runReader ctx (runStrom $ consumer 0)
1550+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
12281551
12291552--------------------------------------------------------------------------------
12301553-- Parallel Processing
@@ -1264,14 +1587,26 @@ mapPar concurrency f stream
12641587 in
12651588 if Array.null group then [] else Array.cons group (chunksOf n rest)
12661589
1590+-- | Unordered parallel map (currently same as mapPar)
1591+mapParUnordered :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1592+mapParUnordered = mapPar
1593+
12671594-- | Alias for mapPar with explicit "M" naming
12681595mapMPar :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
12691596mapMPar = mapPar
12701597
1598+-- | Alias for unordered mapPar with explicit "M" naming
1599+mapMParUnordered :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1600+mapMParUnordered = mapParUnordered
1601+
12711602-- | For each element, execute an effect in parallel with bounded concurrency
12721603foreachPar :: forall ctx err a. Int -> (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
12731604foreachPar concurrency f stream = runDrain (mapPar concurrency f stream)
12741605
1606+-- | Unordered parallel foreach
1607+foreachParUnordered :: forall ctx err a. Int -> (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
1608+foreachParUnordered concurrency f stream = runDrain (mapParUnordered concurrency f stream)
1609+
12751610--------------------------------------------------------------------------------
12761611-- Error Handling
12771612--------------------------------------------------------------------------------