git.iko.soy/rowtype-yoga/purescript-yoga-om-strom
on main help sign in
Extract yoga-om-strom from monorepo
dd20ac6Mark
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โˆ’
5package:
1package:
6 name: yoga-om-strom
2 name: yoga-om-strom
7 publish:
3 publish:
@@ -12,7 +8,8 @@ package:
12 - "src/Test/**"
8 - "src/Test/**"
13 location:
9 location:
14 githubOwner: rowtype-yoga
10 githubOwner: rowtype-yoga
15โˆ’ githubRepo: purescript-yoga-om-strom
11+ githubRepo: purescript-yoga-om
12+ subdir: packages/yoga-om-strom
16 dependencies:
13 dependencies:
17 - aff: ">=7.1.0 <8.0.0"
14 - aff: ">=7.1.0 <8.0.0"
18 - arrays: ">=7.3.0 <8.0.0"
15 - arrays: ">=7.3.0 <8.0.0"
src/Yoga/Om/Strom.purs+396โˆ’61
@@ -74,16 +74,29 @@ module Yoga.Om.Strom
74 , zipWithStrom
74 , zipWithStrom
75 , interleave
75 , interleave
76 , intersperse
76 , intersperse
77+ , HaltStrategy(..)
77 , merge
78 , merge
78 , mergeAll
79 , mergeAll
80+ , mergeWith
81+ , mergeHaltEither
82+ , mergeHaltLeft
83+ , mergeHaltRight
79 , mergeND
84 , mergeND
85+ , mergeNDWith
80 , mergeAllND
86 , mergeAllND
87+ , mergeAllNDWith
81 , race
88 , race
82 , raceAll
89 , raceAll
83 -- Parallel Processing
90 -- Parallel Processing
84 , mapPar
91 , mapPar
85 , mapMPar
92 , mapMPar
86 , foreachPar
93 , foreachPar
94+ , mapParUnordered
95+ , mapMParUnordered
96+ , foreachParUnordered
97+ -- Buffering
98+ , buffer
99+ , bufferChunks
87 -- Error Handling
100 -- Error Handling
88 , catchAll
101 , catchAll
89 -- , catchSome -- TODO: Fix type constraints
102 -- , catchSome -- TODO: Fix type constraints
@@ -115,7 +128,7 @@ import Data.Functor (map) as Functor
115import Data.List as List
128import Data.List as List
116import Data.Maybe (Maybe(..))
129import Data.Maybe (Maybe(..))
117import Data.Time.Duration (Milliseconds(..))
130import Data.Time.Duration (Milliseconds(..))
118โˆ’import Data.Traversable (traverse)
131+import Data.Traversable (traverse, traverse_)
119import Data.Tuple (Tuple(..))
132import Data.Tuple (Tuple(..))
120import Data.Variant (Variant)
133import Data.Variant (Variant)
121import Effect.Aff (Aff, Error, delay, forkAff, joinFiber, killFiber)
134import Effect.Aff (Aff, Error, delay, forkAff, joinFiber, killFiber)
@@ -123,7 +136,10 @@ import Effect.Aff.AVar as AVar
123import Effect.Aff.Class (liftAff)
136import Effect.Aff.Class (liftAff)
124import Effect.Exception as Exception
137import Effect.Exception as Exception
125import Effect.Now (now)
138import Effect.Now (now)
139+import Effect.Ref as Ref
126import Effect.Class (liftEffect)
140import Effect.Class (liftEffect)
141+import Data.CatQueue as CatQueue
142+import Data.CatQueue (CatQueue)
127import Data.DateTime.Instant (unInstant)
143import Data.DateTime.Instant (unInstant)
128import Yoga.Om (Om)
144import Yoga.Om (Om)
129import Yoga.Om as Om
145import Yoga.Om as Om
@@ -175,6 +191,29 @@ data QueueItem a
175
191
176derive instance functorQueueItem :: Functor QueueItem
192derive instance functorQueueItem :: Functor QueueItem
177
193
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+
178--------------------------------------------------------------------------------
217--------------------------------------------------------------------------------
179-- Construction
218-- Construction
180--------------------------------------------------------------------------------
219--------------------------------------------------------------------------------
@@ -1097,41 +1136,187 @@ intersperse separator stream = intersperseHelper true stream
1097 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1136 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1098 pure $ Loop $ Tuple (Just withSeparators) (intersperseHelper false next)
1137 pure $ Loop $ Tuple (Just withSeparators) (intersperseHelper false next)
1099
1138
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+
1100--------------------------------------------------------------------------------
1291--------------------------------------------------------------------------------
1101-- Concurrency
1292-- Concurrency
1102--------------------------------------------------------------------------------
1293--------------------------------------------------------------------------------
1103
1294
1104โˆ’-- | Merge two streams concurrently
1295+-- | Merge two streams concurrently (nondeterministic like ZStream merge)
1105-- | Elements are emitted as soon as they're available from either stream
1296-- | Elements are emitted as soon as they're available from either stream
1106merge :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1297merge :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1107โˆ’merge s1 s2 = mkStrom do
1298+merge = mergeNDWith 1024
1108โˆ’ -- Pull from both streams concurrently
1299+
1109โˆ’ step1 <- runStrom s1
1300+-- | Merge with halt strategy and mapping functions (ZStream-like)
1110โˆ’ step2 <- runStrom s2
1301+mergeWith
1111โˆ’ case step1, step2 of
1302+ :: forall ctx err a b c
1112โˆ’ Done Nothing, Done Nothing -> pure $ Done Nothing
1303+ . HaltStrategy
1113โˆ’ Done Nothing, Done (Just chunk2) -> pure $ Done $ Just chunk2
1304+ -> (a -> c)
1114โˆ’ Done (Just chunk1), Done Nothing -> pure $ Done $ Just chunk1
1305+ -> (b -> c)
1115โˆ’ Done (Just chunk1), Done (Just chunk2) -> pure $ Done $ Just (chunk1 <> chunk2)
1306+ -> Strom ctx err a
1116โˆ’ Done Nothing, Loop (Tuple maybeChunk2 next2) ->
1307+ -> Strom ctx err b
1117โˆ’ pure $ Loop $ Tuple maybeChunk2 next2
1308+ -> Strom ctx err c
1118โˆ’ Done (Just chunk1), Loop (Tuple maybeChunk2 next2) ->
1309+mergeWith strategy f g s1 s2 =
1119โˆ’ case maybeChunk2 of
1310+ mergeNDWithStrategy 1024 strategy (mapStrom f s1) (mapStrom g s2)
1120โˆ’ Nothing -> pure $ Loop $ Tuple (Just chunk1) next2
1311+
1121โˆ’ Just chunk2 -> pure $ Loop $ Tuple (Just $ chunk1 <> chunk2) next2
1312+mergeHaltEither :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1122โˆ’ Loop (Tuple maybeChunk1 next1), Done Nothing ->
1313+mergeHaltEither = mergeNDWithStrategy 1024 HaltEither
1123โˆ’ pure $ Loop $ Tuple maybeChunk1 next1
1314+
1124โˆ’ Loop (Tuple maybeChunk1 next1), Done (Just chunk2) ->
1315+mergeHaltLeft :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1125โˆ’ case maybeChunk1 of
1316+mergeHaltLeft = mergeNDWithStrategy 1024 HaltLeft
1126โˆ’ Nothing -> pure $ Loop $ Tuple (Just chunk2) next1
1317+
1127โˆ’ Just chunk1 -> pure $ Loop $ Tuple (Just $ chunk1 <> chunk2) next1
1318+mergeHaltRight :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1128โˆ’ Loop (Tuple maybeChunk1 next1), Loop (Tuple maybeChunk2 next2) ->
1319+mergeHaltRight = mergeNDWithStrategy 1024 HaltRight
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)
1135
1320
1136-- | Merge multiple streams concurrently
1321-- | Merge multiple streams concurrently
1137mergeAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1322mergeAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
@@ -1140,27 +1325,83 @@ mergeAll streams = Array.foldl merge empty streams
1140-- | Race two streams - returns elements from whichever produces first
1325-- | Race two streams - returns elements from whichever produces first
1141-- | Uses Om.race for true concurrent racing
1326-- | Uses Om.race for true concurrent racing
1142race :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1327race :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1143โˆ’race s1 s2 = mkStrom do
1328+race = mergeHaltEither
1144โˆ’ Om.race [ runStrom s1, runStrom s2 ]
1145
1329
1146-- | Race multiple streams - returns elements from whichever produces first
1330-- | Race multiple streams - returns elements from whichever produces first
1147raceAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1331raceAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1148โˆ’raceAll streams = mkStrom do
1332+raceAll streams =
1149โˆ’ Om.race (Functor.map runStrom streams)
1333+ case Array.uncons streams of
1334+ Nothing -> empty
1335+ Just { head, tail } -> Array.foldl mergeHaltEither head tail
1150
1336
1151-- | Non-deterministic merge: Merge two streams concurrently, emitting elements as soon as they're available
1337-- | Non-deterministic merge: Merge two streams concurrently, emitting elements as soon as they're available
1152-- | Unlike `merge`, this runs both streams in parallel and elements may arrive in any order
1338-- | Unlike `merge`, this runs both streams in parallel and elements may arrive in any order
1153-- | The stream that produces results faster will have its elements appear first
1339-- | The stream that produces results faster will have its elements appear first
1154-- | Optimized: Spawns producer fibers once and uses a queue for communication
1340-- | Optimized: Spawns producer fibers once and uses a queue for communication
1155mergeND :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1341mergeND :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
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
1156โˆ’mergeND stream1 stream2 = mkStrom do
1355+mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
1157 -- Capture context
1356 -- Capture context
1158 ctx <- Om.ask
1357 ctx <- Om.ask
1159โˆ’
1358+
1160 liftAff do
1359 liftAff do
1161โˆ’ -- Create communication queue
1360+ -- Create buffered queue with backpressure
1162โˆ’ queue <- AVar.empty
1361+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (QueueItem a))
1163โˆ’
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+
1164 -- Spawn producer fiber for stream1
1405 -- Spawn producer fiber for stream1
1165 fiber1 <- forkAff do
1406 fiber1 <- forkAff do
1166 let
1407 let
@@ -1168,18 +1409,18 @@ mergeND stream1 stream2 = mkStrom do
1168 stepResult <- Om.runReader ctx (runStrom s)
1409 stepResult <- Om.runReader ctx (runStrom s)
1169 step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1410 step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1170 case step of
1411 case step of
1171โˆ’ Done Nothing ->
1412+ Done Nothing ->
1172โˆ’ AVar.put (Left StreamId1) queue -- Signal stream1 done
1413+ enqueue (StreamDone StreamId1)
1173 Done (Just chunk) -> do
1414 Done (Just chunk) -> do
1174โˆ’ AVar.put (Right chunk) queue -- Emit final chunk
1415+ enqueue (DataChunk chunk StreamId1)
1175โˆ’ AVar.put (Left StreamId1) queue -- Signal stream1 done
1416+ enqueue (StreamDone StreamId1)
1176 Loop (Tuple maybeChunk next) -> do
1417 Loop (Tuple maybeChunk next) -> do
1177 case maybeChunk of
1418 case maybeChunk of
1178โˆ’ Just chunk -> AVar.put (Right chunk) queue
1419+ Just chunk -> enqueue (DataChunk chunk StreamId1)
1179 Nothing -> pure unit
1420 Nothing -> pure unit
1180 producer next
1421 producer next
1181 producer stream1
1422 producer stream1
1182โˆ’
1423+
1183 -- Spawn producer fiber for stream2
1424 -- Spawn producer fiber for stream2
1184 fiber2 <- forkAff do
1425 fiber2 <- forkAff do
1185 let
1426 let
@@ -1187,44 +1428,126 @@ mergeND stream1 stream2 = mkStrom do
1187 stepResult <- Om.runReader ctx (runStrom s)
1428 stepResult <- Om.runReader ctx (runStrom s)
1188 step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1429 step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1189 case step of
1430 case step of
1190โˆ’ Done Nothing ->
1431+ Done Nothing ->
1191โˆ’ AVar.put (Left StreamId2) queue -- Signal stream2 done
1432+ enqueue (StreamDone StreamId2)
1192 Done (Just chunk) -> do
1433 Done (Just chunk) -> do
1193โˆ’ AVar.put (Right chunk) queue -- Emit final chunk
1434+ enqueue (DataChunk chunk StreamId2)
1194โˆ’ AVar.put (Left StreamId2) queue -- Signal stream2 done
1435+ enqueue (StreamDone StreamId2)
1195 Loop (Tuple maybeChunk next) -> do
1436 Loop (Tuple maybeChunk next) -> do
1196 case maybeChunk of
1437 case maybeChunk of
1197โˆ’ Just chunk -> AVar.put (Right chunk) queue
1438+ Just chunk -> enqueue (DataChunk chunk StreamId2)
1198 Nothing -> pure unit
1439 Nothing -> pure unit
1199 producer next
1440 producer next
1200 producer stream2
1441 producer stream2
1201โˆ’
1442+
1202 -- Consumer: pull from queue
1443 -- Consumer: pull from queue
1203 let
1444 let
1204โˆ’ consumer doneCount = mkStrom $ liftAff do
1445+ consumer doneLeft doneRight = mkStrom $ liftAff do
1205โˆ’ if doneCount >= 2 then do
1446+ if shouldStop doneLeft doneRight then do
1206 -- Both streams done, cleanup
1447 -- Both streams done, cleanup
1207 killFiber (Exception.error "done") fiber1
1448 killFiber (Exception.error "done") fiber1
1208 killFiber (Exception.error "done") fiber2
1449 killFiber (Exception.error "done") fiber2
1209 pure $ Done Nothing
1450 pure $ Done Nothing
1210 else do
1451 else do
1211โˆ’ msg <- AVar.take queue
1452+ msg <- dequeue
1212 case msg of
1453 case msg of
1213โˆ’ Left _streamId -> do
1454+ StreamDone streamId -> do
1214 -- One stream finished, recurse
1455 -- One stream finished, recurse
1456+ let
1457+ doneLeft' = doneLeft || streamId == StreamId1
1458+ doneRight' = doneRight || streamId == StreamId2
1215โˆ’ stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
1459+ stepResult <- Om.runReader ctx (runStrom $ consumer doneLeft' doneRight')
1216 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1460 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1217โˆ’ Right chunk ->
1461+ DataChunk chunk _streamId ->
1218 -- Got a chunk
1462 -- Got a chunk
1219โˆ’ pure $ Loop $ Tuple (Just chunk) (consumer doneCount)
1463+ pure $ Loop $ Tuple (Just chunk) (consumer doneLeft doneRight)
1220โˆ’
1464+
1221โˆ’ stepResult <- Om.runReader ctx (runStrom $ consumer 0)
1465+ stepResult <- Om.runReader ctx (runStrom $ consumer false false)
1222 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1466 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1223
1467
1224-- | Non-deterministic merge of multiple streams
1468-- | Non-deterministic merge of multiple streams
1225-- | All streams run concurrently and emit elements as soon as they're available
1469-- | All streams run concurrently and emit elements as soon as they're available
1226mergeAllND :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1470mergeAllND :: 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
1228
1551
1229--------------------------------------------------------------------------------
1552--------------------------------------------------------------------------------
1230-- Parallel Processing
1553-- Parallel Processing
@@ -1264,14 +1587,26 @@ mapPar concurrency f stream
1264 in
1587 in
1265 if Array.null group then [] else Array.cons group (chunksOf n rest)
1588 if Array.null group then [] else Array.cons group (chunksOf n rest)
1266
1589
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+
1267-- | Alias for mapPar with explicit "M" naming
1594-- | Alias for mapPar with explicit "M" naming
1268mapMPar :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1595mapMPar :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1269mapMPar = mapPar
1596mapMPar = mapPar
1270
1597
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+
1271-- | For each element, execute an effect in parallel with bounded concurrency
1602-- | For each element, execute an effect in parallel with bounded concurrency
1272foreachPar :: forall ctx err a. Int -> (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
1603foreachPar :: forall ctx err a. Int -> (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
1273foreachPar concurrency f stream = runDrain (mapPar concurrency f stream)
1604foreachPar concurrency f stream = runDrain (mapPar concurrency f stream)
1274
1605
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+
1275--------------------------------------------------------------------------------
1610--------------------------------------------------------------------------------
1276-- Error Handling
1611-- Error Handling
1277--------------------------------------------------------------------------------
1612--------------------------------------------------------------------------------