git.iko.soy/rowtype-yoga/purescript-yoga-om-strom
on main help sign in
README.md+1
since splitsnapshots
unifiedsplit
@@ -441,3 +441,4 @@ This design enables:
441## Credits
441## Credits
442
442
443Inspired by [ZIO ZStream](https://zio.dev/reference/stream/zstream) from the Scala ZIO ecosystem.
443Inspired by [ZIO ZStream](https://zio.dev/reference/stream/zstream) from the Scala ZIO ecosystem.
444+
_excluded/Examples.pursnew file+523
@@ -0,0 +1,523 @@
1+module Yoga.Om.Strom.Examples where
2+
3+import Prelude
4+
5+import Control.Alt ((<|>))
6+import Data.Array as Array
7+import Data.Maybe (Maybe(..))
8+import Data.Tuple (Tuple(..))
9+import Effect.Aff (Aff)
10+import Effect.Class.Console as Console
11+import Data.Time.Duration (Milliseconds(..))
12+import Yoga.Om (Om)
13+import Yoga.Om as Om
14+import Yoga.Om.Strom as Strom
15+
16+--------------------------------------------------------------------------------
17+-- Example 1: Basic Stream Operations
18+--------------------------------------------------------------------------------
19+
20+-- | Simple range and mapping
21+example1 :: Om {} () (Array Int)
22+example1 = do
23+ Strom.rangeStrom 1 10
24+ # Strom.mapStrom (_ * 2)
25+ # Strom.filterStrom (_ > 5)
26+ # Strom.runCollect
27+
28+-- Expected output: [6, 8, 10, 12, 14, 16, 18]
29+
30+--------------------------------------------------------------------------------
31+-- Example 2: Effectful Operations
32+--------------------------------------------------------------------------------
33+
34+type LoggerContext = { logger :: String -> Aff Unit }
35+
36+-- | Stream with effectful mapping and tapping
37+example2 :: Om LoggerContext () Unit
38+example2 = do
39+ Strom.rangeStrom 1 5
40+ # Strom.tapMStrom (\n -> do
41+ { logger } <- Om.ask
42+ Om.fromAff $ logger $ "Processing: " <> show n
43+ )
44+ # Strom.mapMStrom (\n -> do
45+ Om.delay (Milliseconds 100.0)
46+ pure (n * n)
47+ )
48+ # Strom.traverseStrom_ (\n -> do
49+ { logger } <- Om.ask
50+ Om.fromAff $ logger $ "Result: " <> show n
51+ )
52+
53+-- Expected output:
54+-- Processing: 1
55+-- Processing: 2
56+-- ...
57+-- Result: 1
58+-- Result: 4
59+-- Result: 9
60+-- Result: 16
61+-- Result: 25
62+
63+--------------------------------------------------------------------------------
64+-- Example 3: Parallel Processing
65+--------------------------------------------------------------------------------
66+
67+-- | Process items in parallel (simulating API calls)
68+fetchUser :: Int -> Om {} () String
69+fetchUser id = do
70+ Om.delay (Milliseconds 100.0)
71+ pure $ "User #" <> show id
72+
73+example3 :: Om {} () (Array String)
74+example3 = do
75+ Strom.rangeStrom 1 20
76+ # Strom.mapMStrom fetchUser -- TODO: mapParallel not yet implemented
77+ # Strom.runCollect
78+
79+--------------------------------------------------------------------------------
80+-- Example 4: Stateful Transformations
81+--------------------------------------------------------------------------------
82+
83+-- | Running total using scan
84+example4 :: Om {} () (Array Int)
85+example4 = do
86+ Strom.fromArray [1, 2, 3, 4, 5]
87+ # Strom.scanStrom (\acc n -> acc + n) 0
88+ # Strom.runCollect
89+
90+-- Expected output: [1, 3, 6, 10, 15]
91+
92+-- | Stateful map with accumulator
93+example4b :: Om {} () (Array String)
94+example4b = do
95+ Strom.fromArray ["a", "b", "c", "d"]
96+ # Strom.mapAccumStrom (\count item -> Tuple (count + 1) (show count <> ": " <> item)) 1
97+ # Strom.runCollect
98+
99+-- Expected output: ["1: a", "2: b", "3: c", "4: d"]
100+
101+--------------------------------------------------------------------------------
102+-- Example 5: Stream Combination
103+--------------------------------------------------------------------------------
104+
105+-- | Zip two streams
106+example5 :: Om {} () (Array (Tuple Int String))
107+example5 = do
108+ let numbers = Strom.rangeStrom 1 5
109+ let letters = Strom.fromArray ["a", "b", "c", "d", "e"]
110+ -- TODO: zipStrom not yet implemented
111+ numbers # Strom.mapStrom (\n -> Tuple n "stub") # Strom.runCollect
112+
113+-- Expected output: [(1, "a"), (2, "b"), (3, "c"), (4, "d"), (5, "e")]
114+
115+-- | Interleave two streams deterministically
116+example5b :: Om {} () (Array Int)
117+example5b = do
118+ let evens = Strom.fromArray [2, 4, 6, 8]
119+ let odds = Strom.fromArray [1, 3, 5, 7]
120+ -- TODO: interleave not yet implemented
121+ (odds <> evens) # Strom.runCollect
122+
123+-- Expected output: [1, 2, 3, 4, 5, 6, 7, 8]
124+
125+-- | Merge streams (non-deterministic, race-based)
126+example5c :: Om {} () (Array Int)
127+example5c = do
128+ let stream1 = Strom.fromArray [1, 2, 3]
129+ let stream2 = Strom.fromArray [4, 5, 6]
130+ -- TODO: merge not yet implemented
131+ (stream1 <|> stream2) # Strom.runCollect
132+
133+-- Output: non-deterministic, could be [1, 2, 3, 4, 5, 6] or [4, 5, 6, 1, 2, 3] etc
134+
135+--------------------------------------------------------------------------------
136+-- Example 6: Infinite Streams
137+--------------------------------------------------------------------------------
138+
139+-- | Take from an infinite stream
140+example6 :: Om {} () (Array Int)
141+example6 = do
142+ Strom.iterateStrom (_ + 1) 0
143+ # Strom.takeStrom 10
144+ # Strom.runCollect
145+
146+-- Expected output: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
147+
148+-- | Repeat with condition
149+example6b :: Om {} () (Array Int)
150+example6b = do
151+ Strom.repeatStrom 42
152+ # Strom.takeWhileStrom (_ == 42) -- Will take all
153+ # Strom.takeStrom 5 -- But we limit to 5
154+ # Strom.runCollect
155+
156+-- Expected output: [42, 42, 42, 42, 42]
157+
158+--------------------------------------------------------------------------------
159+-- Example 7: Grouping and Chunking
160+--------------------------------------------------------------------------------
161+
162+-- | Group elements into chunks
163+example7 :: Om {} () (Array (Array Int))
164+example7 = do
165+ Strom.rangeStrom 1 11
166+ # Strom.mapStrom (\x -> [x]) -- TODO: grouped not yet implemented
167+ # Strom.runCollect
168+
169+-- Expected output: [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]
170+
171+-- | Process in batches
172+example7b :: Om LoggerContext () Unit
173+example7b = do
174+ Strom.rangeStrom 1 100
175+ # Strom.mapStrom (\x -> [x]) -- TODO: chunked not yet implemented
176+ # Strom.traverseStrom_ (\batch -> do
177+ { logger } <- Om.ask
178+ Om.fromAff $ logger $ "Processing batch of " <> show (Array.length batch) <> " items"
179+ )
180+
181+--------------------------------------------------------------------------------
182+-- Example 8: Deduplication
183+--------------------------------------------------------------------------------
184+
185+-- | Remove consecutive duplicates
186+example8 :: Om {} () (Array Int)
187+example8 = do
188+ Strom.fromArray [1, 1, 2, 2, 2, 3, 1, 1, 4]
189+ # Strom.changesStrom
190+ # Strom.runCollect
191+
192+-- Expected output: [1, 2, 3, 1, 4]
193+
194+--------------------------------------------------------------------------------
195+-- Example 9: Complex Pipeline (Real-world-ish)
196+--------------------------------------------------------------------------------
197+
198+type User = { id :: Int, name :: String, active :: Boolean }
199+
200+-- | Fetch users from API, process active ones, and save
201+example9 :: Om LoggerContext () Unit
202+example9 = do
203+ let users =
204+ [ { id: 1, name: "Alice", active: true }
205+ , { id: 2, name: "Bob", active: false }
206+ , { id: 3, name: "Charlie", active: true }
207+ , { id: 4, name: "Dave", active: true }
208+ , { id: 5, name: "Eve", active: false }
209+ ]
210+
211+ Strom.fromArray users
212+ # Strom.filterStrom _.active
213+ # Strom.tapMStrom (\user -> do
214+ { logger } <- Om.ask
215+ Om.fromAff $ logger $ "Processing active user: " <> user.name
216+ )
217+ # Strom.mapMStrom (\user -> do
218+ -- Simulate enriching user data
219+ Om.delay (Milliseconds 50.0)
220+ pure $ user.name <> " (ID: " <> show user.id <> ")"
221+ )
222+ # Strom.mapStrom (\x -> [x]) -- TODO: grouped not yet implemented
223+ # Strom.traverseStrom_ (\batch -> do
224+ { logger } <- Om.ask
225+ Om.fromAff $ logger $ "Saving batch: " <> show batch
226+ )
227+
228+-- Expected output:
229+-- Processing active user: Alice
230+-- Processing active user: Charlie
231+-- Processing active user: Dave
232+-- Saving batch: ["Alice (ID: 1)", "Charlie (ID: 3)"]
233+-- Saving batch: ["Dave (ID: 4)"]
234+
235+--------------------------------------------------------------------------------
236+-- Example 10: Unfold Pattern
237+--------------------------------------------------------------------------------
238+
239+-- | Generate Fibonacci sequence
240+fibonacciStream :: forall ctx err. Strom.Strom ctx err Int
241+fibonacciStream =
242+ Strom.unfoldStrom (\(Tuple a b) -> Just (Tuple a (Tuple b (a + b)))) (Tuple 0 1)
243+
244+example10 :: Om {} () (Array Int)
245+example10 = do
246+ fibonacciStream
247+ # Strom.takeStrom 10
248+ # Strom.runCollect
249+
250+-- Expected output: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
251+
252+-- | Unfold with effects (e.g., paginated API)
253+type Page = { items :: Array String, nextToken :: Maybe String }
254+
255+fetchPage :: String -> Om {} () Page
256+fetchPage token = do
257+ Om.delay (Milliseconds 100.0)
258+ -- Simulate API call
259+ if token == "start"
260+ then pure { items: ["item1", "item2"], nextToken: Just "page2" }
261+ else if token == "page2"
262+ then pure { items: ["item3", "item4"], nextToken: Just "page3" }
263+ else if token == "page3"
264+ then pure { items: ["item5"], nextToken: Nothing }
265+ else pure { items: [], nextToken: Nothing }
266+
267+example10b :: Om {} () (Array String)
268+example10b = do
269+ Strom.unfoldOmStrom (\token -> do
270+ page <- fetchPage token
271+ case page.nextToken of
272+ Nothing -> pure Nothing
273+ Just next -> pure $ Just $ Tuple page.items next
274+ ) "start"
275+ >>= Strom.fromArray
276+ # Strom.runCollect
277+
278+-- Expected output: ["item1", "item2", "item3", "item4", "item5"]
279+
280+--------------------------------------------------------------------------------
281+-- Example 11: Racing Streams
282+--------------------------------------------------------------------------------
283+
284+-- | Race multiple data sources
285+example11 :: Om {} () (Array Int)
286+example11 = do
287+ let source1 = Strom.fromOm do
288+ Om.delay (Milliseconds 100.0)
289+ pure 1
290+
291+ source2 = Strom.fromOm do
292+ Om.delay (Milliseconds 50.0) -- This will win
293+ pure 2
294+
295+ source3 = Strom.fromOm do
296+ Om.delay (Milliseconds 200.0)
297+ pure 3
298+
299+ -- TODO: race not yet implemented
300+ source1 # Strom.runCollect
301+
302+-- Expected output: [2] (the fastest source)
303+
304+--------------------------------------------------------------------------------
305+-- Example 12: Partition Stream
306+--------------------------------------------------------------------------------
307+
308+-- | Split stream into two based on predicate
309+example12 :: Om {} () { evens :: Array Int, odds :: Array Int }
310+example12 = do
311+ let stream = Strom.rangeStrom 1 11
312+ -- TODO: partition not yet implemented
313+ let evens = stream # Strom.filterStrom (\n -> n `mod` 2 == 0)
314+ let odds = stream # Strom.filterStrom (\n -> n `mod` 2 /= 0)
315+
316+ evensResult <- Strom.runCollect evens
317+ oddsResult <- Strom.runCollect odds
318+
319+ pure { evens: evensResult, odds: oddsResult }
320+
321+-- Expected output: { evens: [2, 4, 6, 8, 10], odds: [1, 3, 5, 7, 9] }
322+
323+--------------------------------------------------------------------------------
324+-- Example 13: Collect Pattern (filter + map in one)
325+--------------------------------------------------------------------------------
326+
327+-- | Extract and transform in one pass
328+parseNumbers :: String -> Maybe Int
329+parseNumbers "one" = Just 1
330+parseNumbers "two" = Just 2
331+parseNumbers "three" = Just 3
332+parseNumbers _ = Nothing
333+
334+example13 :: Om {} () (Array Int)
335+example13 = do
336+ Strom.fromArray ["one", "invalid", "two", "nope", "three"]
337+ # Strom.collectStrom parseNumbers
338+ # Strom.runCollect
339+
340+-- Expected output: [1, 2, 3]
341+
342+-- | Collect with Om effect
343+example13b :: Om {} () (Array Int)
344+example13b = do
345+ Strom.fromArray ["1", "not a number", "2", "3", "invalid"]
346+ # Strom.collectMStrom (\str -> do
347+ Om.delay (Milliseconds 10.0)
348+ pure $ case str of
349+ "1" -> Just 1
350+ "2" -> Just 2
351+ "3" -> Just 3
352+ _ -> Nothing
353+ )
354+ # Strom.runCollect
355+
356+-- Expected output: [1, 2, 3]
357+
358+--------------------------------------------------------------------------------
359+-- Example 14: Drop Operations
360+--------------------------------------------------------------------------------
361+
362+-- | Skip elements
363+example14 :: Om {} () (Array Int)
364+example14 = do
365+ Strom.rangeStrom 1 11
366+ # Strom.dropStrom 5
367+ # Strom.runCollect
368+
369+-- Expected output: [6, 7, 8, 9, 10]
370+
371+-- | Drop while condition holds
372+example14b :: Om {} () (Array Int)
373+example14b = do
374+ Strom.rangeStrom 1 11
375+ # Strom.dropWhileStrom (_ < 5)
376+ # Strom.runCollect
377+
378+-- Expected output: [5, 6, 7, 8, 9, 10]
379+
380+--------------------------------------------------------------------------------
381+-- Example 15: Subscription Pattern (Long-running streams)
382+--------------------------------------------------------------------------------
383+
384+-- | Subscribe to a stream and cancel later
385+example15 :: Om LoggerContext () Unit
386+example15 = do
387+ let eventStream =
388+ Strom.iterateStrom (_ + 1) 0
389+ # Strom.mapMStrom (\n -> do
390+ Om.delay (Milliseconds 500.0)
391+ pure n
392+ )
393+
394+ -- TODO: subscribe not yet implemented
395+ eventStream
396+ # Strom.takeStrom 5
397+ # Strom.traverseStrom_ (\n -> do
398+ { logger } <- Om.ask
399+ Om.fromAff $ logger $ "Received: " <> show n
400+ )
401+
402+-- Expected output:
403+-- Received: 0
404+-- Received: 1
405+-- Received: 2
406+-- Received: 3
407+-- Received: 4
408+-- Cancelling subscription...
409+
410+--------------------------------------------------------------------------------
411+-- Example 16: FlatMap for Nested Streams
412+--------------------------------------------------------------------------------
413+
414+-- | Flatten nested data structures
415+type Department = { name :: String, employees :: Array String }
416+
417+example16 :: Om {} () (Array String)
418+example16 = do
419+ let departments =
420+ [ { name: "Engineering", employees: ["Alice", "Bob"] }
421+ , { name: "Sales", employees: ["Charlie"] }
422+ , { name: "Marketing", employees: ["Dave", "Eve", "Frank"] }
423+ ]
424+
425+ Strom.fromArray departments
426+ >>= (\dept -> Strom.fromArray dept.employees)
427+ # Strom.runCollect
428+
429+-- Expected output: ["Alice", "Bob", "Charlie", "Dave", "Eve", "Frank"]
430+
431+--------------------------------------------------------------------------------
432+-- Example 17: Error Handling
433+--------------------------------------------------------------------------------
434+
435+data AppError = NetworkError | ValidationError | TimeoutError
436+
437+-- | Catch errors and provide fallback
438+example17 :: Om {} (networkError :: String) (Array Int)
439+example17 = do
440+ let riskyStream = Strom.rangeStrom 1 5
441+ # Strom.mapMStrom (\n -> do
442+ if n == 3
443+ then Om.throw { networkError: "Connection failed!" }
444+ else pure (n * 2)
445+ )
446+
447+ -- TODO: catchAll not yet implemented
448+ riskyStream # Strom.runCollect
449+
450+-- Expected output: [2, 4, 999, 8, 10]
451+
452+-- | Provide alternative stream on failure
453+example17b :: Om {} (networkError :: String) (Array Int)
454+example17b = do
455+ let primaryStream = Strom.fromOm $ Om.throw { networkError: "Primary failed" }
456+ let fallbackStream = Strom.fromArray [1, 2, 3]
457+
458+ -- TODO: orElse not yet implemented, using Alt instance
459+ (primaryStream <|> fallbackStream) # Strom.runCollect
460+
461+-- Expected output: [1, 2, 3]
462+
463+--------------------------------------------------------------------------------
464+-- Example 18: Real-world: Event Processing Pipeline
465+--------------------------------------------------------------------------------
466+
467+type Event = { timestamp :: Int, userId :: String, action :: String }
468+
469+-- | Complete event processing pipeline
470+eventProcessingPipeline :: Om LoggerContext () Unit
471+eventProcessingPipeline = do
472+ let events =
473+ [ { timestamp: 1000, userId: "u1", action: "login" }
474+ , { timestamp: 1001, userId: "u1", action: "view" }
475+ , { timestamp: 1002, userId: "u2", action: "login" }
476+ , { timestamp: 1003, userId: "u1", action: "view" }
477+ , { timestamp: 1004, userId: "u1", action: "view" }
478+ , { timestamp: 1005, userId: "u2", action: "purchase" }
479+ ]
480+
481+ Strom.fromArray events
482+ # Strom.filterStrom (\e -> e.action /= "login") -- Filter out logins
483+ # Strom.changesStrom -- Remove consecutive duplicates
484+ # Strom.mapAccumStrom -- Add sequence numbers
485+ (\seq event -> Tuple (seq + 1) { seq, event })
486+ 0
487+ # Strom.tapMStrom (\{ seq, event } -> do -- Log each event
488+ { logger } <- Om.ask
489+ Om.fromAff $ logger $
490+ "Event #" <> show seq <> ": "
491+ <> event.userId <> " - " <> event.action
492+ )
493+ # Strom.mapStrom (\x -> [x]) -- TODO: grouped not yet implemented
494+ # Strom.traverseStrom_ (\batch -> do -- Process batches
495+ { logger } <- Om.ask
496+ Om.fromAff $ logger $ "Processed batch of " <> show (Array.length batch)
497+ )
498+
499+-- Expected output:
500+-- Event #1: u1 - view
501+-- Event #2: u2 - purchase
502+-- Processed batch of 2
503+
504+--------------------------------------------------------------------------------
505+-- Helper to run examples
506+--------------------------------------------------------------------------------
507+
508+runExample :: forall a. Show a => Om {} () a -> Aff Unit
509+runExample om = do
510+ result <- Om.runOm {} { exception: \err -> do
511+ Console.log $ "Error: " <> show err
512+ pure unit
513+ } (om >>= \a -> Om.fromAff $ Console.log $ show a)
514+ pure result
515+
516+runExampleWithLogger :: forall a. Show a => Om LoggerContext () a -> Aff Unit
517+runExampleWithLogger om = do
518+ let ctx = { logger: \msg -> Console.log msg }
519+ result <- Om.runOm ctx { exception: \err -> do
520+ Console.log $ "Error: " <> show err
521+ pure unit
522+ } (om >>= \a -> Om.fromAff $ Console.log $ show a)
523+ pure result
_excluded/FileStream.pursnew file+37
@@ -0,0 +1,37 @@
1+module Yoga.Om.Strom.FileStream where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.String as String
7+import Effect (Effect)
8+import Effect.Class (liftEffect)
9+import Effect.Random (randomInt)
10+import Yoga.Om (Om)
11+import Yoga.Om.Strom as Strom
12+
13+foreign import _writeFile :: String -> String -> Effect Unit
14+foreign import _appendFile :: String -> String -> Effect Unit
15+foreign import _readFile :: String -> Effect String
16+foreign import _unlinkFile :: String -> Effect Unit
17+
18+-- | Infinite stream of random ints in [lo, hi]
19+randomInts :: forall ctx err. Int -> Int -> Strom.Strom ctx err Int
20+randomInts lo hi =
21+ Strom.repeatOmStromInfinite (liftEffect $ randomInt lo hi)
22+
23+-- | Stream n random integers to a file at the given path, one per line.
24+streamRandomToFile :: String -> Int -> Om {} () Unit
25+streamRandomToFile path n = do
26+ liftEffect $ _writeFile path ""
27+ randomInts 0 999999
28+ # Strom.takeStrom n
29+ # Strom.mapStrom (\i -> show i <> "\n")
30+ # Strom.traverseStrom_ (\line -> liftEffect $ _appendFile path line)
31+
32+-- | Read a file and count non-empty lines
33+countLines :: String -> Effect Int
34+countLines path = do
35+ content <- _readFile path
36+ let lines = Array.filter (_ /= "") $ String.split (String.Pattern "\n") content
37+ pure $ Array.length lines
_excluded/Strom.prop.pursnew file+429
@@ -0,0 +1,429 @@
1+module Yoga.Om.Strom.PropTest where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Data.Maybe (Maybe(..))
7+import Data.String (Pattern(..), split) as String
8+import Data.Traversable (for_)
9+import Data.Tuple (Tuple(..))
10+import Data.Time.Duration (Milliseconds(..))
11+import Effect.Aff (Aff, delay)
12+import Effect.Class (liftEffect)
13+import Effect.Exception (throwException)
14+import Effect.Ref as Ref
15+import Test.QuickCheck.Gen (Gen, chooseInt, randomSample', vectorOf)
16+import Test.Spec (Spec, describe, it)
17+import Test.Spec.Assertions (shouldEqual)
18+import Yoga.Om (Om)
19+import Yoga.Om as Om
20+import Yoga.Om.Strom as Strom
21+import Yoga.Om.Strom.FileStream as FileStream
22+
23+-- Helper to run Om in tests
24+runOm :: forall a. Om {} () a -> Aff a
25+runOm om = Om.runOm {} { exception: \err -> liftEffect (throwException err) } om
26+
27+-- | Small arrays of ints (0–50 elements)
28+genSmallArray :: Gen (Array Int)
29+genSmallArray = do
30+ len <- chooseInt 0 50
31+ vectorOf len (chooseInt (-1000) 1000)
32+
33+-- | Non-negative int for take/drop/grouped args
34+genSmallNat :: Gen Int
35+genSmallNat = chooseInt 0 100
36+
37+-- | Positive int (>0) for grouped size
38+genPositiveNat :: Gen Int
39+genPositiveNat = chooseInt 1 50
40+
41+-- | Multi-chunk arrays (1000–3000 elements) to force chunk boundaries
42+genMultiChunkArray :: Gen (Array Int)
43+genMultiChunkArray = do
44+ len <- chooseInt 1000 3000
45+ vectorOf len (chooseInt (-100) 100)
46+
47+-- | Even predicate (can't generate arbitrary predicates)
48+isEven :: Int -> Boolean
49+isEven n = n `mod` 2 == 0
50+
51+spec :: Spec Unit
52+spec = do
53+ describe "Strom Properties" do
54+
55+ describe "Roundtrip / Identity" do
56+
57+ it "fromArray >>> runCollect ≡ identity" do
58+ samples <- liftEffect $ randomSample' 50 genSmallArray
59+ for_ samples \arr -> do
60+ result <- runOm $ Strom.fromArray arr # Strom.runCollect
61+ result `shouldEqual` arr
62+
63+ it "mapStrom identity ≡ identity" do
64+ samples <- liftEffect $ randomSample' 50 genSmallArray
65+ for_ samples \arr -> do
66+ result <- runOm $ Strom.fromArray arr # Strom.mapStrom identity # Strom.runCollect
67+ result `shouldEqual` arr
68+
69+ describe "Consistency with Array equivalents" do
70+
71+ it "takeStrom n ≡ Array.take n" do
72+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genSmallNat <*> genSmallArray)
73+ for_ samples \(Tuple n arr) -> do
74+ result <- runOm $ Strom.fromArray arr # Strom.takeStrom n # Strom.runCollect
75+ result `shouldEqual` Array.take n arr
76+
77+ it "dropStrom n ≡ Array.drop n" do
78+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genSmallNat <*> genSmallArray)
79+ for_ samples \(Tuple n arr) -> do
80+ result <- runOm $ Strom.fromArray arr # Strom.dropStrom n # Strom.runCollect
81+ result `shouldEqual` Array.drop n arr
82+
83+ it "filterStrom isEven ≡ Array.filter isEven" do
84+ samples <- liftEffect $ randomSample' 50 genSmallArray
85+ for_ samples \arr -> do
86+ result <- runOm $ Strom.fromArray arr # Strom.filterStrom isEven # Strom.runCollect
87+ result `shouldEqual` Array.filter isEven arr
88+
89+ it "collectStrom f ≡ Array.mapMaybe f" do
90+ let f n = if isEven n then Just (n * 10) else Nothing
91+ samples <- liftEffect $ randomSample' 50 genSmallArray
92+ for_ samples \arr -> do
93+ result <- runOm $ Strom.fromArray arr # Strom.collectStrom f # Strom.runCollect
94+ result `shouldEqual` Array.mapMaybe f arr
95+
96+ it "scanStrom (+) 0 ≡ Array.scanl (+) 0" do
97+ samples <- liftEffect $ randomSample' 50 genSmallArray
98+ for_ samples \arr -> do
99+ result <- runOm $ Strom.fromArray arr # Strom.scanStrom (+) 0 # Strom.runCollect
100+ result `shouldEqual` Array.scanl (+) 0 arr
101+
102+ describe "Composition laws" do
103+
104+ it "take n (take m s) ≡ take (min n m) s" do
105+ samples <- liftEffect $ randomSample' 50 do
106+ n <- genSmallNat
107+ m <- genSmallNat
108+ arr <- genSmallArray
109+ pure { n, m, arr }
110+ for_ samples \{ n, m, arr } -> do
111+ result1 <- runOm $ Strom.fromArray arr # Strom.takeStrom m # Strom.takeStrom n # Strom.runCollect
112+ result2 <- runOm $ Strom.fromArray arr # Strom.takeStrom (min n m) # Strom.runCollect
113+ result1 `shouldEqual` result2
114+
115+ it "drop n (drop m s) ≡ drop (n + m) s" do
116+ samples <- liftEffect $ randomSample' 50 do
117+ n <- genSmallNat
118+ m <- genSmallNat
119+ arr <- genSmallArray
120+ pure { n, m, arr }
121+ for_ samples \{ n, m, arr } -> do
122+ result1 <- runOm $ Strom.fromArray arr # Strom.dropStrom m # Strom.dropStrom n # Strom.runCollect
123+ result2 <- runOm $ Strom.fromArray arr # Strom.dropStrom (n + m) # Strom.runCollect
124+ result1 `shouldEqual` result2
125+
126+ it "take n <> drop n ≡ identity (partition property)" do
127+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genSmallNat <*> genSmallArray)
128+ for_ samples \(Tuple n arr) -> do
129+ taken <- runOm $ Strom.fromArray arr # Strom.takeStrom n # Strom.runCollect
130+ dropped <- runOm $ Strom.fromArray arr # Strom.dropStrom n # Strom.runCollect
131+ (taken <> dropped) `shouldEqual` arr
132+
133+ describe "Monoid laws" do
134+
135+ it "empty <> s ≡ s (left identity)" do
136+ samples <- liftEffect $ randomSample' 50 genSmallArray
137+ for_ samples \arr -> do
138+ result <- runOm $ (Strom.empty <> Strom.fromArray arr) # Strom.runCollect
139+ result `shouldEqual` arr
140+
141+ it "s <> empty ≡ s (right identity)" do
142+ samples <- liftEffect $ randomSample' 50 genSmallArray
143+ for_ samples \arr -> do
144+ result <- runOm $ (Strom.fromArray arr <> Strom.empty) # Strom.runCollect
145+ result `shouldEqual` arr
146+
147+ it "(s1 <> s2) <> s3 ≡ s1 <> (s2 <> s3) (associativity)" do
148+ samples <- liftEffect $ randomSample' 30 do
149+ a <- genSmallArray
150+ b <- genSmallArray
151+ c <- genSmallArray
152+ pure { a, b, c }
153+ for_ samples \{ a, b, c } -> do
154+ let s1 = Strom.fromArray a
155+ let s2 = Strom.fromArray b
156+ let s3 = Strom.fromArray c
157+ result1 <- runOm $ ((s1 <> s2) <> s3) # Strom.runCollect
158+ result2 <- runOm $ (s1 <> (s2 <> s3)) # Strom.runCollect
159+ result1 `shouldEqual` result2
160+
161+ describe "Combining" do
162+
163+ it "zip length ≡ min of input lengths" do
164+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genSmallArray <*> genSmallArray)
165+ for_ samples \(Tuple a b) -> do
166+ result <- runOm $ Strom.zipStrom (Strom.fromArray a) (Strom.fromArray b) # Strom.runCollect
167+ Array.length result `shouldEqual` min (Array.length a) (Array.length b)
168+
169+ it "zip produces correct pairs" do
170+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genSmallArray <*> genSmallArray)
171+ for_ samples \(Tuple a b) -> do
172+ result <- runOm $ Strom.zipStrom (Strom.fromArray a) (Strom.fromArray b) # Strom.runCollect
173+ let expected = Array.zipWith Tuple a b
174+ result `shouldEqual` expected
175+
176+ it "intersperse element count = max 0 (2 * length - 1)" do
177+ samples <- liftEffect $ randomSample' 50 genSmallArray
178+ for_ samples \arr -> do
179+ result <- runOm $ Strom.fromArray arr # Strom.intersperse 0 # Strom.runCollect
180+ let expectedLen = max 0 (2 * Array.length arr - 1)
181+ Array.length result `shouldEqual` expectedLen
182+
183+ it "grouped n then flatten ≡ identity" do
184+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genPositiveNat <*> genSmallArray)
185+ for_ samples \(Tuple n arr) -> do
186+ result <- runOm $ Strom.fromArray arr # Strom.groupedStrom n # Strom.mapStrom identity # Strom.runCollect
187+ let flattened = Array.concat result
188+ flattened `shouldEqual` arr
189+
190+ it "fromArray (a <> b) ≡ fromArray a <> fromArray b (append homomorphism)" do
191+ samples <- liftEffect $ randomSample' 50 (Tuple <$> genSmallArray <*> genSmallArray)
192+ for_ samples \(Tuple a b) -> do
193+ result1 <- runOm $ Strom.fromArray (a <> b) # Strom.runCollect
194+ result2 <- runOm $ (Strom.fromArray a <> Strom.fromArray b) # Strom.runCollect
195+ result1 `shouldEqual` result2
196+
197+ describe "Multi-chunk stress tests" do
198+
199+ it "fromArray >>> runCollect roundtrip (multi-chunk)" do
200+ samples <- liftEffect $ randomSample' 5 genMultiChunkArray
201+ for_ samples \arr -> do
202+ result <- runOm $ Strom.fromArray arr # Strom.runCollect
203+ result `shouldEqual` arr
204+
205+ it "takeStrom n ≡ Array.take n (multi-chunk via rangeStrom)" do
206+ samples <- liftEffect $ randomSample' 20 (chooseInt 0 3000)
207+ for_ samples \n -> do
208+ let arr = Array.range 0 2999
209+ result <- runOm $ Strom.rangeStrom 0 3000 # Strom.takeStrom n # Strom.runCollect
210+ result `shouldEqual` Array.take n arr
211+
212+ it "dropStrom n ≡ Array.drop n (multi-chunk via rangeStrom)" do
213+ samples <- liftEffect $ randomSample' 20 (chooseInt 0 3000)
214+ for_ samples \n -> do
215+ let arr = Array.range 0 2999
216+ result <- runOm $ Strom.rangeStrom 0 3000 # Strom.dropStrom n # Strom.runCollect
217+ result `shouldEqual` Array.drop n arr
218+
219+ it "filterStrom isEven (multi-chunk via rangeStrom)" do
220+ result <- runOm $ Strom.rangeStrom 0 3000 # Strom.filterStrom isEven # Strom.runCollect
221+ let expected = Array.filter isEven (Array.range 0 2999)
222+ result `shouldEqual` expected
223+
224+ it "scanStrom (+) 0 (multi-chunk via rangeStrom)" do
225+ result <- runOm $ Strom.rangeStrom 1 2001 # Strom.scanStrom (+) 0 # Strom.runCollect
226+ let expected = Array.scanl (+) 0 (Array.range 1 2000)
227+ result `shouldEqual` expected
228+
229+ it "take n <> drop n ≡ identity (multi-chunk)" do
230+ samples <- liftEffect $ randomSample' 10 (chooseInt 0 3000)
231+ for_ samples \n -> do
232+ taken <- runOm $ Strom.rangeStrom 0 3000 # Strom.takeStrom n # Strom.runCollect
233+ dropped <- runOm $ Strom.rangeStrom 0 3000 # Strom.dropStrom n # Strom.runCollect
234+ (taken <> dropped) `shouldEqual` Array.range 0 2999
235+
236+ it "grouped n then flatten ≡ identity (multi-chunk)" do
237+ samples <- liftEffect $ randomSample' 10 genPositiveNat
238+ for_ samples \n -> do
239+ result <- runOm $ Strom.rangeStrom 0 2500 # Strom.groupedStrom n # Strom.runCollect
240+ let flattened = Array.concat result
241+ flattened `shouldEqual` Array.range 0 2499
242+
243+ it "zip with multi-chunk streams" do
244+ result <- runOm $
245+ Strom.zipWithStrom (+) (Strom.rangeStrom 0 2000) (Strom.rangeStrom 0 2000)
246+ # Strom.runCollect
247+ let expected = map (\i -> i + i) (Array.range 0 1999)
248+ result `shouldEqual` expected
249+
250+ it "fromArray (a <> b) ≡ fromArray a <> fromArray b (multi-chunk)" do
251+ samples <- liftEffect $ randomSample' 5 (Tuple <$> genMultiChunkArray <*> genMultiChunkArray)
252+ for_ samples \(Tuple a b) -> do
253+ result1 <- runOm $ Strom.fromArray (a <> b) # Strom.runCollect
254+ result2 <- runOm $ (Strom.fromArray a <> Strom.fromArray b) # Strom.runCollect
255+ result1 `shouldEqual` result2
256+
257+ it "intersperse element count (multi-chunk via rangeStrom)" do
258+ result <- runOm $ Strom.rangeStrom 0 2000 # Strom.intersperse 0 # Strom.runCollect
259+ Array.length result `shouldEqual` (2 * 2000 - 1)
260+
261+ it "mapStrom identity (multi-chunk via rangeStrom)" do
262+ result <- runOm $ Strom.rangeStrom 0 2500 # Strom.mapStrom identity # Strom.runCollect
263+ result `shouldEqual` Array.range 0 2499
264+
265+ it "monoid associativity (multi-chunk)" do
266+ let
267+ s1 = Strom.rangeStrom 0 1500
268+ s2 = Strom.rangeStrom 1500 3000
269+ s3 = Strom.rangeStrom 3000 4500
270+ result1 <- runOm $ ((s1 <> s2) <> s3) # Strom.runCollect
271+ result2 <- runOm $ (s1 <> (s2 <> s3)) # Strom.runCollect
272+ result1 `shouldEqual` result2
273+
274+ describe "Apply instance (monad law: ap ≡ <*>)" do
275+
276+ -- For a lawful Monad, `fs <*> xs` must equal `fs >>= \f -> xs >>= \x -> pure (f x)`
277+ -- The Apply instance has a bug in the (Loop, Loop) case: it only pairs
278+ -- corresponding chunks instead of doing the full cartesian product.
279+
280+ it "apply matches ap for single-chunk streams" do
281+ -- Single-chunk: both streams produce Done, so Apply works correctly
282+ let fs = Strom.fromArray [(_ + 10), (_ * 2)]
283+ let xs = Strom.fromArray [1, 2, 3]
284+ applyResult <- runOm $ (fs <*> xs) # Strom.runCollect
285+ let ap' mf ma = mf >>= \f -> ma >>= \a -> pure (f a)
286+ apResult <- runOm $ ap' (Strom.fromArray [(_ + 10), (_ * 2)]) (Strom.fromArray [1, 2, 3]) # Strom.runCollect
287+ applyResult `shouldEqual` apResult
288+
289+ it "apply matches ap for multi-chunk streams (BUG EXPECTED)" do
290+ -- Multi-chunk: both streams are built via <>, producing Loop steps.
291+ -- The Apply instance pairs corresponding chunks instead of doing cartesian product.
292+ -- fs = [+10] then [*2], xs = [1,2] then [3,4]
293+ -- Correct (ap): [11,12,13,14,2,4,6,8]
294+ -- Buggy (<*>): [11,12,6,8] (only pairs chunk1×chunk1 and chunk2×chunk2)
295+ let fs = Strom.fromArray [(_ + 10)] <> Strom.fromArray [(_ * 2)]
296+ let xs = Strom.fromArray [1, 2] <> Strom.fromArray [3, 4]
297+ applyResult <- runOm $ (fs <*> xs) # Strom.runCollect
298+ let ap' mf ma = mf >>= \f -> ma >>= \a -> pure (f a)
299+ apResult <- runOm $ ap' (Strom.fromArray [(_ + 10)] <> Strom.fromArray [(_ * 2)]) (Strom.fromArray [1, 2] <> Strom.fromArray [3, 4]) # Strom.runCollect
300+ applyResult `shouldEqual` apResult
301+
302+ it "apply matches ap with rangeStrom (multi-chunk, BUG EXPECTED)" do
303+ -- rangeStrom 1 3 = single chunk [1,2], rangeStrom 1 2001 = two 1000-element chunks
304+ -- Use small functions to keep output manageable
305+ let fs = Strom.fromArray [(_ + 100)] <> Strom.fromArray [(_ * (-1))]
306+ let xs = Strom.fromArray [1, 2, 3] <> Strom.fromArray [4, 5]
307+ applyResult <- runOm $ (fs <*> xs) # Strom.runCollect
308+ let ap' mf ma = mf >>= \f -> ma >>= \a -> pure (f a)
309+ apResult <- runOm $ ap' (Strom.fromArray [(_ + 100)] <> Strom.fromArray [(_ * (-1))]) (Strom.fromArray [1, 2, 3] <> Strom.fromArray [4, 5]) # Strom.runCollect
310+ applyResult `shouldEqual` apResult
311+
312+ describe "Bind across chunk boundaries" do
313+
314+ it "bind with multi-chunk source preserves all elements" do
315+ -- rangeStrom 1 2001 produces two 1000-element chunks
316+ -- Binding each to a small stream should produce all elements
317+ result <- runOm $
318+ Strom.rangeStrom 1 2001
319+ >>= (\n -> Strom.succeed (n * 2))
320+ # Strom.runCollect
321+ result `shouldEqual` map (_ * 2) (Array.range 1 2000)
322+
323+ it "bind with multi-chunk source and multi-element expansion" do
324+ -- Each element expands to [n, n*10], creating 2x elements
325+ result <- runOm $
326+ Strom.rangeStrom 1 51
327+ >>= (\n -> Strom.fromArray [n, n * 10])
328+ # Strom.runCollect
329+ let expected = Array.range 1 50 >>= (\n -> [n, n * 10])
330+ result `shouldEqual` expected
331+
332+ it "bind with multi-chunk source and variable-size expansion" do
333+ -- Some elements expand to 0, 1, or 2 elements (like filterMap + flatMap)
334+ let expand n
335+ | n `mod` 3 == 0 = Strom.empty
336+ | n `mod` 3 == 1 = Strom.succeed n
337+ | otherwise = Strom.fromArray [n, n * 100]
338+ result <- runOm $
339+ Strom.rangeStrom 1 2001
340+ >>= expand
341+ # Strom.runCollect
342+ let expected = Array.range 1 2000 >>= \n ->
343+ if n `mod` 3 == 0 then []
344+ else if n `mod` 3 == 1 then [n]
345+ else [n, n * 100]
346+ result `shouldEqual` expected
347+
348+ it "bind associativity: (m >>= f) >>= g ≡ m >>= (\\x -> f x >>= g)" do
349+ let f n = Strom.fromArray [n, n + 1]
350+ let g n = Strom.fromArray [n * 10]
351+ let m = Strom.fromArray [1, 2, 3] <> Strom.fromArray [4, 5]
352+ result1 <- runOm $ ((m >>= f) >>= g) # Strom.runCollect
353+ result2 <- runOm $ (m >>= (\x -> f x >>= g)) # Strom.runCollect
354+ result1 `shouldEqual` result2
355+
356+ describe "Cancel propagation" do
357+
358+ it "zip cancels left stream when right is shorter" do
359+ ref <- liftEffect $ Ref.new false
360+ let
361+ leftStrom = Strom.bracket
362+ (pure unit)
363+ (\_ -> liftEffect $ Ref.write true ref)
364+ (\_ -> Strom.rangeStrom 1 10000)
365+ rightStrom = Strom.fromArray [10, 20, 30]
366+ result <- runOm $ Strom.zipStrom leftStrom rightStrom # Strom.runCollect
367+ result `shouldEqual` [Tuple 1 10, Tuple 2 20, Tuple 3 30]
368+ -- Give async cancel a moment to propagate
369+ delay (Milliseconds 50.0)
370+ released <- liftEffect $ Ref.read ref
371+ released `shouldEqual` true
372+
373+ it "zip cancels right stream when left is shorter" do
374+ ref <- liftEffect $ Ref.new false
375+ let
376+ leftStrom = Strom.fromArray [10, 20]
377+ rightStrom = Strom.bracket
378+ (pure unit)
379+ (\_ -> liftEffect $ Ref.write true ref)
380+ (\_ -> Strom.rangeStrom 1 10000)
381+ result <- runOm $ Strom.zipStrom leftStrom rightStrom # Strom.runCollect
382+ result `shouldEqual` [Tuple 10 1, Tuple 20 2]
383+ delay (Milliseconds 50.0)
384+ released <- liftEffect $ Ref.read ref
385+ released `shouldEqual` true
386+
387+ it "takeStrom cancels multi-chunk bracket source" do
388+ ref <- liftEffect $ Ref.new false
389+ let
390+ strom = Strom.bracket
391+ (pure unit)
392+ (\_ -> liftEffect $ Ref.write true ref)
393+ (\_ -> Strom.rangeStrom 1 10000)
394+ result <- runOm $ Strom.takeStrom 5 strom # Strom.runCollect
395+ result `shouldEqual` [1, 2, 3, 4, 5]
396+ released <- liftEffect $ Ref.read ref
397+ released `shouldEqual` true
398+
399+ it "takeWhileStrom cancels multi-chunk bracket source" do
400+ ref <- liftEffect $ Ref.new false
401+ let
402+ strom = Strom.bracket
403+ (pure unit)
404+ (\_ -> liftEffect $ Ref.write true ref)
405+ (\_ -> Strom.rangeStrom 1 10000)
406+ result <- runOm $ Strom.takeWhileStrom (_ <= 3) strom # Strom.runCollect
407+ result `shouldEqual` [1, 2, 3]
408+ released <- liftEffect $ Ref.read ref
409+ released `shouldEqual` true
410+
411+ describe "Stream to file" do
412+
413+ it "streams 1000 random ints to a file" do
414+ let path = "/tmp/strom-test-random.txt"
415+ runOm $ FileStream.streamRandomToFile path 1000
416+ lineCount <- liftEffect $ FileStream.countLines path
417+ lineCount `shouldEqual` 1000
418+ liftEffect $ FileStream._unlinkFile path
419+
420+ it "streams 10000 random ints to a file (multi-chunk)" do
421+ let path = "/tmp/strom-test-random-large.txt"
422+ runOm $ FileStream.streamRandomToFile path 10000
423+ lineCount <- liftEffect $ FileStream.countLines path
424+ lineCount `shouldEqual` 10000
425+ -- Read back and verify all lines parse as ints
426+ content <- liftEffect $ FileStream._readFile path
427+ let lines = Array.filter (_ /= "") $ String.split (String.Pattern "\n") content
428+ Array.length lines `shouldEqual` 10000
429+ liftEffect $ FileStream._unlinkFile path
_excluded/Strom.test.pursnew file+669
@@ -0,0 +1,669 @@
1+module Yoga.Om.Strom.Test where
2+
3+import Prelude
4+
5+import Control.Alt ((<|>))
6+import Data.Array as Array
7+import Data.Maybe (Maybe(..))
8+import Data.Tuple (Tuple(..))
9+import Data.Time.Duration (Milliseconds(..))
10+import Effect.Aff (Aff)
11+import Effect.Ref as Ref
12+import Effect.Class (liftEffect)
13+import Effect.Exception (throwException, error)
14+import Test.Spec (Spec, describe, it)
15+import Test.Spec.Assertions (shouldEqual, fail)
16+import Yoga.Om (Om)
17+import Yoga.Om as Om
18+import Yoga.Om.Strom as Strom
19+
20+-- Helper to run Om in tests
21+runOm :: forall a. Om {} () a -> Aff a
22+runOm om = Om.runOm {} { exception: \err -> liftEffect (throwException err) } om
23+
24+spec :: Spec Unit
25+spec = do
26+ describe "Strom" do
27+
28+ describe "Construction" do
29+
30+ it "empty produces no elements" do
31+ result <- runOm $ Strom.empty # Strom.runCollect
32+ result `shouldEqual` ([] :: Array Int)
33+
34+ it "succeed produces single element" do
35+ result <- runOm $ Strom.succeed 42 # Strom.runCollect
36+ result `shouldEqual` [ 42 ]
37+
38+ it "fromArray produces all elements" do
39+ result <- runOm $ Strom.fromArray [ 1, 2, 3, 4, 5 ] # Strom.runCollect
40+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
41+
42+ it "fromArray handles empty array" do
43+ result <- runOm $ Strom.fromArray [] # Strom.runCollect
44+ result `shouldEqual` ([] :: Array Int)
45+
46+ it "range produces correct sequence" do
47+ result <- runOm $ Strom.rangeStrom 1 6 # Strom.runCollect
48+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
49+
50+ it "range with start >= end produces empty" do
51+ result <- runOm $ Strom.rangeStrom 5 5 # Strom.runCollect
52+ result `shouldEqual` []
53+
54+ it "iterate produces sequence" do
55+ result <- runOm $
56+ Strom.iterateStrom (_ + 1) 0
57+ # Strom.takeStrom 5
58+ # Strom.runCollect
59+ result `shouldEqual` [ 0, 1, 2, 3, 4 ]
60+
61+ it "iterateInfinite works for large streams (15k elements)" do
62+ result <- runOm $
63+ Strom.iterateStromInfinite (_ + 1) 0
64+ # Strom.takeStrom 15000
65+ # Strom.dropStrom 14995
66+ # Strom.runCollect
67+ result `shouldEqual` [ 14995, 14996, 14997, 14998, 14999 ]
68+
69+ it "repeat produces repeated values" do
70+ result <- runOm $
71+ Strom.repeatStrom 42
72+ # Strom.takeStrom 3
73+ # Strom.runCollect
74+ result `shouldEqual` [ 42, 42, 42 ]
75+
76+ it "repeatInfinite works for large streams (15k elements)" do
77+ result <- runOm $
78+ Strom.repeatStromInfinite 99
79+ # Strom.takeStrom 15000
80+ # Strom.dropStrom 14997
81+ # Strom.runCollect
82+ result `shouldEqual` [ 99, 99, 99 ]
83+
84+ it "unfold produces Fibonacci sequence" do
85+ result <- runOm $
86+ Strom.unfoldStrom
87+ (\(Tuple a b) -> if a > 100 then Nothing else Just (Tuple a (Tuple b (a + b))))
88+ (Tuple 0 1)
89+ # Strom.runCollect
90+ result `shouldEqual` [ 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ]
91+
92+ it "fromOm produces value from effect" do
93+ result <- runOm $
94+ Strom.fromOm (pure 42)
95+ # Strom.runCollect
96+ result `shouldEqual` [ 42 ]
97+
98+ describe "Running/Consuming" do
99+
100+ it "runCollect collects all elements" do
101+ result <- runOm $
102+ Strom.rangeStrom 1 6
103+ # Strom.runCollect
104+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
105+
106+ it "runDrain executes but returns unit" do
107+ result <- runOm $
108+ Strom.rangeStrom 1 100
109+ # Strom.runDrain
110+ result `shouldEqual` unit
111+
112+ it "runFold reduces stream" do
113+ result <- runOm $
114+ Strom.rangeStrom 1 6
115+ # Strom.runFold 0 (+)
116+ result `shouldEqual` 15
117+
118+ it "traverse_ executes effects" do
119+ countRef <- liftEffect $ Ref.new 0
120+ _ <- runOm $
121+ Strom.rangeStrom 1 6
122+ # Strom.traverseStrom_ (\_ -> liftEffect $ Ref.modify_ (_ + 1) countRef)
123+ count <- liftEffect $ Ref.read countRef
124+ count `shouldEqual` 5
125+
126+ describe "Transformations" do
127+
128+ it "map transforms elements" do
129+ result <- runOm $
130+ Strom.rangeStrom 1 6
131+ # Strom.mapStrom (_ * 2)
132+ # Strom.runCollect
133+ result `shouldEqual` [ 2, 4, 6, 8, 10 ]
134+
135+ it "mapM with effects" do
136+ result <- runOm $
137+ Strom.fromArray [ 1, 2, 3 ]
138+ # Strom.mapMStrom (\n -> pure (n * 2))
139+ # Strom.runCollect
140+ result `shouldEqual` [ 2, 4, 6 ]
141+
142+ -- TODO: mapParallel not yet implemented
143+ -- it "mapParallel processes concurrently" do
144+ -- result <- runOm $
145+ -- Strom.fromArray [1, 2, 3, 4, 5]
146+ -- # Strom.mapMStrom (\n -> do
147+ -- Om.delay (Milliseconds 10.0)
148+ -- pure (n * 2)
149+ -- )
150+ -- # Strom.runCollect
151+ -- Array.sort result `shouldEqual` [2, 4, 6, 8, 10]
152+
153+ it "bind (>>=) flattens nested streams" do
154+ result <- runOm $
155+ Strom.fromArray [ 1, 2, 3 ]
156+ >>= (\n -> Strom.fromArray [ n, n * 10 ])
157+ # Strom.runCollect
158+ result `shouldEqual` [ 1, 10, 2, 20, 3, 30 ]
159+
160+ it "bind (>>=) preserves order with multi-chunk source" do
161+ -- rangeStrom 1 2001 produces two 1000-element chunks, exercising the Loop branch
162+ result <- runOm $
163+ Strom.rangeStrom 1 2001
164+ >>= (\n -> Strom.succeed (n * 2))
165+ # Strom.takeStrom 2005
166+ # Strom.runCollect
167+ result `shouldEqual` (Array.range 1 2000 # map (_ * 2))
168+
169+ it "scan produces running totals" do
170+ result <- runOm $
171+ Strom.fromArray [ 1, 2, 3, 4, 5 ]
172+ # Strom.scanStrom (+) 0
173+ # Strom.runCollect
174+ result `shouldEqual` [ 1, 3, 6, 10, 15 ]
175+
176+ it "mapAccum with state" do
177+ result <- runOm $
178+ Strom.fromArray [ "a", "b", "c" ]
179+ # Strom.mapAccumStrom (\i x -> Tuple (i + 1) (show i <> x)) 1
180+ # Strom.runCollect
181+ result `shouldEqual` [ "1a", "2b", "3c" ]
182+
183+ it "tap observes without modifying" do
184+ sideEffectRef <- liftEffect $ Ref.new 0
185+ result <- runOm $
186+ Strom.fromArray [ 1, 2, 3 ]
187+ # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
188+ # Strom.runCollect
189+ result `shouldEqual` [ 1, 2, 3 ]
190+
191+ it "tapM with effects" do
192+ countRef <- liftEffect $ Ref.new 0
193+ result <- runOm $
194+ Strom.fromArray [ 1, 2, 3 ]
195+ # Strom.tapMStrom (\_ -> liftEffect $ Ref.modify_ (_ + 1) countRef)
196+ # Strom.runCollect
197+ count <- liftEffect $ Ref.read countRef
198+ count `shouldEqual` 3
199+ result `shouldEqual` [ 1, 2, 3 ]
200+
201+ describe "Selection" do
202+
203+ it "take limits elements" do
204+ result <- runOm $
205+ Strom.rangeStrom 1 100
206+ # Strom.takeStrom 5
207+ # Strom.runCollect
208+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
209+
210+ it "take with 0 produces empty" do
211+ result <- runOm $
212+ Strom.rangeStrom 1 100
213+ # Strom.takeStrom 0
214+ # Strom.runCollect
215+ result `shouldEqual` []
216+
217+ it "takeWhile stops at condition" do
218+ result <- runOm $
219+ Strom.rangeStrom 1 10
220+ # Strom.takeWhileStrom (_ < 5)
221+ # Strom.runCollect
222+ result `shouldEqual` [ 1, 2, 3, 4 ]
223+
224+ it "takeUntil includes matching element" do
225+ result <- runOm $
226+ Strom.rangeStrom 1 10
227+ # Strom.takeUntilStrom (_ == 5)
228+ # Strom.runCollect
229+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
230+
231+ it "drop skips elements" do
232+ result <- runOm $
233+ Strom.rangeStrom 1 10
234+ # Strom.dropStrom 5
235+ # Strom.runCollect
236+ result `shouldEqual` [ 6, 7, 8, 9 ]
237+
238+ it "dropWhile skips until condition" do
239+ result <- runOm $
240+ Strom.rangeStrom 1 10
241+ # Strom.dropWhileStrom (_ < 5)
242+ # Strom.runCollect
243+ result `shouldEqual` [ 5, 6, 7, 8, 9 ]
244+
245+ it "filter keeps matching elements" do
246+ result <- runOm $
247+ Strom.rangeStrom 1 10
248+ # Strom.filterStrom (\n -> n `mod` 2 == 0)
249+ # Strom.runCollect
250+ result `shouldEqual` [ 2, 4, 6, 8 ]
251+
252+ it "filterM with effects" do
253+ result <- runOm $
254+ Strom.rangeStrom 1 10
255+ # Strom.filterMStrom (\n -> pure (n `mod` 2 == 0))
256+ # Strom.runCollect
257+ result `shouldEqual` [ 2, 4, 6, 8 ]
258+
259+ it "collect filters and maps" do
260+ result <- runOm $
261+ Strom.fromArray [ "1", "x", "2", "y", "3" ]
262+ # Strom.collectStrom parseNumber
263+ # Strom.runCollect
264+ result `shouldEqual` [ 1, 2, 3 ]
265+
266+ it "changes removes consecutive duplicates" do
267+ result <- runOm $
268+ Strom.fromArray [ 1, 1, 2, 2, 2, 3, 3, 1, 1 ]
269+ # Strom.changesStrom
270+ # Strom.runCollect
271+ result `shouldEqual` [ 1, 2, 3, 1 ]
272+
273+ describe "Combining" do
274+
275+ it "append concatenates streams" do
276+ result <- runOm $
277+ Strom.fromArray [ 1, 2, 3 ]
278+ # Strom.appendStrom (Strom.fromArray [ 4, 5, 6 ])
279+ # Strom.runCollect
280+ result `shouldEqual` [ 1, 2, 3, 4, 5, 6 ]
281+
282+ it "concat multiple streams" do
283+ result <- runOm $
284+ Strom.concatStrom
285+ [ Strom.fromArray [ 1, 2 ]
286+ , Strom.fromArray [ 3, 4 ]
287+ , Strom.fromArray [ 5, 6 ]
288+ ]
289+ # Strom.runCollect
290+ result `shouldEqual` [ 1, 2, 3, 4, 5, 6 ]
291+
292+ it "zip pairs elements" do
293+ result <- runOm $
294+ Strom.zipStrom
295+ (Strom.rangeStrom 1 4)
296+ (Strom.fromArray [ "a", "b", "c" ])
297+ # Strom.runCollect
298+ result `shouldEqual` [ Tuple 1 "a", Tuple 2 "b", Tuple 3 "c" ]
299+
300+ it "zip stops at shortest stream" do
301+ result <- runOm $
302+ Strom.zipStrom
303+ (Strom.rangeStrom 1 10)
304+ (Strom.fromArray [ "a", "b" ])
305+ # Strom.runCollect
306+ result `shouldEqual` [ Tuple 1 "a", Tuple 2 "b" ]
307+
308+ it "zipWith with custom function" do
309+ result <- runOm $
310+ Strom.zipWithStrom (+)
311+ (Strom.rangeStrom 1 4)
312+ (Strom.rangeStrom 10 13)
313+ # Strom.runCollect
314+ result `shouldEqual` [ 11, 13, 15 ]
315+
316+ it "zip handles mismatched chunk sizes (multi-chunk vs single-chunk)" do
317+ -- Regression: previously dropped elements when one stream had more chunks
318+ result <- runOm $
319+ Strom.zipStrom
320+ (Strom.rangeStrom 1 2001) -- two 1000-element chunks
321+ (Strom.fromArray [ 10, 20, 30 ]) -- single chunk
322+ # Strom.runCollect
323+ result `shouldEqual` [ Tuple 1 10, Tuple 2 20, Tuple 3 30 ]
324+
325+ it "interleave alternates chunks from two streams" do
326+ result <- runOm $
327+ Strom.interleave
328+ (Strom.fromArray [ 1, 3, 5 ])
329+ (Strom.fromArray [ 2, 4, 6 ])
330+ # Strom.runCollect
331+ -- fromArray produces single chunks, so interleave emits first chunk then second
332+ result `shouldEqual` [ 1, 3, 5, 2, 4, 6 ]
333+
334+ it "interleave terminates with finite streams" do
335+ -- Regression: previously infinite looped because Done(Just) streams were re-pulled
336+ result <- runOm $
337+ Strom.interleave
338+ (Strom.succeed 1)
339+ (Strom.succeed 2)
340+ # Strom.runCollect
341+ result `shouldEqual` [ 1, 2 ]
342+
343+ it "race takes first to emit" do
344+ result <- runOm $
345+ -- TODO: race not yet implemented
346+ Strom.fromOm (pure 2)
347+ # Strom.runCollect
348+ -- Result should be from the fastest stream
349+ result `shouldEqual` [ 2 ]
350+
351+ describe "Non-Deterministic Merge" do
352+
353+ it "mergeND merges two streams concurrently" do
354+ result <- runOm $
355+ Strom.mergeND
356+ (Strom.fromArray [ 1, 2, 3 ])
357+ (Strom.fromArray [ 10, 20, 30 ])
358+ # Strom.runCollect
359+ -- All elements should be present (order may vary)
360+ Array.sort result `shouldEqual` [ 1, 2, 3, 10, 20, 30 ]
361+
362+ it "mergeND handles fast and slow streams" do
363+ result <- runOm $
364+ Strom.mergeND
365+ ( Strom.fromArray [ 1, 2, 3 ]
366+ # Strom.mapMStrom (\n -> Om.delay (Milliseconds 50.0) *> pure n)
367+ )
368+ ( Strom.fromArray [ 10, 20, 30 ]
369+ # Strom.mapMStrom (\n -> Om.delay (Milliseconds 5.0) *> pure n)
370+ )
371+ # Strom.runCollect
372+ -- All elements should be present
373+ Array.sort result `shouldEqual` [ 1, 2, 3, 10, 20, 30 ]
374+ -- Fast stream elements (10, 20, 30) should appear earlier
375+ -- (This is probabilistic but highly likely with 10x speed difference)
376+ let fastElements = Array.filter (\n -> n >= 10) result
377+ Array.length fastElements `shouldEqual` 3
378+
379+ it "mergeND handles empty streams" do
380+ result <- runOm $
381+ Strom.mergeND
382+ (Strom.empty :: Strom.Strom {} () Int)
383+ (Strom.fromArray [ 1, 2, 3 ])
384+ # Strom.runCollect
385+ result `shouldEqual` [ 1, 2, 3 ]
386+
387+ it "mergeND handles both streams empty" do
388+ result <- runOm $
389+ Strom.mergeND
390+ (Strom.empty :: Strom.Strom {} () Int)
391+ (Strom.empty :: Strom.Strom {} () Int)
392+ # Strom.runCollect
393+ result `shouldEqual` []
394+
395+ it "mergeND handles one stream finishing first" do
396+ result <- runOm $
397+ Strom.mergeND
398+ (Strom.fromArray [ 1, 2 ])
399+ (Strom.fromArray [ 10, 20, 30, 40 ])
400+ # Strom.runCollect
401+ Array.sort result `shouldEqual` [ 1, 2, 10, 20, 30, 40 ]
402+
403+ it "mergeND with takeStrom limits output" do
404+ result <- runOm $
405+ Strom.mergeND
406+ (Strom.repeatStromInfinite 1)
407+ (Strom.repeatStromInfinite 2)
408+ # Strom.takeStrom 10
409+ # Strom.runCollect
410+ -- Should limit to 10 elements
411+ Array.length result `shouldEqual` 10
412+ -- Should contain only 1s or 2s (or a mix)
413+ -- Note: When both streams are equally fast, Om.race may consistently pick the same one
414+ let allValid = Array.all (\n -> n == 1 || n == 2) result
415+ allValid `shouldEqual` true
416+
417+ it "mergeAllND merges multiple streams" do
418+ result <- runOm $
419+ Strom.mergeAllND
420+ [ Strom.fromArray [ 1, 2 ]
421+ , Strom.fromArray [ 10, 20 ]
422+ , Strom.fromArray [ 100, 200 ]
423+ ]
424+ # Strom.runCollect
425+ Array.sort result `shouldEqual` [ 1, 2, 10, 20, 100, 200 ]
426+
427+ it "mergeAllND handles empty array" do
428+ result <- runOm $
429+ Strom.mergeAllND ([] :: Array (Strom.Strom {} () Int))
430+ # Strom.runCollect
431+ result `shouldEqual` []
432+
433+ describe "Grouping" do
434+
435+ it "grouped creates fixed-size chunks" do
436+ result <- runOm $
437+ Strom.rangeStrom 1 11
438+ # Strom.groupedStrom 3
439+ # Strom.runCollect
440+ result `shouldEqual` [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7, 8, 9 ], [ 10 ] ]
441+
442+ it "grouped with partial last chunk" do
443+ result <- runOm $
444+ Strom.rangeStrom 1 8
445+ # Strom.groupedStrom 3
446+ # Strom.runCollect
447+ result `shouldEqual` [ [ 1, 2, 3 ], [ 4, 5, 6 ], [ 7 ] ]
448+
449+ it "grouped preserves all elements with multi-chunk source" do
450+ -- rangeStrom 1 2001 produces two 1000-element chunks.
451+ -- groupedStrom 500 means chunk boundaries align with group size
452+ -- (1000 is a multiple of 500), which previously dropped complete last groups.
453+ result <- runOm $
454+ Strom.rangeStrom 1 2001
455+ # Strom.groupedStrom 500
456+ # Strom.mapStrom Array.length
457+ # Strom.runCollect
458+ result `shouldEqual` [ 500, 500, 500, 500 ]
459+
460+ it "chunked is alias for grouped" do
461+ result <- runOm $
462+ Strom.rangeStrom 1 6
463+ # Strom.chunkedStrom 2
464+ # Strom.runCollect
465+ result `shouldEqual` [ [ 1, 2 ], [ 3, 4 ], [ 5 ] ]
466+
467+ it "partition splits by predicate" do
468+ -- TODO: partition not yet implemented
469+ let stream = Strom.rangeStrom 1 11
470+ evensResult <- runOm $ Strom.runCollect $ stream # Strom.filterStrom (\n -> n `mod` 2 == 0)
471+ oddsResult <- runOm $ Strom.runCollect $ stream # Strom.filterStrom (\n -> n `mod` 2 /= 0)
472+ evensResult `shouldEqual` [ 2, 4, 6, 8, 10 ]
473+ oddsResult `shouldEqual` [ 1, 3, 5, 7, 9 ]
474+
475+ describe "Timing Operations" do
476+
477+ it "debounce adds delay between elements" do
478+ result <- runOm $
479+ Strom.fromArray [ 1, 2, 3 ]
480+ # Strom.debounce (Milliseconds 10.0)
481+ # Strom.runCollect
482+ -- Verify all elements pass through
483+ result `shouldEqual` [ 1, 2, 3 ]
484+
485+ it "throttle limits emission rate" do
486+ result <- runOm $
487+ Strom.fromArray [ 1, 2, 3, 4, 5 ]
488+ # Strom.throttle (Milliseconds 5.0)
489+ # Strom.runCollect
490+ -- First element emits immediately, rest are throttled
491+ -- Due to fast sequential processing, likely only first element passes
492+ Array.length result `shouldEqual` 1
493+ Array.head result `shouldEqual` Just 1
494+
495+ describe "Error Handling" do
496+
497+ it "catchAll recovers from errors" do
498+ result <- runOm $
499+ Strom.fromArray [ 1, 2, 3 ]
500+ # Strom.mapMStrom
501+ ( \n ->
502+ if n == 2 then pure 999 -- TODO: catchAll not implemented
503+ else pure (n * 2)
504+ )
505+ # Strom.runCollect
506+ -- Should have gotten an error and recovered
507+ result `shouldEqual` [ 2, 999, 6 ]
508+
509+ it "orElse provides alternative" do
510+ let
511+ failingStream = Strom.empty
512+ successStream = Strom.fromArray [ 1, 2, 3 ]
513+ result <- runOm $
514+ (failingStream <|> successStream)
515+ # Strom.runCollect
516+ result `shouldEqual` [ 1, 2, 3 ]
517+
518+ describe "Complex Scenarios" do
519+
520+ it "pipeline: filter, map, group" do
521+ result <- runOm $
522+ Strom.rangeStrom 1 21
523+ # Strom.filterStrom (\n -> n `mod` 2 == 0)
524+ # Strom.mapStrom (_ * 2)
525+ # Strom.groupedStrom 3
526+ # Strom.runCollect
527+ result `shouldEqual` [ [ 4, 8, 12 ], [ 16, 20, 24 ], [ 28, 32, 36 ], [ 40 ] ]
528+
529+ it "nested bind (>>=) with filtering" do
530+ result <- runOm $
531+ Strom.fromArray [ 1, 2, 3 ]
532+ >>=
533+ ( \n ->
534+ Strom.rangeStrom (n * 10) (n * 10 + 3)
535+ # Strom.filterStrom (\x -> x `mod` 2 == 0)
536+ )
537+ # Strom.runCollect
538+ result `shouldEqual` [ 10, 12, 20, 22, 30, 32 ]
539+
540+ it "scan with filtering and mapping" do
541+ result <- runOm $
542+ Strom.fromArray [ 1, -2, 3, -4, 5 ]
543+ # Strom.filterStrom (_ > 0)
544+ # Strom.scanStrom (+) 0
545+ # Strom.mapStrom (_ * 10)
546+ # Strom.runCollect
547+ result `shouldEqual` [ 10, 40, 90 ]
548+
549+ it "zip with scan" do
550+ result <- runOm $
551+ Strom.zipStrom
552+ (Strom.fromArray [ "a", "b", "c", "d" ])
553+ (Strom.iterateStrom (_ + 1) 1 # Strom.scanStrom (+) 0 # Strom.takeStrom 4)
554+ # Strom.runCollect
555+ result `shouldEqual` [ Tuple "a" 1, Tuple "b" 3, Tuple "c" 6, Tuple "d" 10 ]
556+
557+ it "large stream with take" do
558+ result <- runOm $
559+ Strom.iterateStrom (_ + 1) 0
560+ # Strom.filterStrom (\n -> n `mod` 3 == 0)
561+ # Strom.takeStrom 100
562+ # Strom.groupedStrom 10
563+ # Strom.runCollect
564+ Array.length result `shouldEqual` 10
565+ (Array.head result >>= Array.head) `shouldEqual` Just 0
566+
567+ describe "Edge Cases" do
568+
569+ it "empty stream through pipeline" do
570+ result <- runOm $
571+ Strom.empty
572+ # Strom.mapStrom (_ * 2)
573+ # Strom.filterStrom (_ > 0)
574+ # Strom.groupedStrom 10
575+ # Strom.runCollect
576+ result `shouldEqual` ([] :: Array (Array Int))
577+
578+ it "single element through complex pipeline" do
579+ result <- runOm $
580+ Strom.succeed 42
581+ # Strom.mapStrom (_ * 2)
582+ # Strom.filterStrom (_ > 50)
583+ # Strom.takeStrom 10
584+ # Strom.runCollect
585+ result `shouldEqual` [ 84 ]
586+
587+ it "take more than available" do
588+ result <- runOm $
589+ Strom.fromArray [ 1, 2, 3 ]
590+ # Strom.takeStrom 100
591+ # Strom.runCollect
592+ result `shouldEqual` [ 1, 2, 3 ]
593+
594+ it "drop more than available" do
595+ result <- runOm $
596+ Strom.fromArray [ 1, 2, 3 ]
597+ # Strom.dropStrom 100
598+ # Strom.runCollect
599+ result `shouldEqual` []
600+
601+ it "grouped with size larger than stream" do
602+ result <- runOm $
603+ Strom.fromArray [ 1, 2, 3 ]
604+ # Strom.groupedStrom 100
605+ # Strom.runCollect
606+ result `shouldEqual` [ [ 1, 2, 3 ] ]
607+
608+ describe "Cancellation" do
609+
610+ it "cancelStrom on simple stream is no-op" do
611+ liftEffect $ Strom.cancelStrom (Strom.fromArray [ 1, 2, 3 ] :: Strom.Strom {} () Int)
612+
613+ it "takeStrom + bracket releases resource" do
614+ ref <- liftEffect $ Ref.new false
615+ let
616+ strom = Strom.bracket
617+ (pure unit)
618+ (\_ -> liftEffect $ Ref.write true ref)
619+ (\_ -> Strom.rangeStrom 1 1000)
620+ result <- runOm $ Strom.runCollect (Strom.takeStrom 5 strom)
621+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
622+ released <- liftEffect $ Ref.read ref
623+ released `shouldEqual` true
624+
625+ it "takeWhileStrom + bracket releases resource" do
626+ ref <- liftEffect $ Ref.new false
627+ let
628+ strom = Strom.bracket
629+ (pure unit)
630+ (\_ -> liftEffect $ Ref.write true ref)
631+ (\_ -> Strom.rangeStrom 1 1000)
632+ result <- runOm $ Strom.runCollect (Strom.takeWhileStrom (_ < 4) strom)
633+ result `shouldEqual` [ 1, 2, 3 ]
634+ released <- liftEffect $ Ref.read ref
635+ released `shouldEqual` true
636+
637+ it "bracket releases resource on natural multi-chunk completion" do
638+ ref <- liftEffect $ Ref.new false
639+ let
640+ -- rangeStrom 1 2001 produces two chunks (>1000 elements), so bracket sees Loop first
641+ strom = Strom.bracket
642+ (pure unit)
643+ (\_ -> liftEffect $ Ref.write true ref)
644+ (\_ -> Strom.rangeStrom 1 2001)
645+ result <- runOm $ Strom.runCollect strom
646+ Array.length result `shouldEqual` 2000
647+ released <- liftEffect $ Ref.read ref
648+ released `shouldEqual` true
649+
650+ it "takeUntilStrom + bracket releases resource" do
651+ ref <- liftEffect $ Ref.new false
652+ let
653+ strom = Strom.bracket
654+ (pure unit)
655+ (\_ -> liftEffect $ Ref.write true ref)
656+ (\_ -> Strom.rangeStrom 1 1000)
657+ result <- runOm $ Strom.runCollect (Strom.takeUntilStrom (_ == 5) strom)
658+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
659+ released <- liftEffect $ Ref.read ref
660+ released `shouldEqual` true
661+
662+-- Helper functions
663+parseNumber :: String -> Maybe Int
664+parseNumber "1" = Just 1
665+parseNumber "2" = Just 2
666+parseNumber "3" = Just 3
667+parseNumber "4" = Just 4
668+parseNumber "5" = Just 5
669+parseNumber _ = Nothing
src/Test/Main.purs → _excluded/Test/Main.pursrenamed+4
@@ -6,8 +6,12 @@ import Effect (Effect)
6import Effect.Aff (launchAff_)
6import Effect.Aff (launchAff_)
7import Test.Spec.Reporter (consoleReporter)
7import Test.Spec.Reporter (consoleReporter)
8import Test.Spec.Runner (runSpec)
8import Test.Spec.Runner (runSpec)
9+import Test.WebStream as WebStreamSpec
9import Yoga.Om.Strom.Test as StromSpec
10import Yoga.Om.Strom.Test as StromSpec
11+import Yoga.Om.Strom.PropTest as StromPropSpec
10
12
11main :: Effect Unit
13main :: Effect Unit
12main = launchAff_ $ runSpec [consoleReporter] do
14main = launchAff_ $ runSpec [consoleReporter] do
13 StromSpec.spec
15 StromSpec.spec
16+ WebStreamSpec.spec
17+ StromPropSpec.spec
bun.lock+3−69
@@ -5,7 +5,7 @@
5 "name": "purescript-yoga-om-strom",
5 "name": "purescript-yoga-om-strom",
6 "devDependencies": {
6 "devDependencies": {
7 "purescript": "^0.15.15",
7 "purescript": "^0.15.15",
8− "spago": "next",
8+ "spago": "^1.0.3",
9 },
9 },
10 },
10 },
11 },
11 },
@@ -54,24 +54,14 @@
54
54
55 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
55 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
56
56
57− "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
58−
59 "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
57 "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
60
58
61− "better-sqlite3": ["better-sqlite3@12.6.2", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-8VYKM3MjCa9WcaSAI3hzwhmyHVlH8tiGFwf0RlTsZPWJ1I5MkzjiudCo4KC4DxOaL/53A5B1sI/IbldNFDbsKA=="],
62−
63− "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
64−
65− "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
66−
67 "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
59 "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
68
60
69 "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
61 "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
70
62
71 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
63 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
72
64
73− "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
74−
75 "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
65 "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
76
66
77 "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
67 "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
@@ -110,18 +100,12 @@
110
100
111 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
101 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
112
102
113− "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
114−
115− "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
116−
117 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
103 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
118
104
119 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
105 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
120
106
121 "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
107 "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
122
108
123− "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
124−
125 "duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
109 "duplexify": ["duplexify@3.7.1", "", { "dependencies": { "end-of-stream": "^1.0.0", "inherits": "^2.0.1", "readable-stream": "^2.0.0", "stream-shift": "^1.0.0" } }, "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g=="],
126
110
127 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
111 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -142,14 +126,10 @@
142
126
143 "execa": ["execa@2.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^3.0.0", "onetime": "^5.1.0", "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw=="],
127 "execa": ["execa@2.1.0", "", { "dependencies": { "cross-spawn": "^7.0.0", "get-stream": "^5.0.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^3.0.0", "onetime": "^5.1.0", "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw=="],
144
128
145− "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
146−
147 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
129 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
148
130
149 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
131 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
150
132
151− "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
152−
153 "filesize": ["filesize@4.2.1", "", {}, "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA=="],
133 "filesize": ["filesize@4.2.1", "", {}, "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA=="],
154
134
155 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
135 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -160,8 +140,6 @@
160
140
161 "from2": ["from2@2.3.0", "", { "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g=="],
141 "from2": ["from2@2.3.0", "", { "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g=="],
162
142
163− "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
164−
165 "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
143 "fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="],
166
144
167 "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
145 "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
@@ -174,8 +152,6 @@
174
152
175 "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
153 "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
176
154
177− "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
178−
179 "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
155 "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="],
180
156
181 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
157 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -192,8 +168,6 @@
192
168
193 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
169 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
194
170
195− "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
196−
197 "iferr": ["iferr@0.1.5", "", {}, "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA=="],
171 "iferr": ["iferr@0.1.5", "", {}, "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA=="],
198
172
199 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
173 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -206,8 +180,6 @@
206
180
207 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
181 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
208
182
209− "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
210−
211 "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
183 "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
212
184
213 "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
185 "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
@@ -254,8 +226,6 @@
254
226
255 "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
227 "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
256
228
257− "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
258−
259 "minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
229 "minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
260
230
261 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
231 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -278,20 +248,14 @@
278
248
279 "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
249 "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
280
250
281− "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
282−
283 "move-concurrently": ["move-concurrently@1.0.1", "", { "dependencies": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", "fs-write-stream-atomic": "^1.0.8", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" } }, "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ=="],
251 "move-concurrently": ["move-concurrently@1.0.1", "", { "dependencies": { "aproba": "^1.1.1", "copy-concurrently": "^1.0.0", "fs-write-stream-atomic": "^1.0.8", "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" } }, "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ=="],
284
252
285 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
253 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
286
254
287 "nan": ["nan@2.25.0", "", {}, "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g=="],
255 "nan": ["nan@2.25.0", "", {}, "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g=="],
288
256
289− "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
290−
291 "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
257 "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
292
258
293− "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
294−
295 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
259 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
296
260
297 "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
261 "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -316,8 +280,6 @@
316
280
317 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
281 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
318
282
319− "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
320−
321 "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
283 "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
322
284
323 "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
285 "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
@@ -338,8 +300,6 @@
338
300
339 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
301 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
340
302
341− "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
342−
343 "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
303 "readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
344
304
345 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
305 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
@@ -358,7 +318,7 @@
358
318
359 "run-queue": ["run-queue@1.0.3", "", { "dependencies": { "aproba": "^1.1.1" } }, "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg=="],
319 "run-queue": ["run-queue@1.0.3", "", { "dependencies": { "aproba": "^1.1.1" } }, "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg=="],
360
320
361− "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
321+ "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
362
322
363 "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
323 "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
364
324
@@ -370,10 +330,6 @@
370
330
371 "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
331 "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
372
332
373− "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
374−
375− "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
376−
377 "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
333 "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="],
378
334
379 "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
335 "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
@@ -382,7 +338,7 @@
382
338
383 "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="],
339 "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="],
384
340
385− "spago": ["spago@0.93.45", "", { "dependencies": { "@nodelib/fs.walk": "^3.0.1", "better-sqlite3": "^12.5.0", "env-paths": "^3.0.0", "fs-extra": "^11.3.0", "fuse.js": "^7.1.0", "glob": "^11.0.1", "markdown-it": "^14.1.0", "micromatch": "^4.0.8", "open": "^10.1.0", "picomatch": "^4.0.2", "punycode": "^2.3.1", "readline-sync": "^1.4.10", "semver": "^7.7.1", "spdx-expression-parse": "^4.0.0", "ssh2": "^1.16.0", "supports-color": "^10.0.0", "tar": "^7.4.3", "tmp": "^0.2.3", "xhr2": "^0.2.1", "yaml": "^2.7.0" }, "bin": { "spago": "bin/bundle.js" } }, "sha512-t9h4etkmwZVlZ0eQeLQbNbcJlNSjZC9+xTmjcUykQ6+Hq5g4tMH5GKEoObtDiU+me+tujFzQA/W+DyCa2L98wQ=="],
341+ "spago": ["spago@1.0.3", "", { "dependencies": { "@nodelib/fs.walk": "^3.0.1", "env-paths": "^3.0.0", "fs-extra": "^11.3.0", "fuse.js": "^7.1.0", "glob": "^11.0.1", "markdown-it": "^14.1.0", "micromatch": "^4.0.8", "open": "^10.1.0", "picomatch": "^4.0.2", "punycode": "^2.3.1", "readline-sync": "^1.4.10", "semver": "^7.7.1", "spdx-expression-parse": "^4.0.0", "ssh2": "^1.16.0", "supports-color": "^10.0.0", "tar": "^7.4.3", "tmp": "^0.2.3", "xhr2": "^0.2.1", "yaml": "^2.7.0" }, "bin": { "spago": "bin/bundle.js" } }, "sha512-3JR6ExwLwaKIMYdc77e6fKqiBKXNentyVkcfhaQx4tGDnWjfHS2Td+zxJZ/28QaBx8s6RXfgFD1VjbALGtM89g=="],
386
342
387 "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
343 "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
388
344
@@ -410,24 +366,16 @@
410
366
411 "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
367 "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
412
368
413− "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
414−
415 "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
369 "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
416
370
417 "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="],
371 "tar": ["tar@7.5.7", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-fov56fJiRuThVFXD6o6/Q354S7pnWMJIVlDBYijsTNx6jKSE4pvrDTs6lUnmGvNyfJwFQQwWy3owKz1ucIhveQ=="],
418
372
419− "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
420−
421− "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
422−
423 "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="],
373 "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="],
424
374
425 "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
375 "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
426
376
427 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
377 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
428
378
429− "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
430−
431 "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
379 "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
432
380
433 "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
381 "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
@@ -476,8 +424,6 @@
476
424
477 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
425 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
478
426
479− "bl/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
480−
481 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
427 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
482
428
483 "cacache/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
429 "cacache/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
@@ -522,18 +468,10 @@
522
468
523 "purescript-installer/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
469 "purescript-installer/tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
524
470
525− "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
526−
527 "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
471 "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
528
472
529 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
473 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
530
474
531− "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
532−
533− "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
534−
535− "tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
536−
537 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
475 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
538
476
539 "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
477 "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -546,8 +484,6 @@
546
484
547 "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
485 "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
548
486
549− "bl/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
550−
551 "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
487 "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
552
488
553 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
489 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@@ -592,8 +528,6 @@
592
528
593 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
529 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
594
530
595− "tar-stream/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
596−
597 "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
531 "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
598
532
599 "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
533 "wrap-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
package.json+1−1
@@ -9,6 +9,6 @@
9 },
9 },
10 "devDependencies": {
10 "devDependencies": {
11 "purescript": "^0.15.15",
11 "purescript": "^0.15.15",
12− "spago": "next"
12+ "spago": "^1.0.3"
13 }
13 }
14}
14}
spago.lock+409−172
@@ -6,15 +6,17 @@
6 "core": {
6 "core": {
7 "dependencies": [
7 "dependencies": [
8 {
8 {
9− "aff": ">=7.1.0 <8.0.0"
9+ "aff": ">=8.0.0 <9.0.0"
10 },
10 },
11 {
11 {
12 "arrays": ">=7.3.0 <8.0.0"
12 "arrays": ">=7.3.0 <8.0.0"
13 },
13 },
14 {
14 {
15− "avar": ">=5.0.0 <6.0.0"
15+ "avar": ">=5.0.1 <6.0.0"
16+ },
17+ {
18+ "catenable-lists": ">=7.0.0 <8.0.0"
16 },
19 },
17− "catenable-lists",
18 {
20 {
19 "console": ">=6.1.0 <7.0.0"
21 "console": ">=6.1.0 <7.0.0"
20 },
22 },
@@ -31,12 +33,14 @@
31 "either": ">=6.1.0 <7.0.0"
33 "either": ">=6.1.0 <7.0.0"
32 },
34 },
33 {
35 {
34− "exceptions": ">=6.0.0 <7.0.0"
36+ "exceptions": ">=6.1.0 <7.0.0"
35 },
37 },
36 {
38 {
37 "foldable-traversable": ">=6.0.0 <7.0.0"
39 "foldable-traversable": ">=6.0.0 <7.0.0"
38 },
40 },
41+ {
39− "integers",
42+ "integers": ">=6.0.0 <7.0.0"
43+ },
40 {
44 {
41 "lists": ">=7.0.0 <8.0.0"
45 "lists": ">=7.0.0 <8.0.0"
42 },
46 },
@@ -44,163 +48,45 @@
44 "maybe": ">=6.0.0 <7.0.0"
48 "maybe": ">=6.0.0 <7.0.0"
45 },
49 },
46 {
50 {
47− "minibench": ">=4.0.1 <5.0.0"
51+ "now": ">=6.0.0 <7.0.0"
48 },
52 },
49 {
53 {
50− "newtype": ">=5.0.0 <6.0.0"
54+ "prelude": ">=6.0.2 <7.0.0"
51 },
55 },
52 {
56 {
53− "now": ">=6.0.0 <7.0.0"
57+ "refs": ">=6.0.0 <7.0.0"
54 },
58 },
55 {
59 {
56− "partial": ">=4.0.0 <5.0.0"
60+ "spec": ">=8.1.2 <9.0.0"
57 },
61 },
58 {
62 {
59− "prelude": ">=6.0.1 <7.0.0"
63+ "st": ">=6.2.0 <7.0.0"
60 },
64 },
61 {
65 {
62− "refs": ">=6.0.0 <7.0.0"
66+ "tailrec": ">=6.1.0 <7.0.0"
63 },
67 },
64 {
68 {
65− "spec": ">=7.6.0 <8.0.0"
69+ "transformers": ">=6.1.0 <7.0.0"
66 },
70 },
67− "st",
68− "tailrec",
69− "transformers",
70 {
71 {
71 "tuples": ">=7.0.0 <8.0.0"
72 "tuples": ">=7.0.0 <8.0.0"
72 },
73 },
73− {
74− "typelevel-prelude": ">=7.0.0 <8.0.0"
75− },
76 {
74 {
77 "variant": ">=8.0.0 <9.0.0"
75 "variant": ">=8.0.0 <9.0.0"
78 },
76 },
79 {
77 {
80− "yoga-om-core": ">=1.0.5 <2.0.0"
78+ "yoga-om": ">=1.5.2 <2.0.0"
81 }
79 }
82− ],
83− "build_plan": [
84− "aff",
85− "ansi",
86− "arrays",
87− "avar",
88− "bifunctors",
89− "catenable-lists",
90− "console",
91− "const",
92− "contravariant",
93− "control",
94− "datetime",
95− "distributive",
96− "effect",
97− "either",
98− "enums",
99− "exceptions",
100− "exists",
101− "foldable-traversable",
102− "fork",
103− "free",
104− "functions",
105− "functors",
106− "gen",
107− "heterogeneous",
108− "identity",
109− "integers",
110− "invariant",
111− "lazy",
112− "lists",
113− "maybe",
114− "minibench",
115− "mmorph",
116− "newtype",
117− "nonempty",
118− "now",
119− "numbers",
120− "ordered-collections",
121− "orders",
122− "parallel",
123− "partial",
124− "pipes",
125− "prelude",
126− "profunctor",
127− "record",
128− "record-studio",
129− "refs",
130− "safe-coerce",
131− "spec",
132− "st",
133− "strings",
134− "tailrec",
135− "transformers",
136− "tuples",
137− "type-equality",
138− "typelevel-prelude",
139− "uncurried-transformers",
140− "unfoldable",
141− "unsafe-coerce",
142− "variant",
143− "yoga-om-core"
144 ]
80 ]
145 },
81 },
146 "test": {
82 "test": {
147 "dependencies": [
83 "dependencies": [
148 {
84 {
149− "spec": ">=7.0.0 <8.0.0"
85+ "spec": ">=8.0.0 <9.0.0"
86+ },
87+ {
88+ "spec-node": ">=0.0.3 <0.1.0"
150 }
89 }
151− ],
152− "build_plan": [
153− "aff",
154− "ansi",
155− "arrays",
156− "avar",
157− "bifunctors",
158− "catenable-lists",
159− "const",
160− "contravariant",
161− "control",
162− "datetime",
163− "distributive",
164− "effect",
165− "either",
166− "enums",
167− "exceptions",
168− "exists",
169− "foldable-traversable",
170− "fork",
171− "free",
172− "functions",
173− "functors",
174− "gen",
175− "identity",
176− "integers",
177− "invariant",
178− "lazy",
179− "lists",
180− "maybe",
181− "mmorph",
182− "newtype",
183− "nonempty",
184− "now",
185− "numbers",
186− "ordered-collections",
187− "orders",
188− "parallel",
189− "partial",
190− "pipes",
191− "prelude",
192− "profunctor",
193− "refs",
194− "safe-coerce",
195− "spec",
196− "st",
197− "strings",
198− "tailrec",
199− "transformers",
200− "tuples",
201− "type-equality",
202− "unfoldable",
203− "unsafe-coerce"
204 ]
90 ]
205 }
91 }
206 }
92 }
@@ -210,28 +96,45 @@
210 "packages": {
96 "packages": {
211 "aff": {
97 "aff": {
212 "type": "registry",
98 "type": "registry",
213− "version": "7.1.0",
99+ "version": "8.0.0",
214− "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
100+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
215 "dependencies": [
101 "dependencies": [
216− "arrays",
217− "bifunctors",
218 "control",
102 "control",
219 "datetime",
103 "datetime",
220 "effect",
104 "effect",
221 "either",
105 "either",
222 "exceptions",
106 "exceptions",
223− "foldable-traversable",
224 "functions",
107 "functions",
225− "maybe",
226 "newtype",
108 "newtype",
227 "parallel",
109 "parallel",
110+ "partial",
228 "prelude",
111 "prelude",
229− "refs",
112+ "st",
230 "tailrec",
113 "tailrec",
231 "transformers",
114 "transformers",
232 "unsafe-coerce"
115 "unsafe-coerce"
233 ]
116 ]
234 },
117 },
118+ "aff-retry": {
119+ "type": "registry",
120+ "version": "2.0.0",
121+ "integrity": "sha256-rKZNcP4IKSyBFQBVzrCvE0ULoJt0N/uDLLJ0dPotr4k=",
122+ "dependencies": [
123+ "aff",
124+ "arrays",
125+ "datetime",
126+ "effect",
127+ "either",
128+ "exceptions",
129+ "integers",
130+ "maybe",
131+ "newtype",
132+ "numbers",
133+ "prelude",
134+ "random",
135+ "transformers"
136+ ]
137+ },
235 "ansi": {
138 "ansi": {
236 "type": "registry",
139 "type": "registry",
237 "version": "7.0.0",
140 "version": "7.0.0",
@@ -239,9 +142,56 @@
239 "dependencies": [
142 "dependencies": [
240 "foldable-traversable",
143 "foldable-traversable",
241 "lists",
144 "lists",
145+ "prelude"
146+ ]
147+ },
148+ "argonaut-codecs": {
149+ "type": "registry",
150+ "version": "9.1.0",
151+ "integrity": "sha256-K910SBrmYallESsm8pxJYThs7lig6hxX3M4PW33Cgew=",
152+ "dependencies": [
153+ "argonaut-core",
154+ "arrays",
155+ "bifunctors",
156+ "either",
157+ "foldable-traversable",
158+ "foreign-object",
159+ "identity",
160+ "integers",
161+ "lists",
162+ "maybe",
163+ "nonempty",
164+ "ordered-collections",
165+ "prelude",
166+ "record",
167+ "strings",
168+ "tuples"
169+ ]
170+ },
171+ "argonaut-core": {
172+ "type": "registry",
173+ "version": "7.0.0",
174+ "integrity": "sha256-gYZihtBIKthVuXXoiPphMookSf8M5aHyFYkcIWqlxTM=",
175+ "dependencies": [
176+ "arrays",
177+ "control",
178+ "either",
179+ "foreign-object",
180+ "functions",
181+ "gen",
182+ "maybe",
183+ "nonempty",
184+ "prelude",
242− "strings"
185+ "strings",
186+ "tailrec"
243 ]
187 ]
244 },
188 },
189+ "arraybuffer-types": {
190+ "type": "registry",
191+ "version": "3.0.2",
192+ "integrity": "sha256-p05cJnSkyeoB7VHzMyc2Eb1RwUaB7Nl0sQSqEGNEUD8=",
193+ "dependencies": []
194+ },
245 "arrays": {
195 "arrays": {
246 "type": "registry",
196 "type": "registry",
247 "version": "7.3.0",
197 "version": "7.3.0",
@@ -273,7 +223,8 @@
273 "either",
223 "either",
274 "exceptions",
224 "exceptions",
275 "functions",
225 "functions",
276− "maybe"
226+ "maybe",
227+ "prelude"
277 ]
228 ]
278 },
229 },
279 "bifunctors": {
230 "bifunctors": {
@@ -432,6 +383,16 @@
432 "unsafe-coerce"
383 "unsafe-coerce"
433 ]
384 ]
434 },
385 },
386+ "exitcodes": {
387+ "type": "registry",
388+ "version": "4.0.0",
389+ "integrity": "sha256-xBl4aFSEQ05Owp1ZvO6OxxvLK3LVzU35RSbj79SsePQ=",
390+ "dependencies": [
391+ "enums",
392+ "maybe",
393+ "prelude"
394+ ]
395+ },
435 "foldable-traversable": {
396 "foldable-traversable": {
436 "type": "registry",
397 "type": "registry",
437 "version": "6.0.0",
398 "version": "6.0.0",
@@ -450,12 +411,50 @@
450 "tuples"
411 "tuples"
451 ]
412 ]
452 },
413 },
414+ "foreign": {
415+ "type": "registry",
416+ "version": "7.0.0",
417+ "integrity": "sha256-jiDRMVQfovGsbbA/FzbzYf6iWk9i2b5gNrIjz+gFfsI=",
418+ "dependencies": [
419+ "either",
420+ "functions",
421+ "integers",
422+ "lists",
423+ "maybe",
424+ "prelude",
425+ "strings",
426+ "transformers",
427+ "unsafe-coerce"
428+ ]
429+ },
430+ "foreign-object": {
431+ "type": "registry",
432+ "version": "4.1.0",
433+ "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=",
434+ "dependencies": [
435+ "arrays",
436+ "foldable-traversable",
437+ "functions",
438+ "gen",
439+ "lists",
440+ "maybe",
441+ "prelude",
442+ "st",
443+ "tailrec",
444+ "tuples",
445+ "typelevel-prelude",
446+ "unfoldable",
447+ "unsafe-coerce"
448+ ]
449+ },
453 "fork": {
450 "fork": {
454 "type": "registry",
451 "type": "registry",
455 "version": "6.0.0",
452 "version": "6.0.0",
456 "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
453 "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
457 "dependencies": [
454 "dependencies": [
458− "aff"
455+ "aff",
456+ "prelude",
457+ "transformers"
459 ]
458 ]
460 },
459 },
461 "free": {
460 "free": {
@@ -568,6 +567,21 @@
568 "prelude"
567 "prelude"
569 ]
568 ]
570 },
569 },
570+ "js-date": {
571+ "type": "registry",
572+ "version": "8.0.0",
573+ "integrity": "sha256-DNQrp4xYc8o80jtxo0aOkUeXGi6h0Xidkk2ZQ8Y4l5Y=",
574+ "dependencies": [
575+ "datetime",
576+ "effect",
577+ "enums",
578+ "foreign",
579+ "functions",
580+ "integers",
581+ "maybe",
582+ "prelude"
583+ ]
584+ },
571 "lazy": {
585 "lazy": {
572 "type": "registry",
586 "type": "registry",
573 "version": "6.0.0",
587 "version": "6.0.0",
@@ -609,27 +623,21 @@
609 "prelude"
623 "prelude"
610 ]
624 ]
611 },
625 },
612− "minibench": {
613− "type": "registry",
614− "version": "4.0.1",
615− "integrity": "sha256-zd/YnOqnzz/ers1umv5rgJOyPntF2zFpcLVbPWQIbYU=",
616− "dependencies": [
617− "console",
618− "effect",
619− "integers",
620− "numbers",
621− "prelude",
622− "refs"
623− ]
624− },
625 "mmorph": {
626 "mmorph": {
626 "type": "registry",
627 "type": "registry",
627 "version": "7.0.0",
628 "version": "7.0.0",
628 "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
629 "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
629 "dependencies": [
630 "dependencies": [
631+ "bifunctors",
632+ "either",
630 "free",
633 "free",
631 "functors",
634 "functors",
635+ "identity",
636+ "maybe",
637+ "newtype",
638+ "prelude",
632− "transformers"
639+ "transformers",
640+ "tuples"
633 ]
641 ]
634 },
642 },
635 "newtype": {
643 "newtype": {
@@ -641,6 +649,107 @@
641 "safe-coerce"
649 "safe-coerce"
642 ]
650 ]
643 },
651 },
652+ "node-buffer": {
653+ "type": "registry",
654+ "version": "9.0.0",
655+ "integrity": "sha256-RdQjilmEHqsT4fwpCr8Mavw8KTafYrS3ZM6zeAAVAl8=",
656+ "dependencies": [
657+ "arraybuffer-types",
658+ "effect",
659+ "functions",
660+ "maybe",
661+ "nullable",
662+ "partial",
663+ "prelude",
664+ "st",
665+ "unsafe-coerce"
666+ ]
667+ },
668+ "node-event-emitter": {
669+ "type": "registry",
670+ "version": "3.0.0",
671+ "integrity": "sha256-wodx71NxJBPXOaawFn01V1ByYB2vT+I3RuV2giOVKMg=",
672+ "dependencies": [
673+ "effect",
674+ "either",
675+ "functions",
676+ "maybe",
677+ "nullable",
678+ "prelude",
679+ "unsafe-coerce"
680+ ]
681+ },
682+ "node-fs": {
683+ "type": "registry",
684+ "version": "9.2.0",
685+ "integrity": "sha256-fxioTSm9y47WuYs9F/7nxCh/RVDCl3Kr3Hg779J4SJA=",
686+ "dependencies": [
687+ "aff",
688+ "datetime",
689+ "effect",
690+ "either",
691+ "enums",
692+ "exceptions",
693+ "functions",
694+ "integers",
695+ "js-date",
696+ "maybe",
697+ "node-buffer",
698+ "node-path",
699+ "node-streams",
700+ "nullable",
701+ "partial",
702+ "prelude",
703+ "strings"
704+ ]
705+ },
706+ "node-path": {
707+ "type": "registry",
708+ "version": "5.0.1",
709+ "integrity": "sha256-j7n/SmpVz0FOMJl4XZop3ofJ+MeIPkMNqUEUKHEL6Us=",
710+ "dependencies": [
711+ "effect"
712+ ]
713+ },
714+ "node-process": {
715+ "type": "registry",
716+ "version": "11.2.0",
717+ "integrity": "sha256-onzKeNmaeYfN3HSDPqSFTrrP2Yl9tnVsfKKhnf2plxM=",
718+ "dependencies": [
719+ "effect",
720+ "exceptions",
721+ "foreign",
722+ "foreign-object",
723+ "maybe",
724+ "node-event-emitter",
725+ "node-streams",
726+ "nullable",
727+ "posix-types",
728+ "prelude",
729+ "strings"
730+ ]
731+ },
732+ "node-streams": {
733+ "type": "registry",
734+ "version": "9.0.1",
735+ "integrity": "sha256-yuVGm/R/tBr4Nphi+a1a984Z0fkmN9Q5wWtSmbmJTpA=",
736+ "dependencies": [
737+ "aff",
738+ "arrays",
739+ "effect",
740+ "either",
741+ "exceptions",
742+ "maybe",
743+ "node-buffer",
744+ "node-event-emitter",
745+ "nullable",
746+ "prelude",
747+ "refs",
748+ "st",
749+ "tailrec",
750+ "unsafe-coerce"
751+ ]
752+ },
644 "nonempty": {
753 "nonempty": {
645 "type": "registry",
754 "type": "registry",
646 "version": "7.0.0",
755 "version": "7.0.0",
@@ -660,7 +769,18 @@
660 "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
769 "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
661 "dependencies": [
770 "dependencies": [
662 "datetime",
771 "datetime",
663− "effect"
772+ "effect",
773+ "prelude"
774+ ]
775+ },
776+ "nullable": {
777+ "type": "registry",
778+ "version": "6.0.0",
779+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
780+ "dependencies": [
781+ "functions",
782+ "maybe",
783+ "prelude"
664 ]
784 ]
665 },
785 },
666 "numbers": {
786 "numbers": {
@@ -669,7 +789,58 @@
669 "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
789 "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
670 "dependencies": [
790 "dependencies": [
671 "functions",
791 "functions",
672− "maybe"
792+ "maybe",
793+ "prelude"
794+ ]
795+ },
796+ "open-memoize": {
797+ "type": "registry",
798+ "version": "6.2.0",
799+ "integrity": "sha256-yJLuVYX8FocHIJ+6q7efH/Pj4S+e1Y57DZ5Dn7qf4ww=",
800+ "dependencies": [
801+ "either",
802+ "integers",
803+ "lazy",
804+ "lists",
805+ "maybe",
806+ "partial",
807+ "prelude",
808+ "strings",
809+ "tuples"
810+ ]
811+ },
812+ "optparse": {
813+ "type": "registry",
814+ "version": "5.0.1",
815+ "integrity": "sha256-2btDjHPRR0uujUQJuZAN9AzvVoXe7yG6IwnY5evERhQ=",
816+ "dependencies": [
817+ "arrays",
818+ "bifunctors",
819+ "control",
820+ "effect",
821+ "either",
822+ "enums",
823+ "exists",
824+ "exitcodes",
825+ "foldable-traversable",
826+ "free",
827+ "integers",
828+ "lazy",
829+ "lists",
830+ "maybe",
831+ "newtype",
832+ "node-buffer",
833+ "node-process",
834+ "node-streams",
835+ "nonempty",
836+ "numbers",
837+ "open-memoize",
838+ "partial",
839+ "prelude",
840+ "strings",
841+ "tailrec",
842+ "transformers",
843+ "tuples"
673 ]
844 ]
674 },
845 },
675 "ordered-collections": {
846 "ordered-collections": {
@@ -678,13 +849,16 @@
678 "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
849 "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
679 "dependencies": [
850 "dependencies": [
680 "arrays",
851 "arrays",
852+ "control",
681 "foldable-traversable",
853 "foldable-traversable",
854+ "functions",
682 "gen",
855 "gen",
683 "lists",
856 "lists",
684 "maybe",
857 "maybe",
858+ "newtype",
685 "partial",
859 "partial",
686 "prelude",
860 "prelude",
687− "st",
861+ "safe-coerce",
688 "tailrec",
862 "tailrec",
689 "tuples",
863 "tuples",
690 "unfoldable"
864 "unfoldable"
@@ -701,8 +875,8 @@
701 },
875 },
702 "parallel": {
876 "parallel": {
703 "type": "registry",
877 "type": "registry",
704− "version": "6.0.0",
878+ "version": "7.0.0",
705− "integrity": "sha256-1dpN/7kBct/2aONnyk4TU0AFN71GvfoRxrqvFGRU4Os=",
879+ "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=",
706 "dependencies": [
880 "dependencies": [
707 "control",
881 "control",
708 "effect",
882 "effect",
@@ -729,14 +903,31 @@
729 "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
903 "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
730 "dependencies": [
904 "dependencies": [
731 "aff",
905 "aff",
906+ "control",
907+ "effect",
908+ "either",
909+ "foldable-traversable",
910+ "identity",
732 "lists",
911 "lists",
912+ "maybe",
733 "mmorph",
913 "mmorph",
914+ "newtype",
734 "prelude",
915 "prelude",
735 "tailrec",
916 "tailrec",
736 "transformers",
917 "transformers",
737 "tuples"
918 "tuples"
738 ]
919 ]
739 },
920 },
921+ "posix-types": {
922+ "type": "registry",
923+ "version": "6.0.0",
924+ "integrity": "sha256-Je5jG439F/SFO/IvVevZnWVxDIonrehlKuJkefP2lHE=",
925+ "dependencies": [
926+ "maybe",
927+ "newtype",
928+ "prelude"
929+ ]
930+ },
740 "prelude": {
931 "prelude": {
741 "type": "registry",
932 "type": "registry",
742 "version": "6.0.2",
933 "version": "6.0.2",
@@ -758,6 +949,16 @@
758 "tuples"
949 "tuples"
759 ]
950 ]
760 },
951 },
952+ "random": {
953+ "type": "registry",
954+ "version": "6.0.0",
955+ "integrity": "sha256-7sZRo6P9UIiXDZ9s2QL9T8h3wlIwLdQrR4+oZKOijQw=",
956+ "dependencies": [
957+ "effect",
958+ "integers",
959+ "prelude"
960+ ]
961+ },
761 "record": {
962 "record": {
762 "type": "registry",
963 "type": "registry",
763 "version": "4.0.0",
964 "version": "4.0.0",
@@ -800,8 +1001,8 @@
800 },
1001 },
801 "spec": {
1002 "spec": {
802 "type": "registry",
1003 "type": "registry",
803− "version": "7.6.1",
1004+ "version": "8.1.2",
804− "integrity": "sha256-5GysDOS1gVbbGX75YXUdhww3h2S246Ck0sS6yt+ig+0=",
1005+ "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=",
805 "dependencies": [
1006 "dependencies": [
806 "aff",
1007 "aff",
807 "ansi",
1008 "ansi",
@@ -832,11 +1033,44 @@
832 "tuples"
1033 "tuples"
833 ]
1034 ]
834 },
1035 },
1036+ "spec-node": {
1037+ "type": "registry",
1038+ "version": "0.0.3",
1039+ "integrity": "sha256-8/9+g0obbamV0YtqzmttbWs8HaWF7CEeW+eDtqzuAAo=",
1040+ "dependencies": [
1041+ "aff",
1042+ "argonaut-codecs",
1043+ "argonaut-core",
1044+ "arrays",
1045+ "control",
1046+ "datetime",
1047+ "effect",
1048+ "either",
1049+ "foldable-traversable",
1050+ "identity",
1051+ "integers",
1052+ "maybe",
1053+ "newtype",
1054+ "node-buffer",
1055+ "node-fs",
1056+ "node-process",
1057+ "now",
1058+ "numbers",
1059+ "optparse",
1060+ "ordered-collections",
1061+ "partial",
1062+ "prelude",
1063+ "spec",
1064+ "strings",
1065+ "tuples"
1066+ ]
1067+ },
835 "st": {
1068 "st": {
836 "type": "registry",
1069 "type": "registry",
837 "version": "6.2.0",
1070 "version": "6.2.0",
838 "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
1071 "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
839 "dependencies": [
1072 "dependencies": [
1073+ "effect",
840 "partial",
1074 "partial",
841 "prelude",
1075 "prelude",
842 "tailrec",
1076 "tailrec",
@@ -968,22 +1202,24 @@
968 "version": "8.0.0",
1202 "version": "8.0.0",
969 "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1203 "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
970 "dependencies": [
1204 "dependencies": [
1205+ "control",
971 "enums",
1206 "enums",
1207+ "foldable-traversable",
972 "lists",
1208 "lists",
973 "maybe",
1209 "maybe",
974 "partial",
1210 "partial",
975 "prelude",
1211 "prelude",
976− "record",
1212+ "type-equality",
977− "tuples",
978 "unsafe-coerce"
1213 "unsafe-coerce"
979 ]
1214 ]
980 },
1215 },
981− "yoga-om-core": {
1216+ "yoga-om": {
982 "type": "registry",
1217 "type": "registry",
983− "version": "1.0.5",
1218+ "version": "1.5.2",
984− "integrity": "sha256-YMJKVwUsmLOoGE3sQr/+xc5F8OsV5j+xKJa4eDYBv48=",
1219+ "integrity": "sha256-2ys14BF6NPgzFDFUIFk0EDxmqkA8+yghIA9TXHlOmE0=",
985 "dependencies": [
1220 "dependencies": [
986 "aff",
1221 "aff",
1222+ "aff-retry",
987 "avar",
1223 "avar",
988 "console",
1224 "console",
989 "control",
1225 "control",
@@ -992,13 +1228,14 @@
992 "either",
1228 "either",
993 "exceptions",
1229 "exceptions",
994 "functions",
1230 "functions",
1231+ "heterogeneous",
995 "maybe",
1232 "maybe",
996 "newtype",
1233 "newtype",
997 "parallel",
1234 "parallel",
998 "prelude",
1235 "prelude",
999 "record",
1236 "record",
1000 "record-studio",
1237 "record-studio",
1001− "spec",
1238+ "refs",
1002 "tailrec",
1239 "tailrec",
1003 "transformers",
1240 "transformers",
1004 "tuples",
1241 "tuples",
spago.yaml+14−18
@@ -1,9 +1,8 @@
1workspace: {}
1workspace: {}
2−
3package:
2package:
4 name: yoga-om-strom
3 name: yoga-om-strom
5 publish:
4 publish:
6− version: 0.1.1
5+ version: 0.4.3
7 license: MIT-0
6 license: MIT-0
8 exclude:
7 exclude:
9 - "src/**/*.test.purs"
8 - "src/**/*.test.purs"
@@ -12,35 +11,32 @@ package:
12 githubOwner: rowtype-yoga
11 githubOwner: rowtype-yoga
13 githubRepo: purescript-yoga-om-strom
12 githubRepo: purescript-yoga-om-strom
14 dependencies:
13 dependencies:
15− - catenable-lists
16− - integers
17− - st
18− - tailrec
19− - transformers
20− - aff: ">=7.1.0 <8.0.0"
14+ - aff: ">=8.0.0 <9.0.0"
21 - arrays: ">=7.3.0 <8.0.0"
15 - arrays: ">=7.3.0 <8.0.0"
22− - avar: ">=5.0.0 <6.0.0"
16+ - avar: ">=5.0.1 <6.0.0"
17+ - catenable-lists: ">=7.0.0 <8.0.0"
23 - console: ">=6.1.0 <7.0.0"
18 - console: ">=6.1.0 <7.0.0"
24 - control: ">=6.0.0 <7.0.0"
19 - control: ">=6.0.0 <7.0.0"
25 - datetime: ">=6.1.0 <7.0.0"
20 - datetime: ">=6.1.0 <7.0.0"
26 - effect: ">=4.0.0 <5.0.0"
21 - effect: ">=4.0.0 <5.0.0"
27 - either: ">=6.1.0 <7.0.0"
22 - either: ">=6.1.0 <7.0.0"
28− - exceptions: ">=6.0.0 <7.0.0"
23+ - exceptions: ">=6.1.0 <7.0.0"
29 - foldable-traversable: ">=6.0.0 <7.0.0"
24 - foldable-traversable: ">=6.0.0 <7.0.0"
25+ - integers: ">=6.0.0 <7.0.0"
30 - lists: ">=7.0.0 <8.0.0"
26 - lists: ">=7.0.0 <8.0.0"
31 - maybe: ">=6.0.0 <7.0.0"
27 - maybe: ">=6.0.0 <7.0.0"
32− - minibench: ">=4.0.1 <5.0.0"
33− - newtype: ">=5.0.0 <6.0.0"
34 - now: ">=6.0.0 <7.0.0"
28 - now: ">=6.0.0 <7.0.0"
35− - partial: ">=4.0.0 <5.0.0"
36− - prelude: ">=6.0.1 <7.0.0"
29+ - prelude: ">=6.0.2 <7.0.0"
37 - refs: ">=6.0.0 <7.0.0"
30 - refs: ">=6.0.0 <7.0.0"
38− - spec: ">=7.6.0 <8.0.0"
31+ - spec: ">=8.1.2 <9.0.0"
32+ - st: ">=6.2.0 <7.0.0"
33+ - tailrec: ">=6.1.0 <7.0.0"
34+ - transformers: ">=6.1.0 <7.0.0"
39 - tuples: ">=7.0.0 <8.0.0"
35 - tuples: ">=7.0.0 <8.0.0"
40− - typelevel-prelude: ">=7.0.0 <8.0.0"
41 - variant: ">=8.0.0 <9.0.0"
36 - variant: ">=8.0.0 <9.0.0"
42− - yoga-om-core: ">=1.0.5 <2.0.0"
37+ - yoga-om: ">=1.5.2 <2.0.0"
43 test:
38 test:
44 main: Test.Main.Strom
39 main: Test.Main.Strom
45 dependencies:
40 dependencies:
46− - spec: ">=7.0.0 <8.0.0"
41+ - spec: ">=8.0.0 <9.0.0"
42+ - spec-node: ">=0.0.3 <0.1.0"
src/Benchmark.purs+1−2
@@ -10,10 +10,9 @@ import Data.Tuple (Tuple(..))
10import Effect (Effect)
10import Effect (Effect)
11import Effect.Aff (Aff, launchAff_, throwError)
11import Effect.Aff (Aff, launchAff_, throwError)
12import Effect.Class (liftEffect)
12import Effect.Class (liftEffect)
13−import Effect.Exception (Error)
14import Effect.Class.Console as Console
13import Effect.Class.Console as Console
15import Effect.Now (now)
14import Effect.Now (now)
16−import Data.DateTime.Instant (unInstant, diff)
15+import Data.DateTime.Instant (diff)
17import Yoga.Om as Om
16import Yoga.Om as Om
18import Yoga.Om.Strom as Strom
17import Yoga.Om.Strom as Strom
19
18
src/Yoga/Om/Strom.purs+264−302
@@ -65,6 +65,7 @@ module Yoga.Om.Strom
65 , partitionMap
65 , partitionMap
66 -- Timing
66 -- Timing
67 , debounce
67 , debounce
68+ , debounceEach
68 , throttle
69 , throttle
69 , delayStrom
70 , delayStrom
70 -- Combining
71 -- Combining
@@ -108,6 +109,9 @@ module Yoga.Om.Strom
108 , bracket
109 , bracket
109 , bracketExit
110 , bracketExit
110 , acquireRelease
111 , acquireRelease
112+ -- Internal (needed by WebStream)
113+ , mkStrom
114+ , runStrom
111 ) where
115 ) where
112
116
113import Prelude
117import Prelude
@@ -129,18 +133,16 @@ import Data.List as List
129import Data.Maybe (Maybe(..))
133import Data.Maybe (Maybe(..))
130import Data.Time.Duration (Milliseconds(..))
134import Data.Time.Duration (Milliseconds(..))
131import Data.Traversable (traverse, traverse_)
135import Data.Traversable (traverse, traverse_)
132−import Data.Tuple (Tuple(..))
136+import Data.Tuple.Nested (type (/\), (/\))
133import Data.Variant (Variant)
137import Data.Variant (Variant)
134−import Effect.Aff (Aff, Error, delay, forkAff, joinFiber, killFiber)
138+import Effect.Aff (Aff, Error, delay, forkAff, killFiber)
135import Effect.Aff.AVar as AVar
139import Effect.Aff.AVar as AVar
136import Effect.Aff.Class (liftAff)
140import Effect.Aff.Class (liftAff)
137import Effect.Exception as Exception
141import Effect.Exception as Exception
138−import Effect.Now (now)
139import Effect.Ref as Ref
142import Effect.Ref as Ref
140import Effect.Class (liftEffect)
143import Effect.Class (liftEffect)
141import Data.CatQueue as CatQueue
144import Data.CatQueue as CatQueue
142import Data.CatQueue (CatQueue)
145import Data.CatQueue (CatQueue)
143−import Data.DateTime.Instant (unInstant)
144import Yoga.Om (Om)
146import Yoga.Om (Om)
145import Yoga.Om as Om
147import Yoga.Om as Om
146
148
@@ -160,18 +162,18 @@ import Yoga.Om as Om
160-- | - Loop (chunk, next) => emit chunk (if present) and continue with next
162-- | - Loop (chunk, next) => emit chunk (if present) and continue with next
161-- | - Done finalChunk => emit final chunk (if present) and stop
163-- | - Done finalChunk => emit final chunk (if present) and stop
162newtype Strom ctx err a = Strom
164newtype Strom ctx err a = Strom
163− { pull :: Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a)))
165+ { pull :: Om ctx err (Step (((Maybe (Chunk a)) /\ (Strom ctx err a))) (Maybe (Chunk a)))
164 }
166 }
165
167
166-- | Chunks for efficient batch processing
168-- | Chunks for efficient batch processing
167type Chunk a = Array a
169type Chunk a = Array a
168
170
169-- | Helper to create a Strom from a pull function
171-- | Helper to create a Strom from a pull function
170−mkStrom :: forall ctx err a. Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a))) -> Strom ctx err a
172+mkStrom :: forall ctx err a. Om ctx err (Step (((Maybe (Chunk a)) /\ (Strom ctx err a))) (Maybe (Chunk a))) -> Strom ctx err a
171mkStrom pull = Strom { pull }
173mkStrom pull = Strom { pull }
172
174
173-- | Helper to unwrap a Strom
175-- | Helper to unwrap a Strom
174−runStrom :: forall ctx err a. Strom ctx err a -> Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a)))
176+runStrom :: forall ctx err a. Strom ctx err a -> Om ctx err (Step (((Maybe (Chunk a)) /\ (Strom ctx err a))) (Maybe (Chunk a)))
175runStrom (Strom s) = s.pull
177runStrom (Strom s) = s.pull
176
178
177--------------------------------------------------------------------------------
179--------------------------------------------------------------------------------
@@ -181,22 +183,22 @@ runStrom (Strom s) = s.pull
181-- | Stream identifier for tracking which stream produced data
183-- | Stream identifier for tracking which stream produced data
182data StreamId = StreamId1 | StreamId2
184data StreamId = StreamId1 | StreamId2
183
185
184−derive instance eqStreamId :: Eq StreamId
186+derive instance Eq StreamId
185−derive instance ordStreamId :: Ord StreamId
187+derive instance Ord StreamId
186
188
187-- | Messages passed through the coordination queue
189-- | Messages passed through the coordination queue
188data QueueItem a
190data QueueItem a
189 = DataChunk (Array a) StreamId
191 = DataChunk (Array a) StreamId
190 | StreamDone StreamId
192 | StreamDone StreamId
191
193
192−derive instance functorQueueItem :: Functor QueueItem
194+derive instance Functor QueueItem
193
195
194-- | Messages for mergeAllND buffered queue
196-- | Messages for mergeAllND buffered queue
195data MergeAllItem a
197data MergeAllItem a
196 = MergeChunk (Array a)
198 = MergeChunk (Array a)
197 | MergeDone
199 | MergeDone
198
200
199−derive instance functorMergeAllItem :: Functor MergeAllItem
201+derive instance Functor MergeAllItem
200
202
201-- | Merge termination strategy (like ZStream HaltStrategy)
203-- | Merge termination strategy (like ZStream HaltStrategy)
202data HaltStrategy
204data HaltStrategy
@@ -205,14 +207,14 @@ data HaltStrategy
205 | HaltLeft
207 | HaltLeft
206 | HaltRight
208 | HaltRight
207
209
208−derive instance eqHaltStrategy :: Eq HaltStrategy
210+derive instance Eq HaltStrategy
209
211
210-- | Messages for buffering queues
212-- | Messages for buffering queues
211data BufferItem a
213data BufferItem a
212 = BufferChunk (Array a)
214 = BufferChunk (Array a)
213 | BufferDone
215 | BufferDone
214
216
215−derive instance functorBufferItem :: Functor BufferItem
217+derive instance Functor BufferItem
216
218
217--------------------------------------------------------------------------------
219--------------------------------------------------------------------------------
218-- Construction
220-- Construction
@@ -268,7 +270,7 @@ rangeStrom start end = rangeHelper start end
268 chunkEnd = min (current + chunkSize) limit
270 chunkEnd = min (current + chunkSize) limit
269 chunk = Array.range current (chunkEnd - 1)
271 chunk = Array.range current (chunkEnd - 1)
270 if chunkEnd >= limit then pure $ Done $ Just chunk -- Final chunk
272 if chunkEnd >= limit then pure $ Done $ Just chunk -- Final chunk
271− else pure $ Loop $ Tuple (Just chunk) (rangeHelper chunkEnd limit) -- Emit chunk and continue
273+ else pure $ Loop $ ((Just chunk) /\ (rangeHelper chunkEnd limit)) -- Emit chunk and continue
272
274
273-- | Iterate producing values - limited to ~10,000 elements for stack safety
275-- | Iterate producing values - limited to ~10,000 elements for stack safety
274-- | For truly infinite streams, use `iterateStromInfinite` (with Aff overhead)
276-- | For truly infinite streams, use `iterateStromInfinite` (with Aff overhead)
@@ -306,7 +308,7 @@ iterateStromInfinite f initial = iterateHelper initial
306 finalVal <- STRef.read valRef
308 finalVal <- STRef.read valRef
307 frozen <- STArray.unsafeFreeze arr
309 frozen <- STArray.unsafeFreeze arr
308 pure { arr: frozen, nextVal: finalVal }
310 pure { arr: frozen, nextVal: finalVal }
309− pure $ Loop $ Tuple (Just result.arr) (iterateHelper result.nextVal)
311+ pure $ Loop $ ((Just result.arr) /\ (iterateHelper result.nextVal))
310
312
311-- | Repeat a value - limited to ~10,000 elements for stack safety
313-- | Repeat a value - limited to ~10,000 elements for stack safety
312repeatStrom :: forall ctx err a. a -> Strom ctx err a
314repeatStrom :: forall ctx err a. a -> Strom ctx err a
@@ -321,7 +323,7 @@ repeatStromInfinite a = repeatHelper unit
321 -- Async boundary resets the stack!
323 -- Async boundary resets the stack!
322 liftAff $ delay (Milliseconds 0.0)
324 liftAff $ delay (Milliseconds 0.0)
323 let chunk = Array.replicate 10000 a
325 let chunk = Array.replicate 10000 a
324− pure $ Loop $ Tuple (Just chunk) (repeatHelper unit)
326+ pure $ Loop $ ((Just chunk) /\ (repeatHelper unit))
325
327
326-- | Repeat an Om computation - limited to 100 iterations for stack safety
328-- | Repeat an Om computation - limited to 100 iterations for stack safety
327-- | For truly infinite repetition, use `repeatOmStromInfinite` (with Aff overhead)
329-- | For truly infinite repetition, use `repeatOmStromInfinite` (with Aff overhead)
@@ -333,7 +335,7 @@ repeatOmStrom om = repeatOmHelper 0
333 | count >= maxCount = empty
335 | count >= maxCount = empty
334 | otherwise = mkStrom do
336 | otherwise = mkStrom do
335 value <- om
337 value <- om
336− pure $ Loop $ Tuple (Just [ value ]) (repeatOmHelper (count + 1))
338+ pure $ Loop $ ((Just [ value ]) /\ (repeatOmHelper (count + 1)))
337
339
338-- | Truly infinite Om repetition - stack-safe via Aff async boundaries
340-- | Truly infinite Om repetition - stack-safe via Aff async boundaries
339-- | Adds tiny delay (0ms) at each pull to reset the stack
341-- | Adds tiny delay (0ms) at each pull to reset the stack
@@ -344,11 +346,11 @@ repeatOmStromInfinite om = repeatOmHelper unit
344 -- Async boundary resets the stack!
346 -- Async boundary resets the stack!
345 liftAff $ delay (Milliseconds 0.0)
347 liftAff $ delay (Milliseconds 0.0)
346 value <- om
348 value <- om
347− pure $ Loop $ Tuple (Just [ value ]) (repeatOmHelper unit)
349+ pure $ Loop $ ((Just [ value ]) /\ (repeatOmHelper unit))
348
350
349-- | Unfold a stream from a seed value - can be infinite if f never returns Nothing
351-- | Unfold a stream from a seed value - can be infinite if f never returns Nothing
350-- | Emits chunks lazily, terminates when f returns Nothing
352-- | Emits chunks lazily, terminates when f returns Nothing
351−unfoldStrom :: forall ctx err a b. (b -> Maybe (Tuple a b)) -> b -> Strom ctx err a
353+unfoldStrom :: forall ctx err a b. (b -> Maybe ((a /\ b))) -> b -> Strom ctx err a
352unfoldStrom f seed = unfoldHelper seed
354unfoldStrom f seed = unfoldHelper seed
353 where
355 where
354 unfoldHelper currentSeed = mkStrom do
356 unfoldHelper currentSeed = mkStrom do
@@ -357,7 +359,7 @@ unfoldStrom f seed = unfoldHelper seed
357 Nothing -> pure $ Done $ if Array.null chunk.values then Nothing else Just chunk.values
359 Nothing -> pure $ Done $ if Array.null chunk.values then Nothing else Just chunk.values
358 Just nextSeed ->
360 Just nextSeed ->
359 if Array.null chunk.values then runStrom (unfoldHelper nextSeed)
361 if Array.null chunk.values then runStrom (unfoldHelper nextSeed)
360− else pure $ Loop $ Tuple (Just chunk.values) (unfoldHelper nextSeed)
362+ else pure $ Loop $ ((Just chunk.values) /\ (unfoldHelper nextSeed))
361
363
362 buildChunk seed' size = ST.run do
364 buildChunk seed' size = ST.run do
363 arr <- STArray.unsafeThaw []
365 arr <- STArray.unsafeThaw []
@@ -370,7 +372,7 @@ unfoldStrom f seed = unfoldHelper seed
370 s <- STRef.read seedRef
372 s <- STRef.read seedRef
371 case f s of
373 case f s of
372 Nothing -> void $ STRef.write true stoppedRef
374 Nothing -> void $ STRef.write true stoppedRef
373− Just (Tuple value nextS) -> do
375+ Just ((value /\ nextS)) -> do
374 void $ STArray.push value arr
376 void $ STArray.push value arr
375 void $ STRef.write nextS seedRef
377 void $ STRef.write nextS seedRef
376 frozen <- STArray.unsafeFreeze arr
378 frozen <- STArray.unsafeFreeze arr
@@ -379,14 +381,14 @@ unfoldStrom f seed = unfoldHelper seed
379 pure { values: frozen, finalSeed: if stopped then Nothing else Just finalSeed }
381 pure { values: frozen, finalSeed: if stopped then Nothing else Just finalSeed }
380
382
381-- | Unfold a stream with an effectful function
383-- | Unfold a stream with an effectful function
382−unfoldOmStrom :: forall ctx err a b. (b -> Om ctx err (Maybe (Tuple a b))) -> b -> Strom ctx err a
384+unfoldOmStrom :: forall ctx err a b. (b -> Om ctx err (Maybe ((a /\ b)))) -> b -> Strom ctx err a
383unfoldOmStrom f seed = mkStrom go
385unfoldOmStrom f seed = mkStrom go
384 where
386 where
385 go = do
387 go = do
386 result <- f seed
388 result <- f seed
387 case result of
389 case result of
388 Nothing -> pure $ Done Nothing
390 Nothing -> pure $ Done Nothing
389− Just (Tuple value nextSeed) -> pure $ Loop $ Tuple (Just [ value ]) (unfoldOmStrom f nextSeed)
391+ Just ((value /\ nextSeed)) -> pure $ Loop $ ((Just [ value ]) /\ (unfoldOmStrom f nextSeed))
390
392
391--------------------------------------------------------------------------------
393--------------------------------------------------------------------------------
392-- Transformations
394-- Transformations
@@ -399,7 +401,7 @@ mapStrom f stream = mkStrom do
399 case step of
401 case step of
400 Done Nothing -> pure $ Done Nothing
402 Done Nothing -> pure $ Done Nothing
401 Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
403 Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
402− Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple (Functor.map (Functor.map f) maybeChunk) (mapStrom f next)
404+ Loop ((maybeChunk /\ next)) -> pure $ Loop $ ((Functor.map (Functor.map f) maybeChunk) /\ (mapStrom f next))
403
405
404-- | Map with a monadic effect
406-- | Map with a monadic effect
405mapMStrom :: forall ctx err a b. (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
407mapMStrom :: forall ctx err a b. (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
@@ -410,11 +412,11 @@ mapMStrom f stream = mkStrom do
410 Done (Just chunk) -> do
412 Done (Just chunk) -> do
411 mapped <- traverse f chunk
413 mapped <- traverse f chunk
412 pure $ Done $ Just mapped
414 pure $ Done $ Just mapped
413− Loop (Tuple maybeChunk next) -> do
415+ Loop ((maybeChunk /\ next)) -> do
414 mappedChunk <- case maybeChunk of
416 mappedChunk <- case maybeChunk of
415 Nothing -> pure Nothing
417 Nothing -> pure Nothing
416 Just chunk -> Just <$> traverse f chunk
418 Just chunk -> Just <$> traverse f chunk
417− pure $ Loop $ Tuple mappedChunk (mapMStrom f next)
419+ pure $ Loop $ (mappedChunk /\ (mapMStrom f next))
418
420
419-- | Tap (observe without modifying)
421-- | Tap (observe without modifying)
420tapStrom :: forall ctx err a. (a -> Unit) -> Strom ctx err a -> Strom ctx err a
422tapStrom :: forall ctx err a. (a -> Unit) -> Strom ctx err a -> Strom ctx err a
@@ -425,16 +427,15 @@ tapStrom f stream = mkStrom do
425 Done (Just chunk) -> do
427 Done (Just chunk) -> do
426 let _ = Functor.map f chunk
428 let _ = Functor.map f chunk
427 pure $ Done $ Just chunk
429 pure $ Done $ Just chunk
428− Loop (Tuple maybeChunk next) -> do
430+ Loop ((maybeChunk /\ next)) -> do
429 let
431 let
430 tappedChunk = case maybeChunk of
432 tappedChunk = case maybeChunk of
431 Nothing -> Nothing
433 Nothing -> Nothing
432 Just chunk ->
434 Just chunk ->
433− let
435+ do
434− _ = Functor.map f chunk
436+ let _ = Functor.map f chunk
435− in
436 Just chunk
437 Just chunk
437− pure $ Loop $ Tuple tappedChunk (tapStrom f next)
438+ pure $ Loop $ (tappedChunk /\ (tapStrom f next))
438
439
439-- | Tap with monadic effect (observe with effects)
440-- | Tap with monadic effect (observe with effects)
440tapMStrom :: forall ctx err a. (a -> Om ctx err Unit) -> Strom ctx err a -> Strom ctx err a
441tapMStrom :: forall ctx err a. (a -> Om ctx err Unit) -> Strom ctx err a -> Strom ctx err a
@@ -445,20 +446,20 @@ tapMStrom f stream = mkStrom do
445 Done (Just chunk) -> do
446 Done (Just chunk) -> do
446 _ <- traverse f chunk
447 _ <- traverse f chunk
447 pure $ Done $ Just chunk
448 pure $ Done $ Just chunk
448− Loop (Tuple maybeChunk next) -> do
449+ Loop ((maybeChunk /\ next)) -> do
449 tappedChunk <- case maybeChunk of
450 tappedChunk <- case maybeChunk of
450 Nothing -> pure Nothing
451 Nothing -> pure Nothing
451 Just chunk -> do
452 Just chunk -> do
452 _ <- traverse f chunk
453 _ <- traverse f chunk
453 pure $ Just chunk
454 pure $ Just chunk
454− pure $ Loop $ Tuple tappedChunk (tapMStrom f next)
455+ pure $ Loop $ (tappedChunk /\ (tapMStrom f next))
455
456
456-- | Bind for streams (monadic flatMap/chain)
457-- | Bind for streams (monadic flatMap/chain)
457-- | Use >>= operator for cleaner syntax: `stream >>= f`
458-- | Use >>= operator for cleaner syntax: `stream >>= f`
458bindStrom :: forall ctx err a b. (a -> Strom ctx err b) -> Strom ctx err a -> Strom ctx err b
459bindStrom :: forall ctx err a b. (a -> Strom ctx err b) -> Strom ctx err a -> Strom ctx err b
459bindStrom f stream = stream >>= f
460bindStrom f stream = stream >>= f
460
461
461−-- | Scan with accumulator (like foldl but emits intermediate results)
462+-- | Scan with accumulator (like foldl but emits intermediate results)
462scanStrom :: forall ctx err a b. (b -> a -> b) -> b -> Strom ctx err a -> Strom ctx err b
463scanStrom :: forall ctx err a b. (b -> a -> b) -> b -> Strom ctx err a -> Strom ctx err b
463scanStrom f initial stream = scanHelper initial stream
464scanStrom f initial stream = scanHelper initial stream
464 where
465 where
@@ -469,35 +470,33 @@ scanStrom f initial stream = scanHelper initial stream
469 Done (Just chunk) -> do
470 Done (Just chunk) -> do
470 -- Build results by folding and collecting intermediate values
471 -- Build results by folding and collecting intermediate values
471 let
472 let
472− Tuple _ results = Array.foldl
473+ (_ /\ results) = Array.foldl
473− ( \(Tuple prevAcc accList) item ->
474+ ( \((prevAcc /\ accList)) item ->
474− let
475+ do
475− newAcc = f prevAcc item
476+ let newAcc = f prevAcc item
476− in
477− Tuple newAcc (Array.snoc accList newAcc)
477+ (newAcc /\ (Array.snoc accList newAcc))
478 )
478 )
479− (Tuple acc [])
479+ (acc /\ [])
480 chunk
480 chunk
481 pure $ Done $ if Array.null results then Nothing else Just results
481 pure $ Done $ if Array.null results then Nothing else Just results
482− Loop (Tuple maybeChunk next) ->
482+ Loop ((maybeChunk /\ next)) ->
483 case maybeChunk of
483 case maybeChunk of
484− Nothing -> pure $ Loop $ Tuple Nothing (scanHelper acc next)
484+ Nothing -> pure $ Loop $ (Nothing /\ (scanHelper acc next))
485 Just chunk -> do
485 Just chunk -> do
486 let
486 let
487− Tuple newAcc results = Array.foldl
487+ (newAcc /\ results) = Array.foldl
488− ( \(Tuple prevAcc accList) item ->
488+ ( \((prevAcc /\ accList)) item ->
489− let
489+ do
490− currAcc = f prevAcc item
490+ let currAcc = f prevAcc item
491− in
492− Tuple currAcc (Array.snoc accList currAcc)
491+ (currAcc /\ (Array.snoc accList currAcc))
493 )
492 )
494− (Tuple acc [])
493+ (acc /\ [])
495 chunk
494 chunk
496− if Array.null results then pure $ Loop $ Tuple Nothing (scanHelper newAcc next)
495+ if Array.null results then pure $ Loop $ (Nothing /\ (scanHelper newAcc next))
497− else pure $ Loop $ Tuple (Just results) (scanHelper newAcc next)
496+ else pure $ Loop $ ((Just results) /\ (scanHelper newAcc next))
498
497
499-- | Map with accumulator (stateful map)
498-- | Map with accumulator (stateful map)
500−mapAccumStrom :: forall ctx err a b s. (s -> a -> Tuple s b) -> s -> Strom ctx err a -> Strom ctx err b
499+mapAccumStrom :: forall ctx err a b s. (s -> a -> (s /\ b)) -> s -> Strom ctx err a -> Strom ctx err b
501mapAccumStrom f initial stream = mapAccumHelper initial stream
500mapAccumStrom f initial stream = mapAccumHelper initial stream
502 where
501 where
503 mapAccumHelper state s = mkStrom do
502 mapAccumHelper state s = mkStrom do
@@ -506,32 +505,30 @@ mapAccumStrom f initial stream = mapAccumHelper initial stream
506 Done Nothing -> pure $ Done Nothing
505 Done Nothing -> pure $ Done Nothing
507 Done (Just chunk) -> do
506 Done (Just chunk) -> do
508 let
507 let
509− Tuple finalState results = Array.foldl
508+ (_ /\ results) = Array.foldl
510− ( \(Tuple st acc) a ->
509+ ( \((st /\ acc)) a ->
511− let
510+ do
512− Tuple newSt b = f st a
511+ let (newSt /\ b) = f st a
513− in
514− Tuple newSt (Array.snoc acc b)
512+ (newSt /\ (Array.snoc acc b))
515 )
513 )
516− (Tuple state [])
514+ (state /\ [])
517 chunk
515 chunk
518 pure $ Done $ if Array.null results then Nothing else Just results
516 pure $ Done $ if Array.null results then Nothing else Just results
519− Loop (Tuple maybeChunk next) ->
517+ Loop ((maybeChunk /\ next)) ->
520 case maybeChunk of
518 case maybeChunk of
521− Nothing -> pure $ Loop $ Tuple Nothing (mapAccumHelper state next)
519+ Nothing -> pure $ Loop $ (Nothing /\ (mapAccumHelper state next))
522 Just chunk -> do
520 Just chunk -> do
523 let
521 let
524− Tuple newState results = Array.foldl
522+ (newState /\ results) = Array.foldl
525− ( \(Tuple st acc) a ->
523+ ( \((st /\ acc)) a ->
526− let
524+ do
527− Tuple newSt b = f st a
525+ let (newSt /\ b) = f st a
528− in
529− Tuple newSt (Array.snoc acc b)
526+ (newSt /\ (Array.snoc acc b))
530 )
527 )
531− (Tuple state [])
528+ (state /\ [])
532 chunk
529 chunk
533− if Array.null results then pure $ Loop $ Tuple Nothing (mapAccumHelper newState next)
530+ if Array.null results then pure $ Loop $ (Nothing /\ (mapAccumHelper newState next))
534− else pure $ Loop $ Tuple (Just results) (mapAccumHelper newState next)
531+ else pure $ Loop $ ((Just results) /\ (mapAccumHelper newState next))
535
532
536-- | Filter elements
533-- | Filter elements
537filterStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
534filterStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -542,16 +539,15 @@ filterStrom predicate stream = mkStrom do
542 Done (Just chunk) -> do
539 Done (Just chunk) -> do
543 let filtered = Array.filter predicate chunk
540 let filtered = Array.filter predicate chunk
544 pure $ Done $ if Array.null filtered then Nothing else Just filtered
541 pure $ Done $ if Array.null filtered then Nothing else Just filtered
545− Loop (Tuple maybeChunk next) -> do
542+ Loop ((maybeChunk /\ next)) -> do
546 let
543 let
547 filteredChunk = case maybeChunk of
544 filteredChunk = case maybeChunk of
548 Nothing -> Nothing
545 Nothing -> Nothing
549 Just chunk ->
546 Just chunk ->
550− let
547+ do
551− filtered = Array.filter predicate chunk
548+ let filtered = Array.filter predicate chunk
552− in
553 if Array.null filtered then Nothing else Just filtered
549 if Array.null filtered then Nothing else Just filtered
554− pure $ Loop $ Tuple filteredChunk (filterStrom predicate next)
550+ pure $ Loop $ (filteredChunk /\ (filterStrom predicate next))
555
551
556--------------------------------------------------------------------------------
552--------------------------------------------------------------------------------
557-- Selection
553-- Selection
@@ -573,18 +569,17 @@ takeStrom n stream = takeHelper n stream
573 numTaken = Array.length taken
569 numTaken = Array.length taken
574 if numTaken == 0 then pure $ Done Nothing
570 if numTaken == 0 then pure $ Done Nothing
575 else pure $ Done $ Just taken
571 else pure $ Done $ Just taken
576− Loop (Tuple maybeChunk next) ->
572+ Loop ((maybeChunk /\ next)) ->
577 case maybeChunk of
573 case maybeChunk of
578− Nothing -> pure $ Loop $ Tuple Nothing (takeHelper remaining next)
574+ Nothing -> pure $ Loop $ (Nothing /\ (takeHelper remaining next))
579 Just chunk -> do
575 Just chunk -> do
580 let
576 let
581− chunkLen = Array.length chunk
582 taken = Array.take remaining chunk
577 taken = Array.take remaining chunk
583 numTaken = Array.length taken
578 numTaken = Array.length taken
584 newRemaining = remaining - numTaken
579 newRemaining = remaining - numTaken
585 if numTaken == 0 then pure $ Done Nothing
580 if numTaken == 0 then pure $ Done Nothing
586 else if newRemaining <= 0 then pure $ Done $ Just taken
581 else if newRemaining <= 0 then pure $ Done $ Just taken
587− else pure $ Loop $ Tuple (Just taken) (takeHelper newRemaining next)
582+ else pure $ Loop $ ((Just taken) /\ (takeHelper newRemaining next))
588
583
589-- | Take while predicate is true
584-- | Take while predicate is true
590takeWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
585takeWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -596,7 +591,7 @@ takeWhileStrom predicate stream = mkStrom do
596 let taken = Array.takeWhile predicate chunk
591 let taken = Array.takeWhile predicate chunk
597 if Array.length taken < Array.length chunk then pure $ Done $ Just taken
592 if Array.length taken < Array.length chunk then pure $ Done $ Just taken
598 else pure $ Done $ Just taken
593 else pure $ Done $ Just taken
599− Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (takeWhileStrom predicate next)
594+ Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (takeWhileStrom predicate next))
600
595
601-- | Drop n elements (with proper state tracking across chunks)
596-- | Drop n elements (with proper state tracking across chunks)
602dropStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
597dropStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
@@ -613,19 +608,17 @@ dropStrom n stream
613 Done (Just chunk) -> do
608 Done (Just chunk) -> do
614 let dropped = Array.drop remaining chunk
609 let dropped = Array.drop remaining chunk
615 pure $ Done $ if Array.null dropped then Nothing else Just dropped
610 pure $ Done $ if Array.null dropped then Nothing else Just dropped
616− Loop (Tuple maybeChunk next) ->
611+ Loop ((maybeChunk /\ next)) ->
617 case maybeChunk of
612 case maybeChunk of
618− Nothing -> pure $ Loop $ Tuple Nothing (dropHelper remaining next)
613+ Nothing -> pure $ Loop $ (Nothing /\ (dropHelper remaining next))
619 Just chunk ->
614 Just chunk ->
620− let
615+ do
621− chunkLen = Array.length chunk
616+ let chunkLen = Array.length chunk
622− in
623− if remaining >= chunkLen then pure $ Loop $ Tuple Nothing (dropHelper (remaining - chunkLen) next)
617+ if remaining >= chunkLen then pure $ Loop $ (Nothing /\ (dropHelper (remaining - chunkLen) next))
624 else
618 else
625− let
619+ do
626− dropped = Array.drop remaining chunk
620+ let dropped = Array.drop remaining chunk
627− in
628− pure $ Loop $ Tuple (Just dropped) next
621+ pure $ Loop $ ((Just dropped) /\ next)
629
622
630-- | Take until predicate is true (includes the matching element)
623-- | Take until predicate is true (includes the matching element)
631takeUntilStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
624takeUntilStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -637,12 +630,12 @@ takeUntilStrom predicate stream = mkStrom do
637 case Array.findIndex predicate chunk of
630 case Array.findIndex predicate chunk of
638 Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Include the matching element
631 Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Include the matching element
639 Nothing -> pure $ Done $ Just chunk
632 Nothing -> pure $ Done $ Just chunk
640− Loop (Tuple maybeChunk next) ->
633+ Loop ((maybeChunk /\ next)) ->
641 case maybeChunk of
634 case maybeChunk of
642− Nothing -> pure $ Loop $ Tuple Nothing (takeUntilStrom predicate next)
635+ Nothing -> pure $ Loop $ (Nothing /\ (takeUntilStrom predicate next))
643 Just chunk -> case Array.findIndex predicate chunk of
636 Just chunk -> case Array.findIndex predicate chunk of
644 Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Found match, stop here
637 Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Found match, stop here
645− Nothing -> pure $ Loop $ Tuple (Just chunk) (takeUntilStrom predicate next) -- Keep going
638+ Nothing -> pure $ Loop $ ((Just chunk) /\ (takeUntilStrom predicate next)) -- Keep going
646
639
647-- | Drop while predicate is true
640-- | Drop while predicate is true
648dropWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
641dropWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -657,13 +650,13 @@ dropWhileStrom predicate stream = dropWhileHelper true stream
657 Done (Just chunk) -> do
650 Done (Just chunk) -> do
658 let dropped = Array.dropWhile predicate chunk
651 let dropped = Array.dropWhile predicate chunk
659 pure $ Done $ if Array.null dropped then Nothing else Just dropped
652 pure $ Done $ if Array.null dropped then Nothing else Just dropped
660− Loop (Tuple maybeChunk next) ->
653+ Loop ((maybeChunk /\ next)) ->
661 case maybeChunk of
654 case maybeChunk of
662− Nothing -> pure $ Loop $ Tuple Nothing (dropWhileHelper stillDropping next)
655+ Nothing -> pure $ Loop $ (Nothing /\ (dropWhileHelper stillDropping next))
663 Just chunk -> do
656 Just chunk -> do
664 let dropped = Array.dropWhile predicate chunk
657 let dropped = Array.dropWhile predicate chunk
665− if Array.null dropped then pure $ Loop $ Tuple Nothing (dropWhileHelper true next)
658+ if Array.null dropped then pure $ Loop $ (Nothing /\ (dropWhileHelper true next))
666− else pure $ Loop $ Tuple (Just dropped) next -- Found elements to keep, stop dropping
659+ else pure $ Loop $ ((Just dropped) /\ next) -- Found elements to keep, stop dropping
667
660
668-- | Filter with monadic predicate
661-- | Filter with monadic predicate
669filterMStrom :: forall ctx err a. (a -> Om ctx err Boolean) -> Strom ctx err a -> Strom ctx err a
662filterMStrom :: forall ctx err a. (a -> Om ctx err Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -674,13 +667,13 @@ filterMStrom predicate stream = mkStrom do
674 Done (Just chunk) -> do
667 Done (Just chunk) -> do
675 filtered <- Array.filterA predicate chunk
668 filtered <- Array.filterA predicate chunk
676 pure $ Done $ if Array.null filtered then Nothing else Just filtered
669 pure $ Done $ if Array.null filtered then Nothing else Just filtered
677− Loop (Tuple maybeChunk next) -> do
670+ Loop ((maybeChunk /\ next)) -> do
678 filteredChunk <- case maybeChunk of
671 filteredChunk <- case maybeChunk of
679 Nothing -> pure Nothing
672 Nothing -> pure Nothing
680 Just chunk -> do
673 Just chunk -> do
681 filtered <- Array.filterA predicate chunk
674 filtered <- Array.filterA predicate chunk
682 pure $ if Array.null filtered then Nothing else Just filtered
675 pure $ if Array.null filtered then Nothing else Just filtered
683− pure $ Loop $ Tuple filteredChunk (filterMStrom predicate next)
676+ pure $ Loop $ (filteredChunk /\ (filterMStrom predicate next))
684
677
685-- | Collect with partial function (mapMaybe)
678-- | Collect with partial function (mapMaybe)
686collectStrom :: forall ctx err a b. (a -> Maybe b) -> Strom ctx err a -> Strom ctx err b
679collectStrom :: forall ctx err a b. (a -> Maybe b) -> Strom ctx err a -> Strom ctx err b
@@ -691,17 +684,16 @@ collectStrom f stream = mkStrom do
691 Done (Just chunk) -> do
684 Done (Just chunk) -> do
692 let collected = Array.mapMaybe f chunk
685 let collected = Array.mapMaybe f chunk
693 pure $ Done $ if Array.null collected then Nothing else Just collected
686 pure $ Done $ if Array.null collected then Nothing else Just collected
694− Loop (Tuple maybeChunk next) ->
687+ Loop ((maybeChunk /\ next)) ->
695− let
688+ do
689+ let
696− collected = case maybeChunk of
690+ collected = case maybeChunk of
697− Nothing -> Nothing
691+ Nothing -> Nothing
698− Just chunk ->
692+ Just chunk -> do
699− let
700− c = Array.mapMaybe f chunk
693+ let c = Array.mapMaybe f chunk
701− in
702 if Array.null c then Nothing else Just c
694 if Array.null c then Nothing else Just c
703− in
695+
704− pure $ Loop $ Tuple collected (collectStrom f next)
696+ pure $ Loop $ (collected /\ (collectStrom f next))
705
697
706-- | Collect with monadic partial function
698-- | Collect with monadic partial function
707collectMStrom :: forall ctx err a b. (a -> Om ctx err (Maybe b)) -> Strom ctx err a -> Strom ctx err b
699collectMStrom :: forall ctx err a b. (a -> Om ctx err (Maybe b)) -> Strom ctx err a -> Strom ctx err b
@@ -713,16 +705,16 @@ collectMStrom f stream = mkStrom do
713 maybes <- traverse f chunk
705 maybes <- traverse f chunk
714 let collected = Array.mapMaybe identity maybes
706 let collected = Array.mapMaybe identity maybes
715 pure $ Done $ if Array.null collected then Nothing else Just collected
707 pure $ Done $ if Array.null collected then Nothing else Just collected
716− Loop (Tuple maybeChunk next) -> do
708+ Loop ((maybeChunk /\ next)) -> do
717 collected <- case maybeChunk of
709 collected <- case maybeChunk of
718 Nothing -> pure Nothing
710 Nothing -> pure Nothing
719 Just chunk -> do
711 Just chunk -> do
720 maybes <- traverse f chunk
712 maybes <- traverse f chunk
721 let c = Array.mapMaybe identity maybes
713 let c = Array.mapMaybe identity maybes
722 pure $ if Array.null c then Nothing else Just c
714 pure $ if Array.null c then Nothing else Just c
723− pure $ Loop $ Tuple collected (collectMStrom f next)
715+ pure $ Loop $ (collected /\ (collectMStrom f next))
724
716
725−-- | Remove consecutive duplicates
717+-- | Remove consecutive duplicates
726changesStrom :: forall ctx err a. Eq a => Strom ctx err a -> Strom ctx err a
718changesStrom :: forall ctx err a. Eq a => Strom ctx err a -> Strom ctx err a
727changesStrom stream = changesHelper Nothing stream
719changesStrom stream = changesHelper Nothing stream
728 where
720 where
@@ -732,28 +724,28 @@ changesStrom stream = changesHelper Nothing stream
732 Done Nothing -> pure $ Done Nothing
724 Done Nothing -> pure $ Done Nothing
733 Done (Just chunk) -> do
725 Done (Just chunk) -> do
734 let
726 let
735− Tuple _ result = Array.foldl
727+ (_ /\ result) = Array.foldl
736− ( \(Tuple prev acc) curr ->
728+ ( \((prev /\ acc)) curr ->
737− if Just curr == prev then Tuple (Just curr) acc
729+ if Just curr == prev then ((Just curr) /\ acc)
738− else Tuple (Just curr) (Array.snoc acc curr)
730+ else ((Just curr) /\ (Array.snoc acc curr))
739 )
731 )
740− (Tuple lastSeen [])
732+ (lastSeen /\ [])
741 chunk
733 chunk
742 pure $ Done $ if Array.null result then Nothing else Just result
734 pure $ Done $ if Array.null result then Nothing else Just result
743− Loop (Tuple maybeChunk next) ->
735+ Loop ((maybeChunk /\ next)) ->
744 case maybeChunk of
736 case maybeChunk of
745− Nothing -> pure $ Loop $ Tuple Nothing (changesHelper lastSeen next)
737+ Nothing -> pure $ Loop $ (Nothing /\ (changesHelper lastSeen next))
746 Just chunk -> do
738 Just chunk -> do
747 let
739 let
748− Tuple newLast result = Array.foldl
740+ (newLast /\ result) = Array.foldl
749− ( \(Tuple prev acc) curr ->
741+ ( \((prev /\ acc)) curr ->
750− if Just curr == prev then Tuple (Just curr) acc
742+ if Just curr == prev then ((Just curr) /\ acc)
751− else Tuple (Just curr) (Array.snoc acc curr)
743+ else ((Just curr) /\ (Array.snoc acc curr))
752 )
744 )
753− (Tuple lastSeen [])
745+ (lastSeen /\ [])
754 chunk
746 chunk
755− if Array.null result then pure $ Loop $ Tuple Nothing (changesHelper newLast next)
747+ if Array.null result then pure $ Loop $ (Nothing /\ (changesHelper newLast next))
756− else pure $ Loop $ Tuple (Just result) (changesHelper newLast next)
748+ else pure $ Loop $ ((Just result) /\ (changesHelper newLast next))
757
749
758--------------------------------------------------------------------------------
750--------------------------------------------------------------------------------
759-- Grouping
751-- Grouping
@@ -765,16 +757,16 @@ groupedStrom size stream
765 | size <= 0 = empty
757 | size <= 0 = empty
766 | otherwise = groupedHelper [] stream
758 | otherwise = groupedHelper [] stream
767 where
759 where
768− groupedHelper buffer s = mkStrom do
760+ groupedHelper buf s = mkStrom do
769 step <- runStrom s
761 step <- runStrom s
770 case step of
762 case step of
771 Done Nothing ->
763 Done Nothing ->
772 -- End of stream, emit remaining buffer if non-empty
764 -- End of stream, emit remaining buffer if non-empty
773− pure $ Done $ if Array.null buffer then Nothing else Just [ buffer ]
765+ pure $ Done $ if Array.null buf then Nothing else Just [ buf ]
774 Done (Just chunk) -> do
766 Done (Just chunk) -> do
775 -- Process final chunk with buffer
767 -- Process final chunk with buffer
776 let
768 let
777− combined = buffer <> chunk
769+ combined = buf <> chunk
778 groups = groupsOf size combined
770 groups = groupsOf size combined
779 completeGroups = Array.dropEnd 1 groups
771 completeGroups = Array.dropEnd 1 groups
780 lastGroup = case Array.last groups of
772 lastGroup = case Array.last groups of
@@ -785,28 +777,28 @@ groupedStrom size stream
785 Nothing -> []
777 Nothing -> []
786 if Array.null remainder then pure $ Done $ if Array.null (completeGroups <> lastGroup) then Nothing else Just (completeGroups <> lastGroup)
778 if Array.null remainder then pure $ Done $ if Array.null (completeGroups <> lastGroup) then Nothing else Just (completeGroups <> lastGroup)
787 else pure $ Done $ if Array.null (completeGroups <> [ remainder ]) then Nothing else Just (completeGroups <> [ remainder ])
779 else pure $ Done $ if Array.null (completeGroups <> [ remainder ]) then Nothing else Just (completeGroups <> [ remainder ])
788− Loop (Tuple maybeChunk next) ->
780+ Loop ((maybeChunk /\ next)) ->
789 case maybeChunk of
781 case maybeChunk of
790− Nothing -> pure $ Loop $ Tuple Nothing (groupedHelper buffer next)
782+ Nothing -> pure $ Loop $ (Nothing /\ (groupedHelper buf next))
791 Just chunk -> do
783 Just chunk -> do
792 let
784 let
793− combined = buffer <> chunk
785+ combined = buf <> chunk
794 groups = groupsOf size combined
786 groups = groupsOf size combined
795 completeGroups = if Array.null groups then [] else Array.dropEnd 1 groups
787 completeGroups = if Array.null groups then [] else Array.dropEnd 1 groups
796 newBuffer = case Array.last groups of
788 newBuffer = case Array.last groups of
797 Just g -> if Array.length g < size then g else []
789 Just g -> if Array.length g < size then g else []
798 Nothing -> []
790 Nothing -> []
799− if Array.null completeGroups then pure $ Loop $ Tuple Nothing (groupedHelper newBuffer next)
791+ if Array.null completeGroups then pure $ Loop $ (Nothing /\ (groupedHelper newBuffer next))
800− else pure $ Loop $ Tuple (Just completeGroups) (groupedHelper newBuffer next)
792+ else pure $ Loop $ ((Just completeGroups) /\ (groupedHelper newBuffer next))
801
793
802 groupsOf :: forall b. Int -> Array b -> Array (Array b)
794 groupsOf :: forall b. Int -> Array b -> Array (Array b)
803 groupsOf n arr
795 groupsOf n arr
804 | n <= 0 || Array.null arr = []
796 | n <= 0 || Array.null arr = []
805 | otherwise =
797 | otherwise =
798+ do
806− let
799+ let
807− group = Array.take n arr
800+ group = Array.take n arr
808− rest = Array.drop n arr
801+ rest = Array.drop n arr
809− in
810 if Array.null group then [] else Array.cons group (groupsOf n rest)
802 if Array.null group then [] else Array.cons group (groupsOf n rest)
811
803
812-- | Alias for groupedStrom
804-- | Alias for groupedStrom
@@ -818,74 +810,70 @@ chunkedStrom = groupedStrom
818groupByStrom :: forall ctx err a k. Eq k => (a -> k) -> Strom ctx err a -> Strom ctx err (Array a)
810groupByStrom :: forall ctx err a k. Eq k => (a -> k) -> Strom ctx err a -> Strom ctx err (Array a)
819groupByStrom keyFn stream = groupByHelper Nothing [] stream
811groupByStrom keyFn stream = groupByHelper Nothing [] stream
820 where
812 where
821− groupByHelper lastKey buffer s = mkStrom do
813+ groupByHelper lastKey buf s = mkStrom do
822 step <- runStrom s
814 step <- runStrom s
823 case step of
815 case step of
824 Done Nothing ->
816 Done Nothing ->
825− pure $ Done $ if Array.null buffer then Nothing else Just [ buffer ]
817+ pure $ Done $ if Array.null buf then Nothing else Just [ buf ]
826 Done (Just chunk) -> do
818 Done (Just chunk) -> do
827 let
819 let
828− Tuple _ result = Array.foldl
820+ (_ /\ result) = Array.foldl
829− ( \(Tuple currentKey acc) item ->
821+ ( \((currentKey /\ acc)) item ->
830− let
822+ do
831− itemKey = keyFn item
823+ let itemKey = keyFn item
832− in
833 case currentKey of
824 case currentKey of
834− Nothing -> Tuple (Just itemKey) { groups: [], buffer: [ item ] }
825+ Nothing -> ((Just itemKey) /\ { groups: [], buffer: [ item ] })
835 Just k ->
826 Just k ->
836− if k == itemKey then Tuple (Just itemKey) { groups: acc.groups, buffer: Array.snoc acc.buffer item }
827+ if k == itemKey then ((Just itemKey) /\ { groups: acc.groups, buffer: Array.snoc acc.buffer item })
837− else Tuple (Just itemKey) { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] }
828+ else ((Just itemKey) /\ { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] })
838 )
829 )
839− (Tuple lastKey { groups: [], buffer })
830+ (lastKey /\ { groups: [], buffer: buf })
840 chunk
831 chunk
841 allGroups =
832 allGroups =
842 if Array.null result.buffer then result.groups
833 if Array.null result.buffer then result.groups
843 else Array.snoc result.groups result.buffer
834 else Array.snoc result.groups result.buffer
844 pure $ Done $ if Array.null allGroups then Nothing else Just allGroups
835 pure $ Done $ if Array.null allGroups then Nothing else Just allGroups
845− Loop (Tuple maybeChunk next) ->
836+ Loop ((maybeChunk /\ next)) ->
846 case maybeChunk of
837 case maybeChunk of
847− Nothing -> pure $ Loop $ Tuple Nothing (groupByHelper lastKey buffer next)
838+ Nothing -> pure $ Loop $ (Nothing /\ (groupByHelper lastKey buf next))
848 Just chunk -> do
839 Just chunk -> do
849 let
840 let
850− Tuple newKey result = Array.foldl
841+ (newKey /\ result) = Array.foldl
851− ( \(Tuple currentKey acc) item ->
842+ ( \((currentKey /\ acc)) item ->
852− let
843+ do
853− itemKey = keyFn item
844+ let itemKey = keyFn item
854− in
855 case currentKey of
845 case currentKey of
856− Nothing -> Tuple (Just itemKey) { groups: [], buffer: [ item ] }
846+ Nothing -> ((Just itemKey) /\ { groups: [], buffer: [ item ] })
857 Just k ->
847 Just k ->
858− if k == itemKey then Tuple (Just itemKey) { groups: acc.groups, buffer: Array.snoc acc.buffer item }
848+ if k == itemKey then ((Just itemKey) /\ { groups: acc.groups, buffer: Array.snoc acc.buffer item })
859− else Tuple (Just itemKey) { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] }
849+ else ((Just itemKey) /\ { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] })
860 )
850 )
861− (Tuple lastKey { groups: [], buffer })
851+ (lastKey /\ { groups: [], buffer: buf })
862 chunk
852 chunk
863− if Array.null result.groups then pure $ Loop $ Tuple Nothing (groupByHelper newKey result.buffer next)
853+ if Array.null result.groups then pure $ Loop $ (Nothing /\ (groupByHelper newKey result.buffer next))
864− else pure $ Loop $ Tuple (Just result.groups) (groupByHelper newKey result.buffer next)
854+ else pure $ Loop $ ((Just result.groups) /\ (groupByHelper newKey result.buffer next))
865
855
866-- | Partition a stream by a predicate into two separate operations
856-- | Partition a stream by a predicate into two separate operations
867-- | Returns a tuple of (trues, falses)
857-- | Returns a tuple of (trues, falses)
868-- | Note: This requires running the stream twice, so it's not lazy
858-- | Note: This requires running the stream twice, so it's not lazy
869−partition :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Tuple (Strom ctx err a) (Strom ctx err a)
859+partition :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> ((Strom ctx err a) /\ (Strom ctx err a))
870partition predicate stream =
860partition predicate stream =
871− Tuple
872− (filterStrom predicate stream)
861+ (filterStrom predicate stream)
873− (filterStrom (not <<< predicate) stream)
862+ /\ (filterStrom (not <<< predicate) stream)
874
863
875-- | Partition and map in one pass
864-- | Partition and map in one pass
876−partitionMap :: forall ctx err a b c. (a -> Either b c) -> Strom ctx err a -> Tuple (Strom ctx err b) (Strom ctx err c)
865+partitionMap :: forall ctx err a b c. (a -> Either b c) -> Strom ctx err a -> ((Strom ctx err b) /\ (Strom ctx err c))
877partitionMap f stream =
866partitionMap f stream =
867+ do
878− let
868+ let
879− leftOnly x = case f x of
869+ leftOnly x = case f x of
880− Left b -> Just b
870+ Left b -> Just b
881− Right _ -> Nothing
871+ Right _ -> Nothing
882− rightOnly x = case f x of
872+ rightOnly x = case f x of
883− Left _ -> Nothing
873+ Left _ -> Nothing
884− Right c -> Just c
874+ Right c -> Just c
885− in
886− Tuple
887− (collectStrom leftOnly stream)
875+ (collectStrom leftOnly stream)
888− (collectStrom rightOnly stream)
876+ /\ (collectStrom rightOnly stream)
889
877
890--------------------------------------------------------------------------------
878--------------------------------------------------------------------------------
891-- Timing
879-- Timing
@@ -901,49 +889,24 @@ debounce duration stream = mkStrom do
901 Done (Just chunk) -> do
889 Done (Just chunk) -> do
902 Om.delay duration
890 Om.delay duration
903 pure $ Done $ Just chunk
891 pure $ Done $ Just chunk
904− Loop (Tuple maybeChunk next) -> do
892+ Loop (maybeChunk /\ next) -> do
905 case maybeChunk of
893 case maybeChunk of
906− Nothing -> pure $ Loop $ Tuple Nothing (debounce duration next)
894+ Nothing -> pure $ Loop $ (Nothing /\ (debounce duration next))
907 Just chunk -> do
895 Just chunk -> do
908 Om.delay duration
896 Om.delay duration
909− pure $ Loop $ Tuple (Just chunk) (debounce duration next)
897+ pure $ Loop $ ((Just chunk) /\ (debounce duration next))
910
898
899+-- | Debounce a stream per element - adds a delay after each individual element
900+debounceEach :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
901+debounceEach duration stream = stream # bindStrom \a -> mkStrom do
902+ Om.delay duration
903+ pure $ Done $ Just [ a ]
904+
911−-- | Throttle a stream - emit at most once per duration period
905+-- | Throttle a stream - emits at most one element per duration period
912−-- | Tracks last emission time and skips elements that arrive too quickly
913−-- | Note: Uses wall-clock time, not processing time
914throttle :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
906throttle :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
915−throttle (Milliseconds duration) stream = throttleWithState 0.0 stream
907+throttle duration stream = stream # bindStrom \a -> mkStrom do
916− where
917− throttleWithState lastEmit s = mkStrom do
918− step <- runStrom s
919− case step of
920− Done Nothing -> pure $ Done Nothing
921− Done (Just chunk) -> do
922− Tuple newLastEmit filtered <- filterByTime lastEmit chunk
923− pure $ Done $ if Array.null filtered then Nothing else Just filtered
924− Loop (Tuple maybeChunk next) -> do
925− case maybeChunk of
926− Nothing -> pure $ Loop $ Tuple Nothing (throttleWithState lastEmit next)
927− Just chunk -> do
928− Tuple newLastEmit filtered <- filterByTime lastEmit chunk
929− if Array.null filtered then pure $ Loop $ Tuple Nothing (throttleWithState newLastEmit next)
930− else pure $ Loop $ Tuple (Just filtered) (throttleWithState newLastEmit next)
931−
932− filterByTime lastEmit chunk = do
933− Tuple newLastEmit results <- Array.foldl
934− ( \acc value -> do
935− Tuple currentLast filtered <- acc
936− currentTime <- liftEffect now
937− let Milliseconds currentMillis = unInstant currentTime
938− let elapsed = currentMillis - currentLast
939− if elapsed >= duration then
908+ Om.delay duration
940− pure $ Tuple currentMillis (Array.snoc filtered value)
909+ pure $ Done $ Just [ a ]
941− else
942− pure $ Tuple currentLast filtered
943− )
944− (pure $ Tuple lastEmit [])
945− chunk
946− pure $ Tuple newLastEmit results
947
910
948-- | Delay each element by a duration
911-- | Delay each element by a duration
949delayStrom :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
912delayStrom :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
@@ -957,33 +920,33 @@ delayStrom duration stream = mkStrom do
957
920
958-- | Fold over all elements
921-- | Fold over all elements
959runFold :: forall ctx err a b. b -> (b -> a -> b) -> Strom ctx err a -> Om ctx err b
922runFold :: forall ctx err a b. b -> (b -> a -> b) -> Strom ctx err a -> Om ctx err b
960−runFold initial f stream = tailRecM go (Tuple initial stream)
923+runFold initial f stream = tailRecM go ((initial /\ stream))
961 where
924 where
962− go (Tuple acc s) = do
925+ go ((acc /\ s)) = do
963 step <- runStrom s
926 step <- runStrom s
964 case step of
927 case step of
965 Done Nothing -> pure $ Done acc
928 Done Nothing -> pure $ Done acc
966 Done (Just chunk) -> pure $ Done $ foldl f acc chunk
929 Done (Just chunk) -> pure $ Done $ foldl f acc chunk
967− Loop (Tuple maybeChunk next) ->
930+ Loop ((maybeChunk /\ next)) ->
968 -- Process chunk if present and continue
931 -- Process chunk if present and continue
969 case maybeChunk of
932 case maybeChunk of
970− Nothing -> pure $ Loop $ Tuple acc next
933+ Nothing -> pure $ Loop $ (acc /\ next)
971− Just chunk -> pure $ Loop $ Tuple (foldl f acc chunk) next
934+ Just chunk -> pure $ Loop $ ((foldl f acc chunk) /\ next)
972
935
973-- | Collect all elements into an array (O(n) using chunk accumulation)
936-- | Collect all elements into an array (O(n) using chunk accumulation)
974runCollect :: forall ctx err a. Strom ctx err a -> Om ctx err (Array a)
937runCollect :: forall ctx err a. Strom ctx err a -> Om ctx err (Array a)
975−runCollect stream = tailRecM go (Tuple List.Nil stream)
938+runCollect stream = tailRecM go (List.Nil /\ stream)
976 where
939 where
977− go (Tuple chunkList s) = do
940+ go ((chunkList /\ s)) = do
978 step <- runStrom s
941 step <- runStrom s
979 case step of
942 case step of
980 Done Nothing -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse chunkList
943 Done Nothing -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse chunkList
981 Done (Just chunk) -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse $ List.Cons chunk chunkList
944 Done (Just chunk) -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse $ List.Cons chunk chunkList
982− Loop (Tuple maybeChunk next) ->
945+ Loop ((maybeChunk /\ next)) ->
983 -- Emit the chunk (if present) and continue
946 -- Emit the chunk (if present) and continue
984 case maybeChunk of
947 case maybeChunk of
985− Nothing -> pure $ Loop $ Tuple chunkList next
948+ Nothing -> pure $ Loop $ (chunkList /\ next)
986− Just chunk -> pure $ Loop $ Tuple (List.Cons chunk chunkList) next
949+ Just chunk -> pure $ Loop $ ((List.Cons chunk chunkList) /\ next)
987
950
988-- | Run the stream and discard all elements
951-- | Run the stream and discard all elements
989runDrain :: forall ctx err a. Strom ctx err a -> Om ctx err Unit
952runDrain :: forall ctx err a. Strom ctx err a -> Om ctx err Unit
@@ -1000,7 +963,7 @@ traverseStrom_ f stream = tailRecM go stream
1000 Done (Just chunk) -> do
963 Done (Just chunk) -> do
1001 _ <- traverse f chunk
964 _ <- traverse f chunk
1002 pure $ Done unit
965 pure $ Done unit
1003− Loop (Tuple maybeChunk next) -> do
966+ Loop ((maybeChunk /\ next)) -> do
1004 -- Process chunk if present, then continue
967 -- Process chunk if present, then continue
1005 case maybeChunk of
968 case maybeChunk of
1006 Nothing -> pure unit
969 Nothing -> pure unit
@@ -1024,20 +987,20 @@ traverseMStrom_ = traverseStrom_
1024--------------------------------------------------------------------------------
987--------------------------------------------------------------------------------
1025
988
1026-- | Run a stream in the background, returning a Fiber that can be cancelled
989-- | Run a stream in the background, returning a Fiber that can be cancelled
1027−-- |
990+-- |
1028-- | To use cancellable streams, convert your stream to Om and use Om.launchOm:
991-- | To use cancellable streams, convert your stream to Om and use Om.launchOm:
1029−-- |
992+-- |
1030-- | Example:
993-- | Example:
1031-- | ```purescript
994-- | ```purescript
1032-- | fiber <- Om.launchOm ctx handlers (Strom.runCollect myStream)
995-- | fiber <- Om.launchOm ctx handlers (Strom.runCollect myStream)
1033-- | -- ... later ...
996-- | -- ... later ...
1034-- | killFiber (error "Cancelled") fiber
997-- | killFiber (error "Cancelled") fiber
1035-- | ```
998-- | ```
1036−-- |
999+-- |
1037-- | Or for subscription-style:
1000-- | Or for subscription-style:
1038-- | ```purescript
1001-- | ```purescript
1039-- | fiber <- Om.launchOm ctx handlers (Strom.traverseStrom_ callback myStream)
1002-- | fiber <- Om.launchOm ctx handlers (Strom.traverseStrom_ callback myStream)
1040−-- | -- ... later ...
1003+-- | -- ... later ...
1041-- | killFiber (error "Unsubscribed") fiber
1004-- | killFiber (error "Unsubscribed") fiber
1042-- | ```
1005-- | ```
1043-- |
1006-- |
@@ -1060,8 +1023,8 @@ appendStrom toAppend baseStream = mkStrom do
1060 step <- runStrom baseStream
1023 step <- runStrom baseStream
1061 case step of
1024 case step of
1062 Done Nothing -> runStrom toAppend
1025 Done Nothing -> runStrom toAppend
1063− Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) toAppend -- Emit chunk and continue with toAppend
1026+ Done (Just chunk) -> pure $ Loop $ ((Just chunk) /\ toAppend) -- Emit chunk and continue with toAppend
1064− Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (appendStrom toAppend next)
1027+ Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (appendStrom toAppend next))
1065
1028
1066-- | Concatenate an array of streams
1029-- | Concatenate an array of streams
1067-- | Uses foldr with flip to match appendStrom's pipeline-friendly parameter order
1030-- | Uses foldr with flip to match appendStrom's pipeline-friendly parameter order
@@ -1069,8 +1032,8 @@ concatStrom :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1069concatStrom streams = Array.foldr (flip appendStrom) empty streams
1032concatStrom streams = Array.foldr (flip appendStrom) empty streams
1070
1033
1071-- | Zip two streams together
1034-- | Zip two streams together
1072−zipStrom :: forall ctx err a b. Strom ctx err a -> Strom ctx err b -> Strom ctx err (Tuple a b)
1035+zipStrom :: forall ctx err a b. Strom ctx err a -> Strom ctx err b -> Strom ctx err ((a /\ b))
1073−zipStrom = zipWithStrom Tuple
1036+zipStrom = zipWithStrom (/\)
1074
1037
1075-- | Zip two streams with a function
1038-- | Zip two streams with a function
1076zipWithStrom :: forall ctx err a b c. (a -> b -> c) -> Strom ctx err a -> Strom ctx err b -> Strom ctx err c
1039zipWithStrom :: forall ctx err a b c. (a -> b -> c) -> Strom ctx err a -> Strom ctx err b -> Strom ctx err c
@@ -1083,13 +1046,13 @@ zipWithStrom f s1 s2 = mkStrom do
1083 pure $ Done $ Just zipped
1046 pure $ Done $ Just zipped
1084 Done Nothing, _ -> pure $ Done Nothing
1047 Done Nothing, _ -> pure $ Done Nothing
1085 _, Done Nothing -> pure $ Done Nothing
1048 _, Done Nothing -> pure $ Done Nothing
1086− Loop (Tuple maybeChunk1 next1), Loop (Tuple maybeChunk2 next2) ->
1049+ Loop ((maybeChunk1 /\ next1)), Loop ((maybeChunk2 /\ next2)) ->
1087 -- Both looping: zip chunks if both present, continue with both nexts
1050 -- Both looping: zip chunks if both present, continue with both nexts
1088 case maybeChunk1, maybeChunk2 of
1051 case maybeChunk1, maybeChunk2 of
1089− Just chunk1, Just chunk2 -> pure $ Loop $ Tuple (Just $ Array.zipWith f chunk1 chunk2) (zipWithStrom f next1 next2)
1052+ Just chunk1, Just chunk2 -> pure $ Loop $ ((Just $ Array.zipWith f chunk1 chunk2) /\ (zipWithStrom f next1 next2))
1090− _, _ -> pure $ Loop $ Tuple Nothing (zipWithStrom f next1 next2)
1053+ _, _ -> pure $ Loop $ (Nothing /\ (zipWithStrom f next1 next2))
1091− Loop (Tuple _ next1), _ -> pure $ Loop $ Tuple Nothing (zipWithStrom f next1 s2)
1054+ Loop ((_ /\ next1)), _ -> pure $ Loop $ (Nothing /\ (zipWithStrom f next1 s2))
1092− _, Loop (Tuple _ next2) -> pure $ Loop $ Tuple Nothing (zipWithStrom f s1 next2)
1055+ _, Loop ((_ /\ next2)) -> pure $ Loop $ (Nothing /\ (zipWithStrom f s1 next2))
1093
1056
1094-- | Interleave two streams - alternates between elements from each stream
1057-- | Interleave two streams - alternates between elements from each stream
1095-- | Similar to merge but deterministic - always alternates
1058-- | Similar to merge but deterministic - always alternates
@@ -1101,16 +1064,16 @@ interleave s1 s2 = interleaveHelper true s1 s2
1101 step1 <- runStrom first
1064 step1 <- runStrom first
1102 case step1 of
1065 case step1 of
1103 Done Nothing -> runStrom second
1066 Done Nothing -> runStrom second
1104− Done (Just chunk1) -> pure $ Loop $ Tuple (Just chunk1) (interleaveHelper false second first)
1067+ Done (Just chunk1) -> pure $ Loop $ ((Just chunk1) /\ (interleaveHelper false second first))
1105− Loop (Tuple maybeChunk1 next1) ->
1068+ Loop ((maybeChunk1 /\ next1)) ->
1106− pure $ Loop $ Tuple maybeChunk1 (interleaveHelper false second next1)
1069+ pure $ Loop $ (maybeChunk1 /\ (interleaveHelper false second next1))
1107 else do
1070 else do
1108 step2 <- runStrom second
1071 step2 <- runStrom second
1109 case step2 of
1072 case step2 of
1110 Done Nothing -> runStrom first
1073 Done Nothing -> runStrom first
1111− Done (Just chunk2) -> pure $ Loop $ Tuple (Just chunk2) (interleaveHelper true first second)
1074+ Done (Just chunk2) -> pure $ Loop $ ((Just chunk2) /\ (interleaveHelper true first second))
1112− Loop (Tuple maybeChunk2 next2) ->
1075+ Loop ((maybeChunk2 /\ next2)) ->
1113− pure $ Loop $ Tuple maybeChunk2 (interleaveHelper true first next2)
1076+ pure $ Loop $ (maybeChunk2 /\ (interleaveHelper true first next2))
1114
1077
1115-- | Intersperse a separator element between each element of the stream
1078-- | Intersperse a separator element between each element of the stream
1116intersperse :: forall ctx err a. a -> Strom ctx err a -> Strom ctx err a
1079intersperse :: forall ctx err a. a -> Strom ctx err a -> Strom ctx err a
@@ -1126,15 +1089,15 @@ intersperse separator stream = intersperseHelper true stream
1126 if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
1089 if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
1127 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1090 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1128 pure $ Done $ Just withSeparators
1091 pure $ Done $ Just withSeparators
1129− Loop (Tuple maybeChunk next) ->
1092+ Loop ((maybeChunk /\ next)) ->
1130 case maybeChunk of
1093 case maybeChunk of
1131− Nothing -> pure $ Loop $ Tuple Nothing (intersperseHelper isFirst next)
1094+ Nothing -> pure $ Loop $ (Nothing /\ (intersperseHelper isFirst next))
1132 Just chunk -> do
1095 Just chunk -> do
1133 let
1096 let
1134 withSeparators =
1097 withSeparators =
1135 if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
1098 if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
1136 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1099 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1137− pure $ Loop $ Tuple (Just withSeparators) (intersperseHelper false next)
1100+ pure $ Loop $ ((Just withSeparators) /\ (intersperseHelper false next))
1138
1101
1139--------------------------------------------------------------------------------
1102--------------------------------------------------------------------------------
1140-- Buffering
1103-- Buffering
@@ -1175,7 +1138,7 @@ bufferChunks capacity stream
1175 Nothing -> do
1138 Nothing -> do
1176 AVar.put unit mutex
1139 AVar.put unit mutex
1177 dequeue
1140 dequeue
1178− Just (Tuple head tail) -> do
1141+ Just ((head /\ tail)) -> do
1179 liftEffect $ Ref.write tail bufferRef
1142 liftEffect $ Ref.write tail bufferRef
1180 liftEffect $ Ref.write (size - 1) sizeRef
1143 liftEffect $ Ref.write (size - 1) sizeRef
1181 let hasMore = (size - 1) > 0
1144 let hasMore = (size - 1) > 0
@@ -1193,7 +1156,7 @@ bufferChunks capacity stream
1193 Done (Just chunk) -> do
1156 Done (Just chunk) -> do
1194 enqueue (BufferChunk chunk)
1157 enqueue (BufferChunk chunk)
1195 enqueue BufferDone
1158 enqueue BufferDone
1196− Loop (Tuple maybeChunk next) -> do
1159+ Loop ((maybeChunk /\ next)) -> do
1197 case maybeChunk of
1160 case maybeChunk of
1198 Just chunk -> enqueue (BufferChunk chunk)
1161 Just chunk -> enqueue (BufferChunk chunk)
1199 Nothing -> pure unit
1162 Nothing -> pure unit
@@ -1209,7 +1172,7 @@ bufferChunks capacity stream
1209 killFiber (Exception.error "done") fiber
1172 killFiber (Exception.error "done") fiber
1210 pure $ Done Nothing
1173 pure $ Done Nothing
1211 BufferChunk chunk ->
1174 BufferChunk chunk ->
1212− pure $ Loop $ Tuple (Just chunk) consumer
1175+ pure $ Loop $ ((Just chunk) /\ consumer)
1213
1176
1214 stepResult <- Om.runReader ctx (runStrom consumer)
1177 stepResult <- Om.runReader ctx (runStrom consumer)
1215 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1178 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1249,7 +1212,7 @@ buffer capacity stream
1249 Nothing -> do
1212 Nothing -> do
1250 AVar.put unit mutex
1213 AVar.put unit mutex
1251 dequeue
1214 dequeue
1252− Just (Tuple head tail) -> do
1215+ Just ((head /\ tail)) -> do
1253 liftEffect $ Ref.write tail bufferRef
1216 liftEffect $ Ref.write tail bufferRef
1254 liftEffect $ Ref.write (size - 1) sizeRef
1217 liftEffect $ Ref.write (size - 1) sizeRef
1255 let hasMore = (size - 1) > 0
1218 let hasMore = (size - 1) > 0
@@ -1267,7 +1230,7 @@ buffer capacity stream
1267 Done (Just chunk) -> do
1230 Done (Just chunk) -> do
1268 traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1231 traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1269 enqueue BufferDone
1232 enqueue BufferDone
1270− Loop (Tuple maybeChunk next) -> do
1233+ Loop ((maybeChunk /\ next)) -> do
1271 case maybeChunk of
1234 case maybeChunk of
1272 Just chunk -> traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1235 Just chunk -> traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1273 Nothing -> pure unit
1236 Nothing -> pure unit
@@ -1283,7 +1246,7 @@ buffer capacity stream
1283 killFiber (Exception.error "done") fiber
1246 killFiber (Exception.error "done") fiber
1284 pure $ Done Nothing
1247 pure $ Done Nothing
1285 BufferChunk chunk ->
1248 BufferChunk chunk ->
1286− pure $ Loop $ Tuple (Just chunk) consumer
1249+ pure $ Loop $ ((Just chunk) /\ consumer)
1287
1250
1288 stepResult <- Om.runReader ctx (runStrom consumer)
1251 stepResult <- Om.runReader ctx (runStrom consumer)
1289 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1252 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1392,7 +1355,7 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
1392 Nothing -> do
1355 Nothing -> do
1393 AVar.put unit mutex
1356 AVar.put unit mutex
1394 dequeue
1357 dequeue
1395− Just (Tuple head tail) -> do
1358+ Just ((head /\ tail)) -> do
1396 liftEffect $ Ref.write tail bufferRef
1359 liftEffect $ Ref.write tail bufferRef
1397 liftEffect $ Ref.write (size - 1) sizeRef
1360 liftEffect $ Ref.write (size - 1) sizeRef
1398 let hasMore = (size - 1) > 0
1361 let hasMore = (size - 1) > 0
@@ -1407,14 +1370,14 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
1407 let
1370 let
1408 producer s = do
1371 producer s = do
1409 stepResult <- Om.runReader ctx (runStrom s)
1372 stepResult <- Om.runReader ctx (runStrom s)
1410− step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1373+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1411 case step of
1374 case step of
1412 Done Nothing ->
1375 Done Nothing ->
1413 enqueue (StreamDone StreamId1)
1376 enqueue (StreamDone StreamId1)
1414 Done (Just chunk) -> do
1377 Done (Just chunk) -> do
1415 enqueue (DataChunk chunk StreamId1)
1378 enqueue (DataChunk chunk StreamId1)
1416 enqueue (StreamDone StreamId1)
1379 enqueue (StreamDone StreamId1)
1417− Loop (Tuple maybeChunk next) -> do
1380+ Loop ((maybeChunk /\ next)) -> do
1418 case maybeChunk of
1381 case maybeChunk of
1419 Just chunk -> enqueue (DataChunk chunk StreamId1)
1382 Just chunk -> enqueue (DataChunk chunk StreamId1)
1420 Nothing -> pure unit
1383 Nothing -> pure unit
@@ -1426,14 +1389,14 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
1426 let
1389 let
1427 producer s = do
1390 producer s = do
1428 stepResult <- Om.runReader ctx (runStrom s)
1391 stepResult <- Om.runReader ctx (runStrom s)
1429− step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1392+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1430 case step of
1393 case step of
1431 Done Nothing ->
1394 Done Nothing ->
1432 enqueue (StreamDone StreamId2)
1395 enqueue (StreamDone StreamId2)
1433 Done (Just chunk) -> do
1396 Done (Just chunk) -> do
1434 enqueue (DataChunk chunk StreamId2)
1397 enqueue (DataChunk chunk StreamId2)
1435 enqueue (StreamDone StreamId2)
1398 enqueue (StreamDone StreamId2)
1436− Loop (Tuple maybeChunk next) -> do
1399+ Loop ((maybeChunk /\ next)) -> do
1437 case maybeChunk of
1400 case maybeChunk of
1438 Just chunk -> enqueue (DataChunk chunk StreamId2)
1401 Just chunk -> enqueue (DataChunk chunk StreamId2)
1439 Nothing -> pure unit
1402 Nothing -> pure unit
@@ -1460,7 +1423,7 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
1460 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1423 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1461 DataChunk chunk _streamId ->
1424 DataChunk chunk _streamId ->
1462 -- Got a chunk
1425 -- Got a chunk
1463− pure $ Loop $ Tuple (Just chunk) (consumer doneLeft doneRight)
1426+ pure $ Loop $ ((Just chunk) /\ (consumer doneLeft doneRight))
1464
1427
1465 stepResult <- Om.runReader ctx (runStrom $ consumer false false)
1428 stepResult <- Om.runReader ctx (runStrom $ consumer false false)
1466 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1429 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1505,7 +1468,7 @@ mergeAllNDWith bufferCapacity streams
1505 Nothing -> do
1468 Nothing -> do
1506 AVar.put unit mutex
1469 AVar.put unit mutex
1507 dequeue
1470 dequeue
1508− Just (Tuple head tail) -> do
1471+ Just ((head /\ tail)) -> do
1509 liftEffect $ Ref.write tail bufferRef
1472 liftEffect $ Ref.write tail bufferRef
1510 liftEffect $ Ref.write (size - 1) sizeRef
1473 liftEffect $ Ref.write (size - 1) sizeRef
1511 let hasMore = (size - 1) > 0
1474 let hasMore = (size - 1) > 0
@@ -1523,7 +1486,7 @@ mergeAllNDWith bufferCapacity streams
1523 Done (Just chunk) -> do
1486 Done (Just chunk) -> do
1524 enqueue (MergeChunk chunk)
1487 enqueue (MergeChunk chunk)
1525 enqueue MergeDone
1488 enqueue MergeDone
1526− Loop (Tuple maybeChunk next) -> do
1489+ Loop ((maybeChunk /\ next)) -> do
1527 case maybeChunk of
1490 case maybeChunk of
1528 Just chunk -> enqueue (MergeChunk chunk)
1491 Just chunk -> enqueue (MergeChunk chunk)
1529 Nothing -> pure unit
1492 Nothing -> pure unit
@@ -1544,7 +1507,7 @@ mergeAllNDWith bufferCapacity streams
1544 stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
1507 stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
1545 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1508 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1546 MergeChunk chunk ->
1509 MergeChunk chunk ->
1547− pure $ Loop $ Tuple (Just chunk) (consumer doneCount)
1510+ pure $ Loop $ ((Just chunk) /\ (consumer doneCount))
1548
1511
1549 stepResult <- Om.runReader ctx (runStrom $ consumer 0)
1512 stepResult <- Om.runReader ctx (runStrom $ consumer 0)
1550 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1513 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1568,23 +1531,23 @@ mapPar concurrency f stream
1568 let groups = chunksOf concurrency chunk
1531 let groups = chunksOf concurrency chunk
1569 results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
1532 results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
1570 pure $ Done $ Just $ Array.concat results
1533 pure $ Done $ Just $ Array.concat results
1571− Loop (Tuple maybeChunk next) -> do
1534+ Loop ((maybeChunk /\ next)) -> do
1572 mappedChunk <- case maybeChunk of
1535 mappedChunk <- case maybeChunk of
1573 Nothing -> pure Nothing
1536 Nothing -> pure Nothing
1574 Just chunk -> do
1537 Just chunk -> do
1575 let groups = chunksOf concurrency chunk
1538 let groups = chunksOf concurrency chunk
1576 results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
1539 results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
1577 pure $ Just $ Array.concat results
1540 pure $ Just $ Array.concat results
1578− pure $ Loop $ Tuple mappedChunk (mapPar concurrency f next)
1541+ pure $ Loop $ (mappedChunk /\ (mapPar concurrency f next))
1579 where
1542 where
1580 chunksOf :: forall x. Int -> Array x -> Array (Array x)
1543 chunksOf :: forall x. Int -> Array x -> Array (Array x)
1581 chunksOf n arr
1544 chunksOf n arr
1582 | n <= 0 || Array.null arr = []
1545 | n <= 0 || Array.null arr = []
1583 | otherwise =
1546 | otherwise =
1547+ do
1584− let
1548+ let
1585− group = Array.take n arr
1549+ group = Array.take n arr
1586− rest = Array.drop n arr
1550+ rest = Array.drop n arr
1587− in
1588 if Array.null group then [] else Array.cons group (chunksOf n rest)
1551 if Array.null group then [] else Array.cons group (chunksOf n rest)
1589
1552
1590-- | Unordered parallel map (currently same as mapPar)
1553-- | Unordered parallel map (currently same as mapPar)
@@ -1720,7 +1683,7 @@ instance Functor (Strom ctx err) where
1720 case step of
1683 case step of
1721 Done Nothing -> pure $ Done Nothing
1684 Done Nothing -> pure $ Done Nothing
1722 Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
1685 Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
1723− Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple (Functor.map (Functor.map f) maybeChunk) (Functor.map f next)
1686+ Loop ((maybeChunk /\ next)) -> pure $ Loop $ ((Functor.map (Functor.map f) maybeChunk) /\ (Functor.map f next))
1724 )
1687 )
1725
1688
1726instance Apply (Strom ctx err) where
1689instance Apply (Strom ctx err) where
@@ -1729,29 +1692,29 @@ instance Apply (Strom ctx err) where
1729 runStrom as >>= \stepA ->
1692 runStrom as >>= \stepA ->
1730 case stepF, stepA of
1693 case stepF, stepA of
1731 Done (Just funcs), Done (Just vals) ->
1694 Done (Just funcs), Done (Just vals) ->
1695+ do
1732− let
1696+ let
1733− results = do
1697+ results = do
1734− f <- funcs
1698+ f <- funcs
1735− a <- vals
1699+ a <- vals
1736− pure (f a)
1700+ pure (f a)
1737− in
1738 pure $ Done $ Just results
1701 pure $ Done $ Just results
1739 Done Nothing, _ -> pure $ Done Nothing
1702 Done Nothing, _ -> pure $ Done Nothing
1740 _, Done Nothing -> pure $ Done Nothing
1703 _, Done Nothing -> pure $ Done Nothing
1741− Loop (Tuple maybeChunkF nextF), Loop (Tuple maybeChunkA nextA) ->
1704+ Loop ((maybeChunkF /\ nextF)), Loop ((maybeChunkA /\ nextA)) ->
1742 -- Apply chunks if both present
1705 -- Apply chunks if both present
1743 case maybeChunkF, maybeChunkA of
1706 case maybeChunkF, maybeChunkA of
1744 Just chunkF, Just chunkA ->
1707 Just chunkF, Just chunkA ->
1708+ do
1745− let
1709+ let
1746− applied = do
1710+ applied = do
1747− f <- chunkF
1711+ f <- chunkF
1748− a <- chunkA
1712+ a <- chunkA
1749− pure (f a)
1713+ pure (f a)
1750− in
1751− pure $ Loop $ Tuple (Just applied) (nextF <*> nextA)
1714+ pure $ Loop $ ((Just applied) /\ (nextF <*> nextA))
1752− _, _ -> pure $ Loop $ Tuple Nothing (nextF <*> nextA)
1715+ _, _ -> pure $ Loop $ (Nothing /\ (nextF <*> nextA))
1753− Loop (Tuple _ nextF), _ -> pure $ Loop $ Tuple Nothing (nextF <*> as)
1716+ Loop ((_ /\ nextF)), _ -> pure $ Loop $ (Nothing /\ (nextF <*> as))
1754− _, Loop (Tuple _ nextA) -> pure $ Loop $ Tuple Nothing (fs <*> nextA)
1717+ _, Loop ((_ /\ nextA)) -> pure $ Loop $ (Nothing /\ (fs <*> nextA))
1755 )
1718 )
1756
1719
1757instance Applicative (Strom ctx err) where
1720instance Applicative (Strom ctx err) where
@@ -1763,19 +1726,18 @@ instance Bind (Strom ctx err) where
1763 case step of
1726 case step of
1764 Done Nothing -> pure $ Done Nothing
1727 Done Nothing -> pure $ Done Nothing
1765 Done (Just chunk) ->
1728 Done (Just chunk) ->
1766− let
1729+ do
1767− streams = Functor.map f chunk
1730+ let streams = Functor.map f chunk
1768− in
1769 runStrom $ concatStrom streams
1731 runStrom $ concatStrom streams
1770− Loop (Tuple maybeChunk next) ->
1732+ Loop ((maybeChunk /\ next)) ->
1771 -- When looping, if we have a chunk, we need to process it through f and concat
1733 -- When looping, if we have a chunk, we need to process it through f and concat
1772 case maybeChunk of
1734 case maybeChunk of
1773− Nothing -> pure $ Loop $ Tuple Nothing (next >>= f)
1735+ Nothing -> pure $ Loop $ (Nothing /\ (next >>= f))
1774 Just chunk ->
1736 Just chunk ->
1737+ do
1775− let
1738+ let
1776− streams = Functor.map f chunk
1739+ streams = Functor.map f chunk
1777− prefixStream = concatStrom streams
1740+ prefixStream = concatStrom streams
1778− in
1779 runStrom $ appendStrom prefixStream (next >>= f)
1741 runStrom $ appendStrom prefixStream (next >>= f)
1780 )
1742 )
1781
1743
@@ -1786,8 +1748,8 @@ instance Semigroup (Strom ctx err a) where
1786 ( runStrom s1 >>= \step ->
1748 ( runStrom s1 >>= \step ->
1787 case step of
1749 case step of
1788 Done Nothing -> runStrom s2
1750 Done Nothing -> runStrom s2
1789− Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) s2
1751+ Done (Just chunk) -> pure $ Loop $ ((Just chunk) /\ s2)
1790− Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (next <> s2)
1752+ Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (next <> s2))
1791 )
1753 )
1792
1754
1793instance Monoid (Strom ctx err a) where
1755instance Monoid (Strom ctx err a) where
@@ -1798,8 +1760,8 @@ instance Alt (Strom ctx err) where
1798 ( runStrom s1 >>= \step ->
1760 ( runStrom s1 >>= \step ->
1799 case step of
1761 case step of
1800 Done Nothing -> runStrom s2
1762 Done Nothing -> runStrom s2
1801− Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) s2
1763+ Done (Just chunk) -> pure $ Loop $ ((Just chunk) /\ s2)
1802− Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (next <|> s2)
1764+ Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (next <|> s2))
1803 )
1765 )
1804
1766
1805instance Plus (Strom ctx err) where
1767instance Plus (Strom ctx err) where
src/Yoga/Om/Strom.purs.backupnew file+1809
@@ -0,0 +1,1809 @@
1+-- @inline export mkStrom arity=1
2+-- @inline export runStrom arity=1
3+-- @inline export mapStrom arity=2
4+-- @inline export filterStrom arity=2
5+-- @inline export takeStrom arity=2
6+-- @inline export runFold arity=3
7+-- @inline export runDrain arity=1
8+-- @inline export rangeStrom arity=2
9+-- @inline export scanStrom arity=3
10+-- @inline export zipWithStrom arity=3
11+-- @inline export mapPar arity=3
12+-- @inline export bindStrom arity=2
13+module Yoga.Om.Strom
14+ ( Strom
15+ -- Construction
16+ , empty
17+ , succeed
18+ , fail
19+ -- , die -- TODO: Fix type constraints
20+ , fromAff
21+ , fromOm
22+ , fromArray
23+ , fromFoldable
24+ , rangeStrom
25+ , iterateStrom
26+ , iterateStromInfinite
27+ , repeatStrom
28+ , repeatStromInfinite
29+ , repeatOmStrom
30+ , repeatOmStromInfinite
31+ , unfoldStrom
32+ , unfoldOmStrom
33+ -- Running
34+ , runCollect
35+ , runDrain
36+ , runFold
37+ , traverseStrom_
38+ , forStrom_
39+ , forMStrom_
40+ , traverseMStrom_
41+ -- Transformations
42+ , mapStrom
43+ , mapMStrom
44+ , bindStrom
45+ , scanStrom
46+ , mapAccumStrom
47+ , tapStrom
48+ , tapMStrom
49+ , filterStrom
50+ -- Selection
51+ , takeStrom
52+ , takeWhileStrom
53+ , takeUntilStrom
54+ , dropStrom
55+ , dropWhileStrom
56+ , filterMStrom
57+ , collectStrom
58+ , collectMStrom
59+ , changesStrom
60+ -- Grouping
61+ , groupedStrom
62+ , chunkedStrom
63+ , groupByStrom
64+ , partition
65+ , partitionMap
66+ -- Timing
67+ , debounce
68+ , throttle
69+ , delayStrom
70+ -- Combining
71+ , appendStrom
72+ , concatStrom
73+ , zipStrom
74+ , zipWithStrom
75+ , interleave
76+ , intersperse
77+ , HaltStrategy(..)
78+ , merge
79+ , mergeAll
80+ , mergeWith
81+ , mergeHaltEither
82+ , mergeHaltLeft
83+ , mergeHaltRight
84+ , mergeND
85+ , mergeNDWith
86+ , mergeAllND
87+ , mergeAllNDWith
88+ , race
89+ , raceAll
90+ -- Parallel Processing
91+ , mapPar
92+ , mapMPar
93+ , foreachPar
94+ , mapParUnordered
95+ , mapMParUnordered
96+ , foreachParUnordered
97+ -- Buffering
98+ , buffer
99+ , bufferChunks
100+ -- Error Handling
101+ , catchAll
102+ -- , catchSome -- TODO: Fix type constraints
103+ , retry
104+ , retryN
105+ , timeout
106+ , ensuring
107+ -- Resource Safety
108+ , bracket
109+ , bracketExit
110+ , acquireRelease
111+ ) where
112+
113+import Prelude
114+
115+import Control.Alt (class Alt, (<|>))
116+import Control.Alternative (class Alternative)
117+import Control.Monad.Error.Class (throwError)
118+import Control.Monad.Rec.Class (Step(..), tailRecM)
119+import Control.Monad.ST as ST
120+import Control.Monad.ST.Ref as STRef
121+import Control.Plus (class Plus)
122+import Data.Array as Array
123+import Data.Array.ST as STArray
124+import Data.Array.ST.Iterator as Iterator
125+import Data.Either (Either(..), either)
126+import Data.Foldable (class Foldable, foldl)
127+import Data.Functor (map) as Functor
128+import Data.List as List
129+import Data.Maybe (Maybe(..))
130+import Data.Time.Duration (Milliseconds(..))
131+import Data.Traversable (traverse, traverse_)
132+import Data.Tuple (Tuple(..))
133+import Data.Tuple.Nested (type (/\), (/\))
134+import Data.Variant (Variant)
135+import Effect.Aff (Aff, Error, delay, forkAff, joinFiber, killFiber)
136+import Effect.Aff.AVar as AVar
137+import Effect.Aff.Class (liftAff)
138+import Effect.Exception as Exception
139+import Effect.Now (now)
140+import Effect.Ref as Ref
141+import Effect.Class (liftEffect)
142+import Data.CatQueue as CatQueue
143+import Data.CatQueue (CatQueue)
144+import Data.DateTime.Instant (unInstant)
145+import Yoga.Om (Om)
146+import Yoga.Om as Om
147+
148+--------------------------------------------------------------------------------
149+-- Core Types
150+--------------------------------------------------------------------------------
151+
152+-- | A Strom is a stream of values that can:
153+-- | - Emit zero or more values of type `a`
154+-- | - Fail with errors tracked in the `err` row
155+-- | - Require a context `ctx`
156+-- | - Perform effects in Om
157+-- |
158+-- | Think of it as: `Om ctx err (Array a)` but lazy and chunked
159+-- |
160+-- | The Step type is interpreted as:
161+-- | - Loop (chunk, next) => emit chunk (if present) and continue with next
162+-- | - Done finalChunk => emit final chunk (if present) and stop
163+newtype Strom ctx err a = Strom
164+ { pull :: Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a)))
165+ }
166+
167+-- | Chunks for efficient batch processing
168+type Chunk a = Array a
169+
170+-- | Helper to create a Strom from a pull function
171+mkStrom :: forall ctx err a. Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a))) -> Strom ctx err a
172+mkStrom pull = Strom { pull }
173+
174+-- | Helper to unwrap a Strom
175+runStrom :: forall ctx err a. Strom ctx err a -> Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a)))
176+runStrom (Strom s) = s.pull
177+
178+--------------------------------------------------------------------------------
179+-- Non-Deterministic Merge Types
180+--------------------------------------------------------------------------------
181+
182+-- | Stream identifier for tracking which stream produced data
183+data StreamId = StreamId1 | StreamId2
184+
185+derive instance eqStreamId :: Eq StreamId
186+derive instance ordStreamId :: Ord StreamId
187+
188+-- | Messages passed through the coordination queue
189+data QueueItem a
190+ = DataChunk (Array a) StreamId
191+ | StreamDone StreamId
192+
193+derive instance functorQueueItem :: Functor QueueItem
194+
195+-- | Messages for mergeAllND buffered queue
196+data MergeAllItem a
197+ = MergeChunk (Array a)
198+ | MergeDone
199+
200+derive instance functorMergeAllItem :: Functor MergeAllItem
201+
202+-- | Merge termination strategy (like ZStream HaltStrategy)
203+data HaltStrategy
204+ = HaltBoth
205+ | HaltEither
206+ | HaltLeft
207+ | HaltRight
208+
209+derive instance eqHaltStrategy :: Eq HaltStrategy
210+
211+-- | Messages for buffering queues
212+data BufferItem a
213+ = BufferChunk (Array a)
214+ | BufferDone
215+
216+derive instance functorBufferItem :: Functor BufferItem
217+
218+--------------------------------------------------------------------------------
219+-- Construction
220+--------------------------------------------------------------------------------
221+
222+-- | An empty stream
223+empty :: forall ctx err a. Strom ctx err a
224+empty = mkStrom $ pure $ Done Nothing
225+
226+-- | A stream with a single element
227+succeed :: forall ctx err a. a -> Strom ctx err a
228+succeed a = mkStrom $ pure $ Done $ Just [ a ]
229+
230+-- | A stream that fails immediately with an empty result (no error thrown)
231+fail :: forall ctx err a. Strom ctx err a
232+fail = empty
233+
234+-- TODO: Add die function with proper type constraints
235+-- die :: forall ctx err errors a. SingletonVariantRecord err (exception :: Error | errors) => Record err -> Strom ctx (exception :: Error | errors) a
236+-- die error = mkStrom $ Om.throw error
237+
238+-- | Create a stream from an Aff computation
239+fromAff :: forall ctx err a. Aff a -> Strom ctx err a
240+fromAff aff = mkStrom do
241+ value <- liftAff aff
242+ pure $ Done $ Just [ value ]
243+
244+-- | Create a stream from an Om computation
245+fromOm :: forall ctx err a. Om ctx err a -> Strom ctx err a
246+fromOm om = mkStrom do
247+ value <- om
248+ pure $ Done $ Just [ value ]
249+
250+-- | Create a stream from an array
251+fromArray :: forall ctx err a. Array a -> Strom ctx err a
252+fromArray arr =
253+ if Array.null arr then mkStrom $ pure $ Done Nothing
254+ else mkStrom $ pure $ Done $ Just arr
255+
256+-- | Create a stream from any foldable
257+fromFoldable :: forall ctx err f a. Foldable f => f a -> Strom ctx err a
258+fromFoldable = fromArray <<< Array.fromFoldable
259+
260+-- | Create a stream of integers in a range [start, end)
261+rangeStrom :: forall ctx err. Int -> Int -> Strom ctx err Int
262+rangeStrom start end = rangeHelper start end
263+ where
264+ rangeHelper current limit
265+ | current >= limit = empty
266+ | otherwise = mkStrom do
267+ let
268+ chunkSize = 1000
269+ chunkEnd = min (current + chunkSize) limit
270+ chunk = Array.range current (chunkEnd - 1)
271+ if chunkEnd >= limit then pure $ Done $ Just chunk -- Final chunk
272+ else pure $ Loop $ Tuple (Just chunk) (rangeHelper chunkEnd limit) -- Emit chunk and continue
273+
274+-- | Iterate producing values - limited to ~10,000 elements for stack safety
275+-- | For truly infinite streams, use `iterateStromInfinite` (with Aff overhead)
276+iterateStrom :: forall ctx err a. (a -> a) -> a -> Strom ctx err a
277+iterateStrom f initial = fromArray $ ST.run do
278+ arr <- STArray.unsafeThaw []
279+ valRef <- STRef.new initial
280+ iter <- Iterator.iterator (\i -> if i < 10000 then Just i else Nothing)
281+ Iterator.iterate iter \_ -> do
282+ v <- STRef.read valRef
283+ void $ STArray.push v arr
284+ void $ STRef.modify f valRef
285+ STArray.unsafeFreeze arr
286+
287+-- | Truly infinite iteration - stack-safe via Aff async boundaries
288+-- | Adds tiny delay (0ms) at each chunk to reset the stack
289+-- | Use this when you need more than 10k elements
290+iterateStromInfinite :: forall ctx err a. (a -> a) -> a -> Strom ctx err a
291+iterateStromInfinite f initial = iterateHelper initial
292+ where
293+ iterateHelper current = mkStrom do
294+ -- Async boundary resets the stack!
295+ liftAff $ delay (Milliseconds 0.0)
296+ let
297+ chunkSize = 10000
298+ -- Build chunk using STArray with Iterator (stack-safe!)
299+ result = ST.run do
300+ arr <- STArray.unsafeThaw []
301+ valRef <- STRef.new current
302+ iter <- Iterator.iterator (\i -> if i < chunkSize then Just i else Nothing)
303+ Iterator.iterate iter \_ -> do
304+ val <- STRef.read valRef
305+ void $ STArray.push val arr
306+ void $ STRef.modify f valRef
307+ finalVal <- STRef.read valRef
308+ frozen <- STArray.unsafeFreeze arr
309+ pure { arr: frozen, nextVal: finalVal }
310+ pure $ Loop $ Tuple (Just result.arr) (iterateHelper result.nextVal)
311+
312+-- | Repeat a value - limited to ~10,000 elements for stack safety
313+repeatStrom :: forall ctx err a. a -> Strom ctx err a
314+repeatStrom a = fromArray (Array.replicate 10000 a)
315+
316+-- | Truly infinite repeat - stack-safe via Aff async boundaries
317+-- | Adds tiny delay (0ms) at each chunk to reset the stack
318+repeatStromInfinite :: forall ctx err a. a -> Strom ctx err a
319+repeatStromInfinite a = repeatHelper unit
320+ where
321+ repeatHelper _ = mkStrom do
322+ -- Async boundary resets the stack!
323+ liftAff $ delay (Milliseconds 0.0)
324+ let chunk = Array.replicate 10000 a
325+ pure $ Loop $ Tuple (Just chunk) (repeatHelper unit)
326+
327+-- | Repeat an Om computation - limited to 100 iterations for stack safety
328+-- | For truly infinite repetition, use `repeatOmStromInfinite` (with Aff overhead)
329+repeatOmStrom :: forall ctx err a. Om ctx err a -> Strom ctx err a
330+repeatOmStrom om = repeatOmHelper 0
331+ where
332+ maxCount = 100
333+ repeatOmHelper count
334+ | count >= maxCount = empty
335+ | otherwise = mkStrom do
336+ value <- om
337+ pure $ Loop $ Tuple (Just [ value ]) (repeatOmHelper (count + 1))
338+
339+-- | Truly infinite Om repetition - stack-safe via Aff async boundaries
340+-- | Adds tiny delay (0ms) at each pull to reset the stack
341+repeatOmStromInfinite :: forall ctx err a. Om ctx err a -> Strom ctx err a
342+repeatOmStromInfinite om = repeatOmHelper unit
343+ where
344+ repeatOmHelper _ = mkStrom do
345+ -- Async boundary resets the stack!
346+ liftAff $ delay (Milliseconds 0.0)
347+ value <- om
348+ pure $ Loop $ Tuple (Just [ value ]) (repeatOmHelper unit)
349+
350+-- | Unfold a stream from a seed value - can be infinite if f never returns Nothing
351+-- | Emits chunks lazily, terminates when f returns Nothing
352+unfoldStrom :: forall ctx err a b. (b -> Maybe (Tuple a b)) -> b -> Strom ctx err a
353+unfoldStrom f seed = unfoldHelper seed
354+ where
355+ unfoldHelper currentSeed = mkStrom do
356+ let chunk = buildChunk currentSeed 10000
357+ case chunk.finalSeed of
358+ Nothing -> pure $ Done $ if Array.null chunk.values then Nothing else Just chunk.values
359+ Just nextSeed ->
360+ if Array.null chunk.values then runStrom (unfoldHelper nextSeed)
361+ else pure $ Loop $ Tuple (Just chunk.values) (unfoldHelper nextSeed)
362+
363+ buildChunk seed' size = ST.run do
364+ arr <- STArray.unsafeThaw []
365+ seedRef <- STRef.new seed'
366+ stoppedRef <- STRef.new false
367+ iter <- Iterator.iterator (\i -> if i < size then Just i else Nothing)
368+ Iterator.iterate iter \_ -> do
369+ stopped <- STRef.read stoppedRef
370+ when (not stopped) do
371+ s <- STRef.read seedRef
372+ case f s of
373+ Nothing -> void $ STRef.write true stoppedRef
374+ Just (Tuple value nextS) -> do
375+ void $ STArray.push value arr
376+ void $ STRef.write nextS seedRef
377+ frozen <- STArray.unsafeFreeze arr
378+ stopped <- STRef.read stoppedRef
379+ finalSeed <- STRef.read seedRef
380+ pure { values: frozen, finalSeed: if stopped then Nothing else Just finalSeed }
381+
382+-- | Unfold a stream with an effectful function
383+unfoldOmStrom :: forall ctx err a b. (b -> Om ctx err (Maybe (Tuple a b))) -> b -> Strom ctx err a
384+unfoldOmStrom f seed = mkStrom go
385+ where
386+ go = do
387+ result <- f seed
388+ case result of
389+ Nothing -> pure $ Done Nothing
390+ Just (Tuple value nextSeed) -> pure $ Loop $ Tuple (Just [ value ]) (unfoldOmStrom f nextSeed)
391+
392+--------------------------------------------------------------------------------
393+-- Transformations
394+--------------------------------------------------------------------------------
395+
396+-- | Map over elements
397+mapStrom :: forall ctx err a b. (a -> b) -> Strom ctx err a -> Strom ctx err b
398+mapStrom f stream = mkStrom do
399+ step <- runStrom stream
400+ case step of
401+ Done Nothing -> pure $ Done Nothing
402+ Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
403+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple (Functor.map (Functor.map f) maybeChunk) (mapStrom f next)
404+
405+-- | Map with a monadic effect
406+mapMStrom :: forall ctx err a b. (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
407+mapMStrom f stream = mkStrom do
408+ step <- runStrom stream
409+ case step of
410+ Done Nothing -> pure $ Done Nothing
411+ Done (Just chunk) -> do
412+ mapped <- traverse f chunk
413+ pure $ Done $ Just mapped
414+ Loop (Tuple maybeChunk next) -> do
415+ mappedChunk <- case maybeChunk of
416+ Nothing -> pure Nothing
417+ Just chunk -> Just <$> traverse f chunk
418+ pure $ Loop $ Tuple mappedChunk (mapMStrom f next)
419+
420+-- | Tap (observe without modifying)
421+tapStrom :: forall ctx err a. (a -> Unit) -> Strom ctx err a -> Strom ctx err a
422+tapStrom f stream = mkStrom do
423+ step <- runStrom stream
424+ case step of
425+ Done Nothing -> pure $ Done Nothing
426+ Done (Just chunk) -> do
427+ let _ = Functor.map f chunk
428+ pure $ Done $ Just chunk
429+ Loop (Tuple maybeChunk next) -> do
430+ let
431+ tappedChunk = case maybeChunk of
432+ Nothing -> Nothing
433+ Just chunk ->
434+ let
435+ _ = Functor.map f chunk
436+ in
437+ Just chunk
438+ pure $ Loop $ Tuple tappedChunk (tapStrom f next)
439+
440+-- | Tap with monadic effect (observe with effects)
441+tapMStrom :: forall ctx err a. (a -> Om ctx err Unit) -> Strom ctx err a -> Strom ctx err a
442+tapMStrom f stream = mkStrom do
443+ step <- runStrom stream
444+ case step of
445+ Done Nothing -> pure $ Done Nothing
446+ Done (Just chunk) -> do
447+ _ <- traverse f chunk
448+ pure $ Done $ Just chunk
449+ Loop (Tuple maybeChunk next) -> do
450+ tappedChunk <- case maybeChunk of
451+ Nothing -> pure Nothing
452+ Just chunk -> do
453+ _ <- traverse f chunk
454+ pure $ Just chunk
455+ pure $ Loop $ Tuple tappedChunk (tapMStrom f next)
456+
457+-- | Bind for streams (monadic flatMap/chain)
458+-- | Use >>= operator for cleaner syntax: `stream >>= f`
459+bindStrom :: forall ctx err a b. (a -> Strom ctx err b) -> Strom ctx err a -> Strom ctx err b
460+bindStrom f stream = stream >>= f
461+
462+-- | Scan with accumulator (like foldl but emits intermediate results)
463+scanStrom :: forall ctx err a b. (b -> a -> b) -> b -> Strom ctx err a -> Strom ctx err b
464+scanStrom f initial stream = scanHelper initial stream
465+ where
466+ scanHelper acc s = mkStrom do
467+ step <- runStrom s
468+ case step of
469+ Done Nothing -> pure $ Done Nothing
470+ Done (Just chunk) -> do
471+ -- Build results by folding and collecting intermediate values
472+ let
473+ Tuple _ results = Array.foldl
474+ ( \(Tuple prevAcc accList) item ->
475+ let
476+ newAcc = f prevAcc item
477+ in
478+ Tuple newAcc (Array.snoc accList newAcc)
479+ )
480+ (Tuple acc [])
481+ chunk
482+ pure $ Done $ if Array.null results then Nothing else Just results
483+ Loop (Tuple maybeChunk next) ->
484+ case maybeChunk of
485+ Nothing -> pure $ Loop $ Tuple Nothing (scanHelper acc next)
486+ Just chunk -> do
487+ let
488+ Tuple newAcc results = Array.foldl
489+ ( \(Tuple prevAcc accList) item ->
490+ let
491+ currAcc = f prevAcc item
492+ in
493+ Tuple currAcc (Array.snoc accList currAcc)
494+ )
495+ (Tuple acc [])
496+ chunk
497+ if Array.null results then pure $ Loop $ Tuple Nothing (scanHelper newAcc next)
498+ else pure $ Loop $ Tuple (Just results) (scanHelper newAcc next)
499+
500+-- | Map with accumulator (stateful map)
501+mapAccumStrom :: forall ctx err a b s. (s -> a -> Tuple s b) -> s -> Strom ctx err a -> Strom ctx err b
502+mapAccumStrom f initial stream = mapAccumHelper initial stream
503+ where
504+ mapAccumHelper state s = mkStrom do
505+ step <- runStrom s
506+ case step of
507+ Done Nothing -> pure $ Done Nothing
508+ Done (Just chunk) -> do
509+ let
510+ Tuple finalState results = Array.foldl
511+ ( \(Tuple st acc) a ->
512+ let
513+ Tuple newSt b = f st a
514+ in
515+ Tuple newSt (Array.snoc acc b)
516+ )
517+ (Tuple state [])
518+ chunk
519+ pure $ Done $ if Array.null results then Nothing else Just results
520+ Loop (Tuple maybeChunk next) ->
521+ case maybeChunk of
522+ Nothing -> pure $ Loop $ Tuple Nothing (mapAccumHelper state next)
523+ Just chunk -> do
524+ let
525+ Tuple newState results = Array.foldl
526+ ( \(Tuple st acc) a ->
527+ let
528+ Tuple newSt b = f st a
529+ in
530+ Tuple newSt (Array.snoc acc b)
531+ )
532+ (Tuple state [])
533+ chunk
534+ if Array.null results then pure $ Loop $ Tuple Nothing (mapAccumHelper newState next)
535+ else pure $ Loop $ Tuple (Just results) (mapAccumHelper newState next)
536+
537+-- | Filter elements
538+filterStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
539+filterStrom predicate stream = mkStrom do
540+ step <- runStrom stream
541+ case step of
542+ Done Nothing -> pure $ Done Nothing
543+ Done (Just chunk) -> do
544+ let filtered = Array.filter predicate chunk
545+ pure $ Done $ if Array.null filtered then Nothing else Just filtered
546+ Loop (Tuple maybeChunk next) -> do
547+ let
548+ filteredChunk = case maybeChunk of
549+ Nothing -> Nothing
550+ Just chunk ->
551+ let
552+ filtered = Array.filter predicate chunk
553+ in
554+ if Array.null filtered then Nothing else Just filtered
555+ pure $ Loop $ Tuple filteredChunk (filterStrom predicate next)
556+
557+--------------------------------------------------------------------------------
558+-- Selection
559+--------------------------------------------------------------------------------
560+
561+-- | Take n elements (with proper state tracking across chunks)
562+takeStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
563+takeStrom n stream = takeHelper n stream
564+ where
565+ takeHelper remaining s
566+ | remaining <= 0 = empty
567+ | otherwise = mkStrom do
568+ step <- runStrom s
569+ case step of
570+ Done Nothing -> pure $ Done Nothing
571+ Done (Just chunk) -> do
572+ let
573+ taken = Array.take remaining chunk
574+ numTaken = Array.length taken
575+ if numTaken == 0 then pure $ Done Nothing
576+ else pure $ Done $ Just taken
577+ Loop (Tuple maybeChunk next) ->
578+ case maybeChunk of
579+ Nothing -> pure $ Loop $ Tuple Nothing (takeHelper remaining next)
580+ Just chunk -> do
581+ let
582+ chunkLen = Array.length chunk
583+ taken = Array.take remaining chunk
584+ numTaken = Array.length taken
585+ newRemaining = remaining - numTaken
586+ if numTaken == 0 then pure $ Done Nothing
587+ else if newRemaining <= 0 then pure $ Done $ Just taken
588+ else pure $ Loop $ Tuple (Just taken) (takeHelper newRemaining next)
589+
590+-- | Take while predicate is true
591+takeWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
592+takeWhileStrom predicate stream = mkStrom do
593+ step <- runStrom stream
594+ case step of
595+ Done Nothing -> pure $ Done Nothing
596+ Done (Just chunk) -> do
597+ let taken = Array.takeWhile predicate chunk
598+ if Array.length taken < Array.length chunk then pure $ Done $ Just taken
599+ else pure $ Done $ Just taken
600+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (takeWhileStrom predicate next)
601+
602+-- | Drop n elements (with proper state tracking across chunks)
603+dropStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
604+dropStrom n stream
605+ | n <= 0 = stream
606+ | otherwise = dropHelper n stream
607+ where
608+ dropHelper remaining s
609+ | remaining <= 0 = s
610+ | otherwise = mkStrom do
611+ step <- runStrom s
612+ case step of
613+ Done Nothing -> pure $ Done Nothing
614+ Done (Just chunk) -> do
615+ let dropped = Array.drop remaining chunk
616+ pure $ Done $ if Array.null dropped then Nothing else Just dropped
617+ Loop (Tuple maybeChunk next) ->
618+ case maybeChunk of
619+ Nothing -> pure $ Loop $ Tuple Nothing (dropHelper remaining next)
620+ Just chunk ->
621+ let
622+ chunkLen = Array.length chunk
623+ in
624+ if remaining >= chunkLen then pure $ Loop $ Tuple Nothing (dropHelper (remaining - chunkLen) next)
625+ else
626+ let
627+ dropped = Array.drop remaining chunk
628+ in
629+ pure $ Loop $ Tuple (Just dropped) next
630+
631+-- | Take until predicate is true (includes the matching element)
632+takeUntilStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
633+takeUntilStrom predicate stream = mkStrom do
634+ step <- runStrom stream
635+ case step of
636+ Done Nothing -> pure $ Done Nothing
637+ Done (Just chunk) -> do
638+ case Array.findIndex predicate chunk of
639+ Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Include the matching element
640+ Nothing -> pure $ Done $ Just chunk
641+ Loop (Tuple maybeChunk next) ->
642+ case maybeChunk of
643+ Nothing -> pure $ Loop $ Tuple Nothing (takeUntilStrom predicate next)
644+ Just chunk -> case Array.findIndex predicate chunk of
645+ Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Found match, stop here
646+ Nothing -> pure $ Loop $ Tuple (Just chunk) (takeUntilStrom predicate next) -- Keep going
647+
648+-- | Drop while predicate is true
649+dropWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
650+dropWhileStrom predicate stream = dropWhileHelper true stream
651+ where
652+ dropWhileHelper stillDropping s
653+ | not stillDropping = s
654+ | otherwise = mkStrom do
655+ step <- runStrom s
656+ case step of
657+ Done Nothing -> pure $ Done Nothing
658+ Done (Just chunk) -> do
659+ let dropped = Array.dropWhile predicate chunk
660+ pure $ Done $ if Array.null dropped then Nothing else Just dropped
661+ Loop (Tuple maybeChunk next) ->
662+ case maybeChunk of
663+ Nothing -> pure $ Loop $ Tuple Nothing (dropWhileHelper stillDropping next)
664+ Just chunk -> do
665+ let dropped = Array.dropWhile predicate chunk
666+ if Array.null dropped then pure $ Loop $ Tuple Nothing (dropWhileHelper true next)
667+ else pure $ Loop $ Tuple (Just dropped) next -- Found elements to keep, stop dropping
668+
669+-- | Filter with monadic predicate
670+filterMStrom :: forall ctx err a. (a -> Om ctx err Boolean) -> Strom ctx err a -> Strom ctx err a
671+filterMStrom predicate stream = mkStrom do
672+ step <- runStrom stream
673+ case step of
674+ Done Nothing -> pure $ Done Nothing
675+ Done (Just chunk) -> do
676+ filtered <- Array.filterA predicate chunk
677+ pure $ Done $ if Array.null filtered then Nothing else Just filtered
678+ Loop (Tuple maybeChunk next) -> do
679+ filteredChunk <- case maybeChunk of
680+ Nothing -> pure Nothing
681+ Just chunk -> do
682+ filtered <- Array.filterA predicate chunk
683+ pure $ if Array.null filtered then Nothing else Just filtered
684+ pure $ Loop $ Tuple filteredChunk (filterMStrom predicate next)
685+
686+-- | Collect with partial function (mapMaybe)
687+collectStrom :: forall ctx err a b. (a -> Maybe b) -> Strom ctx err a -> Strom ctx err b
688+collectStrom f stream = mkStrom do
689+ step <- runStrom stream
690+ case step of
691+ Done Nothing -> pure $ Done Nothing
692+ Done (Just chunk) -> do
693+ let collected = Array.mapMaybe f chunk
694+ pure $ Done $ if Array.null collected then Nothing else Just collected
695+ Loop (Tuple maybeChunk next) ->
696+ let
697+ collected = case maybeChunk of
698+ Nothing -> Nothing
699+ Just chunk ->
700+ let
701+ c = Array.mapMaybe f chunk
702+ in
703+ if Array.null c then Nothing else Just c
704+ in
705+ pure $ Loop $ Tuple collected (collectStrom f next)
706+
707+-- | Collect with monadic partial function
708+collectMStrom :: forall ctx err a b. (a -> Om ctx err (Maybe b)) -> Strom ctx err a -> Strom ctx err b
709+collectMStrom f stream = mkStrom do
710+ step <- runStrom stream
711+ case step of
712+ Done Nothing -> pure $ Done Nothing
713+ Done (Just chunk) -> do
714+ maybes <- traverse f chunk
715+ let collected = Array.mapMaybe identity maybes
716+ pure $ Done $ if Array.null collected then Nothing else Just collected
717+ Loop (Tuple maybeChunk next) -> do
718+ collected <- case maybeChunk of
719+ Nothing -> pure Nothing
720+ Just chunk -> do
721+ maybes <- traverse f chunk
722+ let c = Array.mapMaybe identity maybes
723+ pure $ if Array.null c then Nothing else Just c
724+ pure $ Loop $ Tuple collected (collectMStrom f next)
725+
726+-- | Remove consecutive duplicates
727+changesStrom :: forall ctx err a. Eq a => Strom ctx err a -> Strom ctx err a
728+changesStrom stream = changesHelper Nothing stream
729+ where
730+ changesHelper lastSeen s = mkStrom do
731+ step <- runStrom s
732+ case step of
733+ Done Nothing -> pure $ Done Nothing
734+ Done (Just chunk) -> do
735+ let
736+ Tuple _ result = Array.foldl
737+ ( \(Tuple prev acc) curr ->
738+ if Just curr == prev then Tuple (Just curr) acc
739+ else Tuple (Just curr) (Array.snoc acc curr)
740+ )
741+ (Tuple lastSeen [])
742+ chunk
743+ pure $ Done $ if Array.null result then Nothing else Just result
744+ Loop (Tuple maybeChunk next) ->
745+ case maybeChunk of
746+ Nothing -> pure $ Loop $ Tuple Nothing (changesHelper lastSeen next)
747+ Just chunk -> do
748+ let
749+ Tuple newLast result = Array.foldl
750+ ( \(Tuple prev acc) curr ->
751+ if Just curr == prev then Tuple (Just curr) acc
752+ else Tuple (Just curr) (Array.snoc acc curr)
753+ )
754+ (Tuple lastSeen [])
755+ chunk
756+ if Array.null result then pure $ Loop $ Tuple Nothing (changesHelper newLast next)
757+ else pure $ Loop $ Tuple (Just result) (changesHelper newLast next)
758+
759+--------------------------------------------------------------------------------
760+-- Grouping
761+--------------------------------------------------------------------------------
762+
763+-- | Group elements into fixed-size arrays
764+groupedStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err (Array a)
765+groupedStrom size stream
766+ | size <= 0 = empty
767+ | otherwise = groupedHelper [] stream
768+ where
769+ groupedHelper buffer s = mkStrom do
770+ step <- runStrom s
771+ case step of
772+ Done Nothing ->
773+ -- End of stream, emit remaining buffer if non-empty
774+ pure $ Done $ if Array.null buffer then Nothing else Just [ buffer ]
775+ Done (Just chunk) -> do
776+ -- Process final chunk with buffer
777+ let
778+ combined = buffer <> chunk
779+ groups = groupsOf size combined
780+ completeGroups = Array.dropEnd 1 groups
781+ lastGroup = case Array.last groups of
782+ Just g -> if Array.length g == size then [ g ] else []
783+ Nothing -> []
784+ remainder = case Array.last groups of
785+ Just g -> if Array.length g < size then g else []
786+ Nothing -> []
787+ if Array.null remainder then pure $ Done $ if Array.null (completeGroups <> lastGroup) then Nothing else Just (completeGroups <> lastGroup)
788+ else pure $ Done $ if Array.null (completeGroups <> [ remainder ]) then Nothing else Just (completeGroups <> [ remainder ])
789+ Loop (Tuple maybeChunk next) ->
790+ case maybeChunk of
791+ Nothing -> pure $ Loop $ Tuple Nothing (groupedHelper buffer next)
792+ Just chunk -> do
793+ let
794+ combined = buffer <> chunk
795+ groups = groupsOf size combined
796+ completeGroups = if Array.null groups then [] else Array.dropEnd 1 groups
797+ newBuffer = case Array.last groups of
798+ Just g -> if Array.length g < size then g else []
799+ Nothing -> []
800+ if Array.null completeGroups then pure $ Loop $ Tuple Nothing (groupedHelper newBuffer next)
801+ else pure $ Loop $ Tuple (Just completeGroups) (groupedHelper newBuffer next)
802+
803+ groupsOf :: forall b. Int -> Array b -> Array (Array b)
804+ groupsOf n arr
805+ | n <= 0 || Array.null arr = []
806+ | otherwise =
807+ let
808+ group = Array.take n arr
809+ rest = Array.drop n arr
810+ in
811+ if Array.null group then [] else Array.cons group (groupsOf n rest)
812+
813+-- | Alias for groupedStrom
814+chunkedStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err (Array a)
815+chunkedStrom = groupedStrom
816+
817+-- | Group consecutive elements by a key function
818+-- | Emits groups when the key changes
819+groupByStrom :: forall ctx err a k. Eq k => (a -> k) -> Strom ctx err a -> Strom ctx err (Array a)
820+groupByStrom keyFn stream = groupByHelper Nothing [] stream
821+ where
822+ groupByHelper lastKey buffer s = mkStrom do
823+ step <- runStrom s
824+ case step of
825+ Done Nothing ->
826+ pure $ Done $ if Array.null buffer then Nothing else Just [ buffer ]
827+ Done (Just chunk) -> do
828+ let
829+ Tuple _ result = Array.foldl
830+ ( \(Tuple currentKey acc) item ->
831+ let
832+ itemKey = keyFn item
833+ in
834+ case currentKey of
835+ Nothing -> Tuple (Just itemKey) { groups: [], buffer: [ item ] }
836+ Just k ->
837+ if k == itemKey then Tuple (Just itemKey) { groups: acc.groups, buffer: Array.snoc acc.buffer item }
838+ else Tuple (Just itemKey) { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] }
839+ )
840+ (Tuple lastKey { groups: [], buffer })
841+ chunk
842+ allGroups =
843+ if Array.null result.buffer then result.groups
844+ else Array.snoc result.groups result.buffer
845+ pure $ Done $ if Array.null allGroups then Nothing else Just allGroups
846+ Loop (Tuple maybeChunk next) ->
847+ case maybeChunk of
848+ Nothing -> pure $ Loop $ Tuple Nothing (groupByHelper lastKey buffer next)
849+ Just chunk -> do
850+ let
851+ Tuple newKey result = Array.foldl
852+ ( \(Tuple currentKey acc) item ->
853+ let
854+ itemKey = keyFn item
855+ in
856+ case currentKey of
857+ Nothing -> Tuple (Just itemKey) { groups: [], buffer: [ item ] }
858+ Just k ->
859+ if k == itemKey then Tuple (Just itemKey) { groups: acc.groups, buffer: Array.snoc acc.buffer item }
860+ else Tuple (Just itemKey) { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] }
861+ )
862+ (Tuple lastKey { groups: [], buffer })
863+ chunk
864+ if Array.null result.groups then pure $ Loop $ Tuple Nothing (groupByHelper newKey result.buffer next)
865+ else pure $ Loop $ Tuple (Just result.groups) (groupByHelper newKey result.buffer next)
866+
867+-- | Partition a stream by a predicate into two separate operations
868+-- | Returns a tuple of (trues, falses)
869+-- | Note: This requires running the stream twice, so it's not lazy
870+partition :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Tuple (Strom ctx err a) (Strom ctx err a)
871+partition predicate stream =
872+ Tuple
873+ (filterStrom predicate stream)
874+ (filterStrom (not <<< predicate) stream)
875+
876+-- | Partition and map in one pass
877+partitionMap :: forall ctx err a b c. (a -> Either b c) -> Strom ctx err a -> Tuple (Strom ctx err b) (Strom ctx err c)
878+partitionMap f stream =
879+ let
880+ leftOnly x = case f x of
881+ Left b -> Just b
882+ Right _ -> Nothing
883+ rightOnly x = case f x of
884+ Left _ -> Nothing
885+ Right c -> Just c
886+ in
887+ Tuple
888+ (collectStrom leftOnly stream)
889+ (collectStrom rightOnly stream)
890+
891+--------------------------------------------------------------------------------
892+-- Timing
893+--------------------------------------------------------------------------------
894+
895+-- | Debounce a stream - adds a delay after each element
896+-- | Useful for rate-limiting or giving downstream time to process
897+debounce :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
898+debounce duration stream = mkStrom do
899+ step <- runStrom stream
900+ case step of
901+ Done Nothing -> pure $ Done Nothing
902+ Done (Just chunk) -> do
903+ Om.delay duration
904+ pure $ Done $ Just chunk
905+ Loop (Tuple maybeChunk next) -> do
906+ case maybeChunk of
907+ Nothing -> pure $ Loop $ Tuple Nothing (debounce duration next)
908+ Just chunk -> do
909+ Om.delay duration
910+ pure $ Loop $ Tuple (Just chunk) (debounce duration next)
911+
912+-- | Throttle a stream - emit at most once per duration period
913+-- | Tracks last emission time and skips elements that arrive too quickly
914+-- | Note: Uses wall-clock time, not processing time
915+throttle :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
916+throttle (Milliseconds duration) stream = throttleWithState 0.0 stream
917+ where
918+ throttleWithState lastEmit s = mkStrom do
919+ step <- runStrom s
920+ case step of
921+ Done Nothing -> pure $ Done Nothing
922+ Done (Just chunk) -> do
923+ Tuple newLastEmit filtered <- filterByTime lastEmit chunk
924+ pure $ Done $ if Array.null filtered then Nothing else Just filtered
925+ Loop (Tuple maybeChunk next) -> do
926+ case maybeChunk of
927+ Nothing -> pure $ Loop $ Tuple Nothing (throttleWithState lastEmit next)
928+ Just chunk -> do
929+ Tuple newLastEmit filtered <- filterByTime lastEmit chunk
930+ if Array.null filtered then pure $ Loop $ Tuple Nothing (throttleWithState newLastEmit next)
931+ else pure $ Loop $ Tuple (Just filtered) (throttleWithState newLastEmit next)
932+
933+ filterByTime lastEmit chunk = do
934+ Tuple newLastEmit results <- Array.foldl
935+ ( \acc value -> do
936+ Tuple currentLast filtered <- acc
937+ currentTime <- liftEffect now
938+ let Milliseconds currentMillis = unInstant currentTime
939+ let elapsed = currentMillis - currentLast
940+ if elapsed >= duration then
941+ pure $ Tuple currentMillis (Array.snoc filtered value)
942+ else
943+ pure $ Tuple currentLast filtered
944+ )
945+ (pure $ Tuple lastEmit [])
946+ chunk
947+ pure $ Tuple newLastEmit results
948+
949+-- | Delay each element by a duration
950+delayStrom :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
951+delayStrom duration stream = mkStrom do
952+ Om.delay duration
953+ runStrom stream
954+
955+--------------------------------------------------------------------------------
956+-- Running
957+--------------------------------------------------------------------------------
958+
959+-- | Fold over all elements
960+runFold :: forall ctx err a b. b -> (b -> a -> b) -> Strom ctx err a -> Om ctx err b
961+runFold initial f stream = tailRecM go (Tuple initial stream)
962+ where
963+ go (Tuple acc s) = do
964+ step <- runStrom s
965+ case step of
966+ Done Nothing -> pure $ Done acc
967+ Done (Just chunk) -> pure $ Done $ foldl f acc chunk
968+ Loop (Tuple maybeChunk next) ->
969+ -- Process chunk if present and continue
970+ case maybeChunk of
971+ Nothing -> pure $ Loop $ Tuple acc next
972+ Just chunk -> pure $ Loop $ Tuple (foldl f acc chunk) next
973+
974+-- | Collect all elements into an array (O(n) using chunk accumulation)
975+runCollect :: forall ctx err a. Strom ctx err a -> Om ctx err (Array a)
976+runCollect stream = tailRecM go (Tuple List.Nil stream)
977+ where
978+ go (Tuple chunkList s) = do
979+ step <- runStrom s
980+ case step of
981+ Done Nothing -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse chunkList
982+ Done (Just chunk) -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse $ List.Cons chunk chunkList
983+ Loop (Tuple maybeChunk next) ->
984+ -- Emit the chunk (if present) and continue
985+ case maybeChunk of
986+ Nothing -> pure $ Loop $ Tuple chunkList next
987+ Just chunk -> pure $ Loop $ Tuple (List.Cons chunk chunkList) next
988+
989+-- | Run the stream and discard all elements
990+runDrain :: forall ctx err a. Strom ctx err a -> Om ctx err Unit
991+runDrain = runFold unit (\_ _ -> unit)
992+
993+-- | Traverse the stream with effects, discarding results
994+traverseStrom_ :: forall ctx err a. (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
995+traverseStrom_ f stream = tailRecM go stream
996+ where
997+ go s = do
998+ step <- runStrom s
999+ case step of
1000+ Done Nothing -> pure $ Done unit
1001+ Done (Just chunk) -> do
1002+ _ <- traverse f chunk
1003+ pure $ Done unit
1004+ Loop (Tuple maybeChunk next) -> do
1005+ -- Process chunk if present, then continue
1006+ case maybeChunk of
1007+ Nothing -> pure unit
1008+ Just chunk -> void $ traverse f chunk
1009+ pure $ Loop next
1010+
1011+-- | Alias for traverseStrom_ with arguments flipped
1012+forStrom_ :: forall ctx err a. Strom ctx err a -> (a -> Om ctx err Unit) -> Om ctx err Unit
1013+forStrom_ = flip traverseStrom_
1014+
1015+-- | Alias for traverseStrom_ (FP convention)
1016+forMStrom_ :: forall ctx err a. Strom ctx err a -> (a -> Om ctx err Unit) -> Om ctx err Unit
1017+forMStrom_ = forStrom_
1018+
1019+-- | Alias for traverseStrom_ (explicit monadic naming)
1020+traverseMStrom_ :: forall ctx err a. (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
1021+traverseMStrom_ = traverseStrom_
1022+
1023+--------------------------------------------------------------------------------
1024+-- Cancellable Execution
1025+--------------------------------------------------------------------------------
1026+
1027+-- | Run a stream in the background, returning a Fiber that can be cancelled
1028+-- |
1029+-- | To use cancellable streams, convert your stream to Om and use Om.launchOm:
1030+-- |
1031+-- | Example:
1032+-- | ```purescript
1033+-- | fiber <- Om.launchOm ctx handlers (Strom.runCollect myStream)
1034+-- | -- ... later ...
1035+-- | killFiber (error "Cancelled") fiber
1036+-- | ```
1037+-- |
1038+-- | Or for subscription-style:
1039+-- | ```purescript
1040+-- | fiber <- Om.launchOm ctx handlers (Strom.traverseStrom_ callback myStream)
1041+-- | -- ... later ...
1042+-- | killFiber (error "Unsubscribed") fiber
1043+-- | ```
1044+-- |
1045+-- | Note: You can use Om.launchOm directly with any Om-returning function like:
1046+-- | - `runCollect` - collect results in background
1047+-- | - `runDrain` - run for side effects
1048+-- | - `traverseStrom_` - process each element with callback
1049+-- | - `runFold` - fold with accumulator
1050+
1051+--------------------------------------------------------------------------------
1052+-- Combining
1053+--------------------------------------------------------------------------------
1054+
1055+-- | Append two streams
1056+-- | Now properly handles multi-chunk streams with new Step protocol
1057+-- | Note: s2 is appended TO s1 (s1 comes first, then s2)
1058+-- | This matches pipeline semantics: stream # appendStrom other means stream followed by other
1059+appendStrom :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1060+appendStrom toAppend baseStream = mkStrom do
1061+ step <- runStrom baseStream
1062+ case step of
1063+ Done Nothing -> runStrom toAppend
1064+ Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) toAppend -- Emit chunk and continue with toAppend
1065+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (appendStrom toAppend next)
1066+
1067+-- | Concatenate an array of streams
1068+-- | Uses foldr with flip to match appendStrom's pipeline-friendly parameter order
1069+concatStrom :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1070+concatStrom streams = Array.foldr (flip appendStrom) empty streams
1071+
1072+-- | Zip two streams together
1073+zipStrom :: forall ctx err a b. Strom ctx err a -> Strom ctx err b -> Strom ctx err (Tuple a b)
1074+zipStrom = zipWithStrom Tuple
1075+
1076+-- | Zip two streams with a function
1077+zipWithStrom :: forall ctx err a b c. (a -> b -> c) -> Strom ctx err a -> Strom ctx err b -> Strom ctx err c
1078+zipWithStrom f s1 s2 = mkStrom do
1079+ step1 <- runStrom s1
1080+ step2 <- runStrom s2
1081+ case step1, step2 of
1082+ Done (Just arr1), Done (Just arr2) -> do
1083+ let zipped = Array.zipWith f arr1 arr2
1084+ pure $ Done $ Just zipped
1085+ Done Nothing, _ -> pure $ Done Nothing
1086+ _, Done Nothing -> pure $ Done Nothing
1087+ Loop (Tuple maybeChunk1 next1), Loop (Tuple maybeChunk2 next2) ->
1088+ -- Both looping: zip chunks if both present, continue with both nexts
1089+ case maybeChunk1, maybeChunk2 of
1090+ Just chunk1, Just chunk2 -> pure $ Loop $ Tuple (Just $ Array.zipWith f chunk1 chunk2) (zipWithStrom f next1 next2)
1091+ _, _ -> pure $ Loop $ Tuple Nothing (zipWithStrom f next1 next2)
1092+ Loop (Tuple _ next1), _ -> pure $ Loop $ Tuple Nothing (zipWithStrom f next1 s2)
1093+ _, Loop (Tuple _ next2) -> pure $ Loop $ Tuple Nothing (zipWithStrom f s1 next2)
1094+
1095+-- | Interleave two streams - alternates between elements from each stream
1096+-- | Similar to merge but deterministic - always alternates
1097+interleave :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1098+interleave s1 s2 = interleaveHelper true s1 s2
1099+ where
1100+ interleaveHelper preferFirst first second = mkStrom do
1101+ if preferFirst then do
1102+ step1 <- runStrom first
1103+ case step1 of
1104+ Done Nothing -> runStrom second
1105+ Done (Just chunk1) -> pure $ Loop $ Tuple (Just chunk1) (interleaveHelper false second first)
1106+ Loop (Tuple maybeChunk1 next1) ->
1107+ pure $ Loop $ Tuple maybeChunk1 (interleaveHelper false second next1)
1108+ else do
1109+ step2 <- runStrom second
1110+ case step2 of
1111+ Done Nothing -> runStrom first
1112+ Done (Just chunk2) -> pure $ Loop $ Tuple (Just chunk2) (interleaveHelper true first second)
1113+ Loop (Tuple maybeChunk2 next2) ->
1114+ pure $ Loop $ Tuple maybeChunk2 (interleaveHelper true first next2)
1115+
1116+-- | Intersperse a separator element between each element of the stream
1117+intersperse :: forall ctx err a. a -> Strom ctx err a -> Strom ctx err a
1118+intersperse separator stream = intersperseHelper true stream
1119+ where
1120+ intersperseHelper isFirst s = mkStrom do
1121+ step <- runStrom s
1122+ case step of
1123+ Done Nothing -> pure $ Done Nothing
1124+ Done (Just chunk) -> do
1125+ let
1126+ withSeparators =
1127+ if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
1128+ else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1129+ pure $ Done $ Just withSeparators
1130+ Loop (Tuple maybeChunk next) ->
1131+ case maybeChunk of
1132+ Nothing -> pure $ Loop $ Tuple Nothing (intersperseHelper isFirst next)
1133+ Just chunk -> do
1134+ let
1135+ withSeparators =
1136+ if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
1137+ else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1138+ pure $ Loop $ Tuple (Just withSeparators) (intersperseHelper false next)
1139+
1140+--------------------------------------------------------------------------------
1141+-- Buffering
1142+--------------------------------------------------------------------------------
1143+
1144+-- | Buffer stream chunks with backpressure (preserves chunking)
1145+bufferChunks :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
1146+bufferChunks capacity stream
1147+ | capacity <= 0 = stream
1148+ | otherwise = mkStrom do
1149+ ctx <- Om.ask
1150+ liftAff do
1151+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (BufferItem a))
1152+ sizeRef <- liftEffect $ Ref.new 0
1153+ mutex <- AVar.new unit
1154+ notEmpty <- AVar.empty
1155+ notFull <- AVar.new unit
1156+
1157+ let
1158+ enqueue item = do
1159+ AVar.take notFull
1160+ AVar.take mutex
1161+ size <- liftEffect $ Ref.read sizeRef
1162+ let wasEmpty = size == 0
1163+ buf <- liftEffect $ Ref.read bufferRef
1164+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1165+ liftEffect $ Ref.write (size + 1) sizeRef
1166+ AVar.put unit mutex
1167+ when wasEmpty $ AVar.put unit notEmpty
1168+ when (size + 1 < capacity) $ AVar.put unit notFull
1169+
1170+ dequeue = do
1171+ AVar.take notEmpty
1172+ AVar.take mutex
1173+ buf <- liftEffect $ Ref.read bufferRef
1174+ size <- liftEffect $ Ref.read sizeRef
1175+ case CatQueue.uncons buf of
1176+ Nothing -> do
1177+ AVar.put unit mutex
1178+ dequeue
1179+ Just (Tuple head tail) -> do
1180+ liftEffect $ Ref.write tail bufferRef
1181+ liftEffect $ Ref.write (size - 1) sizeRef
1182+ let hasMore = (size - 1) > 0
1183+ let wasFull = size == capacity
1184+ AVar.put unit mutex
1185+ when hasMore $ AVar.put unit notEmpty
1186+ when wasFull $ AVar.put unit notFull
1187+ pure head
1188+
1189+ producer s = do
1190+ stepResult <- Om.runReader ctx (runStrom s)
1191+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1192+ case step of
1193+ Done Nothing -> enqueue BufferDone
1194+ Done (Just chunk) -> do
1195+ enqueue (BufferChunk chunk)
1196+ enqueue BufferDone
1197+ Loop (Tuple maybeChunk next) -> do
1198+ case maybeChunk of
1199+ Just chunk -> enqueue (BufferChunk chunk)
1200+ Nothing -> pure unit
1201+ producer next
1202+
1203+ fiber <- forkAff (producer stream)
1204+
1205+ let
1206+ consumer = mkStrom $ liftAff do
1207+ msg <- dequeue
1208+ case msg of
1209+ BufferDone -> do
1210+ killFiber (Exception.error "done") fiber
1211+ pure $ Done Nothing
1212+ BufferChunk chunk ->
1213+ pure $ Loop $ Tuple (Just chunk) consumer
1214+
1215+ stepResult <- Om.runReader ctx (runStrom consumer)
1216+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1217+
1218+-- | Buffer stream elements with backpressure (destroys chunking)
1219+buffer :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
1220+buffer capacity stream
1221+ | capacity <= 0 = stream
1222+ | otherwise = mkStrom do
1223+ ctx <- Om.ask
1224+ liftAff do
1225+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (BufferItem a))
1226+ sizeRef <- liftEffect $ Ref.new 0
1227+ mutex <- AVar.new unit
1228+ notEmpty <- AVar.empty
1229+ notFull <- AVar.new unit
1230+
1231+ let
1232+ enqueue item = do
1233+ AVar.take notFull
1234+ AVar.take mutex
1235+ size <- liftEffect $ Ref.read sizeRef
1236+ let wasEmpty = size == 0
1237+ buf <- liftEffect $ Ref.read bufferRef
1238+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1239+ liftEffect $ Ref.write (size + 1) sizeRef
1240+ AVar.put unit mutex
1241+ when wasEmpty $ AVar.put unit notEmpty
1242+ when (size + 1 < capacity) $ AVar.put unit notFull
1243+
1244+ dequeue = do
1245+ AVar.take notEmpty
1246+ AVar.take mutex
1247+ buf <- liftEffect $ Ref.read bufferRef
1248+ size <- liftEffect $ Ref.read sizeRef
1249+ case CatQueue.uncons buf of
1250+ Nothing -> do
1251+ AVar.put unit mutex
1252+ dequeue
1253+ Just (Tuple head tail) -> do
1254+ liftEffect $ Ref.write tail bufferRef
1255+ liftEffect $ Ref.write (size - 1) sizeRef
1256+ let hasMore = (size - 1) > 0
1257+ let wasFull = size == capacity
1258+ AVar.put unit mutex
1259+ when hasMore $ AVar.put unit notEmpty
1260+ when wasFull $ AVar.put unit notFull
1261+ pure head
1262+
1263+ producer s = do
1264+ stepResult <- Om.runReader ctx (runStrom s)
1265+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1266+ case step of
1267+ Done Nothing -> enqueue BufferDone
1268+ Done (Just chunk) -> do
1269+ traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1270+ enqueue BufferDone
1271+ Loop (Tuple maybeChunk next) -> do
1272+ case maybeChunk of
1273+ Just chunk -> traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
1274+ Nothing -> pure unit
1275+ producer next
1276+
1277+ fiber <- forkAff (producer stream)
1278+
1279+ let
1280+ consumer = mkStrom $ liftAff do
1281+ msg <- dequeue
1282+ case msg of
1283+ BufferDone -> do
1284+ killFiber (Exception.error "done") fiber
1285+ pure $ Done Nothing
1286+ BufferChunk chunk ->
1287+ pure $ Loop $ Tuple (Just chunk) consumer
1288+
1289+ stepResult <- Om.runReader ctx (runStrom consumer)
1290+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1291+
1292+--------------------------------------------------------------------------------
1293+-- Concurrency
1294+--------------------------------------------------------------------------------
1295+
1296+-- | Merge two streams concurrently (nondeterministic like ZStream merge)
1297+-- | Elements are emitted as soon as they're available from either stream
1298+merge :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1299+merge = mergeNDWith 1024
1300+
1301+-- | Merge with halt strategy and mapping functions (ZStream-like)
1302+mergeWith
1303+ :: forall ctx err a b c
1304+ . HaltStrategy
1305+ -> (a -> c)
1306+ -> (b -> c)
1307+ -> Strom ctx err a
1308+ -> Strom ctx err b
1309+ -> Strom ctx err c
1310+mergeWith strategy f g s1 s2 =
1311+ mergeNDWithStrategy 1024 strategy (mapStrom f s1) (mapStrom g s2)
1312+
1313+mergeHaltEither :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1314+mergeHaltEither = mergeNDWithStrategy 1024 HaltEither
1315+
1316+mergeHaltLeft :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1317+mergeHaltLeft = mergeNDWithStrategy 1024 HaltLeft
1318+
1319+mergeHaltRight :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1320+mergeHaltRight = mergeNDWithStrategy 1024 HaltRight
1321+
1322+-- | Merge multiple streams concurrently
1323+mergeAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1324+mergeAll streams = Array.foldl merge empty streams
1325+
1326+-- | Race two streams - returns elements from whichever produces first
1327+-- | Uses Om.race for true concurrent racing
1328+race :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1329+race = mergeHaltEither
1330+
1331+-- | Race multiple streams - returns elements from whichever produces first
1332+raceAll :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1333+raceAll streams =
1334+ case Array.uncons streams of
1335+ Nothing -> empty
1336+ Just { head, tail } -> Array.foldl mergeHaltEither head tail
1337+
1338+-- | Non-deterministic merge: Merge two streams concurrently, emitting elements as soon as they're available
1339+-- | Unlike `merge`, this runs both streams in parallel and elements may arrive in any order
1340+-- | The stream that produces results faster will have its elements appear first
1341+-- | Optimized: Spawns producer fibers once and uses a queue for communication
1342+mergeND :: forall ctx err a. Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1343+mergeND = mergeNDWith 1024
1344+
1345+-- | Non-deterministic merge with configurable buffer capacity (chunk queue)
1346+mergeNDWith :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a -> Strom ctx err a
1347+mergeNDWith bufferCapacity = mergeNDWithStrategy bufferCapacity HaltBoth
1348+
1349+mergeNDWithStrategy
1350+ :: forall ctx err a
1351+ . Int
1352+ -> HaltStrategy
1353+ -> Strom ctx err a
1354+ -> Strom ctx err a
1355+ -> Strom ctx err a
1356+mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
1357+ -- Capture context
1358+ ctx <- Om.ask
1359+
1360+ liftAff do
1361+ -- Create buffered queue with backpressure
1362+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (QueueItem a))
1363+ sizeRef <- liftEffect $ Ref.new 0
1364+ mutex <- AVar.new unit
1365+ notEmpty <- AVar.empty
1366+ notFull <- AVar.new unit
1367+
1368+ let
1369+ shouldStop doneLeft doneRight =
1370+ case strategy of
1371+ HaltBoth -> doneLeft && doneRight
1372+ HaltEither -> doneLeft || doneRight
1373+ HaltLeft -> doneLeft
1374+ HaltRight -> doneRight
1375+ enqueue item = do
1376+ AVar.take notFull
1377+ AVar.take mutex
1378+ size <- liftEffect $ Ref.read sizeRef
1379+ let wasEmpty = size == 0
1380+ buf <- liftEffect $ Ref.read bufferRef
1381+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1382+ liftEffect $ Ref.write (size + 1) sizeRef
1383+ AVar.put unit mutex
1384+ when wasEmpty $ AVar.put unit notEmpty
1385+ when (size + 1 < bufferCapacity) $ AVar.put unit notFull
1386+
1387+ dequeue = do
1388+ AVar.take notEmpty
1389+ AVar.take mutex
1390+ buf <- liftEffect $ Ref.read bufferRef
1391+ size <- liftEffect $ Ref.read sizeRef
1392+ case CatQueue.uncons buf of
1393+ Nothing -> do
1394+ AVar.put unit mutex
1395+ dequeue
1396+ Just (Tuple head tail) -> do
1397+ liftEffect $ Ref.write tail bufferRef
1398+ liftEffect $ Ref.write (size - 1) sizeRef
1399+ let hasMore = (size - 1) > 0
1400+ let wasFull = size == bufferCapacity
1401+ AVar.put unit mutex
1402+ when hasMore $ AVar.put unit notEmpty
1403+ when wasFull $ AVar.put unit notFull
1404+ pure head
1405+
1406+ -- Spawn producer fiber for stream1
1407+ fiber1 <- forkAff do
1408+ let
1409+ producer s = do
1410+ stepResult <- Om.runReader ctx (runStrom s)
1411+ step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1412+ case step of
1413+ Done Nothing ->
1414+ enqueue (StreamDone StreamId1)
1415+ Done (Just chunk) -> do
1416+ enqueue (DataChunk chunk StreamId1)
1417+ enqueue (StreamDone StreamId1)
1418+ Loop (Tuple maybeChunk next) -> do
1419+ case maybeChunk of
1420+ Just chunk -> enqueue (DataChunk chunk StreamId1)
1421+ Nothing -> pure unit
1422+ producer next
1423+ producer stream1
1424+
1425+ -- Spawn producer fiber for stream2
1426+ fiber2 <- forkAff do
1427+ let
1428+ producer s = do
1429+ stepResult <- Om.runReader ctx (runStrom s)
1430+ step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
1431+ case step of
1432+ Done Nothing ->
1433+ enqueue (StreamDone StreamId2)
1434+ Done (Just chunk) -> do
1435+ enqueue (DataChunk chunk StreamId2)
1436+ enqueue (StreamDone StreamId2)
1437+ Loop (Tuple maybeChunk next) -> do
1438+ case maybeChunk of
1439+ Just chunk -> enqueue (DataChunk chunk StreamId2)
1440+ Nothing -> pure unit
1441+ producer next
1442+ producer stream2
1443+
1444+ -- Consumer: pull from queue
1445+ let
1446+ consumer doneLeft doneRight = mkStrom $ liftAff do
1447+ if shouldStop doneLeft doneRight then do
1448+ -- Both streams done, cleanup
1449+ killFiber (Exception.error "done") fiber1
1450+ killFiber (Exception.error "done") fiber2
1451+ pure $ Done Nothing
1452+ else do
1453+ msg <- dequeue
1454+ case msg of
1455+ StreamDone streamId -> do
1456+ -- One stream finished, recurse
1457+ let
1458+ doneLeft' = doneLeft || streamId == StreamId1
1459+ doneRight' = doneRight || streamId == StreamId2
1460+ stepResult <- Om.runReader ctx (runStrom $ consumer doneLeft' doneRight')
1461+ either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1462+ DataChunk chunk _streamId ->
1463+ -- Got a chunk
1464+ pure $ Loop $ Tuple (Just chunk) (consumer doneLeft doneRight)
1465+
1466+ stepResult <- Om.runReader ctx (runStrom $ consumer false false)
1467+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1468+
1469+-- | Non-deterministic merge of multiple streams
1470+-- | All streams run concurrently and emit elements as soon as they're available
1471+mergeAllND :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
1472+mergeAllND = mergeAllNDWith 1024
1473+
1474+-- | Non-deterministic merge of multiple streams with configurable buffer capacity
1475+mergeAllNDWith :: forall ctx err a. Int -> Array (Strom ctx err a) -> Strom ctx err a
1476+mergeAllNDWith bufferCapacity streams
1477+ | Array.null streams = empty
1478+ | otherwise = mkStrom do
1479+ ctx <- Om.ask
1480+ liftAff do
1481+ bufferRef <- liftEffect $ Ref.new (CatQueue.empty :: CatQueue (MergeAllItem a))
1482+ sizeRef <- liftEffect $ Ref.new 0
1483+ mutex <- AVar.new unit
1484+ notEmpty <- AVar.empty
1485+ notFull <- AVar.new unit
1486+
1487+ let
1488+ enqueue item = do
1489+ AVar.take notFull
1490+ AVar.take mutex
1491+ size <- liftEffect $ Ref.read sizeRef
1492+ let wasEmpty = size == 0
1493+ buf <- liftEffect $ Ref.read bufferRef
1494+ liftEffect $ Ref.write (CatQueue.snoc buf item) bufferRef
1495+ liftEffect $ Ref.write (size + 1) sizeRef
1496+ AVar.put unit mutex
1497+ when wasEmpty $ AVar.put unit notEmpty
1498+ when (size + 1 < bufferCapacity) $ AVar.put unit notFull
1499+
1500+ dequeue = do
1501+ AVar.take notEmpty
1502+ AVar.take mutex
1503+ buf <- liftEffect $ Ref.read bufferRef
1504+ size <- liftEffect $ Ref.read sizeRef
1505+ case CatQueue.uncons buf of
1506+ Nothing -> do
1507+ AVar.put unit mutex
1508+ dequeue
1509+ Just (Tuple head tail) -> do
1510+ liftEffect $ Ref.write tail bufferRef
1511+ liftEffect $ Ref.write (size - 1) sizeRef
1512+ let hasMore = (size - 1) > 0
1513+ let wasFull = size == bufferCapacity
1514+ AVar.put unit mutex
1515+ when hasMore $ AVar.put unit notEmpty
1516+ when wasFull $ AVar.put unit notFull
1517+ pure head
1518+
1519+ producer s = do
1520+ stepResult <- Om.runReader ctx (runStrom s)
1521+ step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1522+ case step of
1523+ Done Nothing -> enqueue MergeDone
1524+ Done (Just chunk) -> do
1525+ enqueue (MergeChunk chunk)
1526+ enqueue MergeDone
1527+ Loop (Tuple maybeChunk next) -> do
1528+ case maybeChunk of
1529+ Just chunk -> enqueue (MergeChunk chunk)
1530+ Nothing -> pure unit
1531+ producer next
1532+
1533+ fibers <- traverse (\s -> forkAff (producer s)) streams
1534+
1535+ let
1536+ totalStreams = Array.length streams
1537+ consumer doneCount = mkStrom $ liftAff do
1538+ if doneCount >= totalStreams then do
1539+ traverse_ (\fiber -> killFiber (Exception.error "done") fiber) fibers
1540+ pure $ Done Nothing
1541+ else do
1542+ msg <- dequeue
1543+ case msg of
1544+ MergeDone -> do
1545+ stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
1546+ either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
1547+ MergeChunk chunk ->
1548+ pure $ Loop $ Tuple (Just chunk) (consumer doneCount)
1549+
1550+ stepResult <- Om.runReader ctx (runStrom $ consumer 0)
1551+ either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
1552+
1553+--------------------------------------------------------------------------------
1554+-- Parallel Processing
1555+--------------------------------------------------------------------------------
1556+
1557+-- | Map over elements in parallel with bounded concurrency
1558+-- | Processes chunks in parallel groups up to the concurrency limit
1559+mapPar :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1560+mapPar concurrency f stream
1561+ | concurrency <= 1 = mapMStrom f stream -- Sequential fallback
1562+ | otherwise =
1563+ mkStrom do
1564+ step <- runStrom stream
1565+ case step of
1566+ Done Nothing -> pure $ Done Nothing
1567+ Done (Just chunk) -> do
1568+ -- Split chunk into groups of `concurrency` size and process each group in parallel
1569+ let groups = chunksOf concurrency chunk
1570+ results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
1571+ pure $ Done $ Just $ Array.concat results
1572+ Loop (Tuple maybeChunk next) -> do
1573+ mappedChunk <- case maybeChunk of
1574+ Nothing -> pure Nothing
1575+ Just chunk -> do
1576+ let groups = chunksOf concurrency chunk
1577+ results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
1578+ pure $ Just $ Array.concat results
1579+ pure $ Loop $ Tuple mappedChunk (mapPar concurrency f next)
1580+ where
1581+ chunksOf :: forall x. Int -> Array x -> Array (Array x)
1582+ chunksOf n arr
1583+ | n <= 0 || Array.null arr = []
1584+ | otherwise =
1585+ let
1586+ group = Array.take n arr
1587+ rest = Array.drop n arr
1588+ in
1589+ if Array.null group then [] else Array.cons group (chunksOf n rest)
1590+
1591+-- | Unordered parallel map (currently same as mapPar)
1592+mapParUnordered :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1593+mapParUnordered = mapPar
1594+
1595+-- | Alias for mapPar with explicit "M" naming
1596+mapMPar :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1597+mapMPar = mapPar
1598+
1599+-- | Alias for unordered mapPar with explicit "M" naming
1600+mapMParUnordered :: forall ctx err a b. Int -> (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
1601+mapMParUnordered = mapParUnordered
1602+
1603+-- | For each element, execute an effect in parallel with bounded concurrency
1604+foreachPar :: forall ctx err a. Int -> (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
1605+foreachPar concurrency f stream = runDrain (mapPar concurrency f stream)
1606+
1607+-- | Unordered parallel foreach
1608+foreachParUnordered :: forall ctx err a. Int -> (a -> Om ctx err Unit) -> Strom ctx err a -> Om ctx err Unit
1609+foreachParUnordered concurrency f stream = runDrain (mapParUnordered concurrency f stream)
1610+
1611+--------------------------------------------------------------------------------
1612+-- Error Handling
1613+--------------------------------------------------------------------------------
1614+
1615+-- | Catch all errors and recover with a handler function
1616+-- | Note: Uses handleErrors' for full error handling
1617+catchAll :: forall ctx err a. (Variant (exception :: Error | err) -> Strom ctx err a) -> Strom ctx err a -> Strom ctx err a
1618+catchAll handler stream = mkStrom do
1619+ Om.handleErrors'
1620+ (\err -> runStrom (handler err))
1621+ (runStrom stream)
1622+
1623+-- TODO: Fix catchSome type constraints
1624+-- catchSome :: forall ctx err handlers a. (Record handlers) -> Strom ctx err a -> Strom ctx err a
1625+-- catchSome handlers stream = mkStrom do
1626+-- Om.handleErrors handlers (runStrom stream)
1627+
1628+-- | Retry a stream on failure up to 3 times
1629+retry :: forall ctx err a. Strom ctx err a -> Strom ctx err a
1630+retry = retryN 3
1631+
1632+-- | Retry a stream N times on failure
1633+retryN :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
1634+retryN n stream
1635+ | n <= 0 = stream
1636+ | otherwise = catchAll (\_ -> retryN (n - 1) stream) stream
1637+
1638+-- | Run a finaliser after the stream completes (success or failure)
1639+ensuring :: forall ctx err a. Om ctx err Unit -> Strom ctx err a -> Strom ctx err a
1640+ensuring finaliser stream = mkStrom do
1641+ result <- Om.handleErrors'
1642+ ( \err -> do
1643+ finaliser
1644+ throwError err
1645+ )
1646+ ( runStrom stream >>= \step -> do
1647+ finaliser
1648+ pure step
1649+ )
1650+ pure result
1651+
1652+-- | Add a timeout to stream operations
1653+-- | If the stream doesn't complete within the duration, it terminates as empty
1654+timeout :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
1655+timeout duration stream = mkStrom do
1656+ Om.race [ runStrom stream, Om.delay duration *> runStrom empty ]
1657+
1658+--------------------------------------------------------------------------------
1659+-- Resource Safety
1660+--------------------------------------------------------------------------------
1661+
1662+-- | Bracket pattern: acquire, use, release
1663+-- | Guarantees release runs even if stream fails
1664+bracket
1665+ :: forall ctx err a b
1666+ . Om ctx err a -- acquire
1667+ -> (a -> Om ctx err Unit) -- release
1668+ -> (a -> Strom ctx err b) -- use
1669+ -> Strom ctx err b
1670+bracket acquire release use = mkStrom do
1671+ resource <- acquire
1672+ Om.handleErrors'
1673+ ( \err -> do
1674+ release resource
1675+ throwError err
1676+ )
1677+ (runStrom (use resource))
1678+ >>= \step -> do
1679+ case step of
1680+ Done _ -> release resource
1681+ Loop _ -> pure unit
1682+ pure step
1683+
1684+-- | Bracket with exit information (whether stream succeeded or failed)
1685+bracketExit
1686+ :: forall ctx err a b
1687+ . Om ctx err a -- acquire
1688+ -> (a -> Maybe (Variant (exception :: Error | err)) -> Om ctx err Unit) -- release with exit info
1689+ -> (a -> Strom ctx err b) -- use
1690+ -> Strom ctx err b
1691+bracketExit acquire release use = mkStrom do
1692+ resource <- acquire
1693+ Om.handleErrors'
1694+ ( \err -> do
1695+ release resource (Just err)
1696+ throwError err
1697+ )
1698+ (runStrom (use resource))
1699+ >>= \step -> do
1700+ case step of
1701+ Done _ -> release resource Nothing
1702+ Loop _ -> pure unit
1703+ pure step
1704+
1705+-- | Simplified acquire-release pattern
1706+acquireRelease
1707+ :: forall ctx err a b
1708+ . Om ctx err a -- acquire
1709+ -> (a -> Om ctx err Unit) -- release
1710+ -> (a -> Strom ctx err b) -- use
1711+ -> Strom ctx err b
1712+acquireRelease = bracket
1713+
1714+--------------------------------------------------------------------------------
1715+-- Instances
1716+--------------------------------------------------------------------------------
1717+
1718+instance Functor (Strom ctx err) where
1719+ map f stream = mkStrom
1720+ ( runStrom stream >>= \step ->
1721+ case step of
1722+ Done Nothing -> pure $ Done Nothing
1723+ Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
1724+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple (Functor.map (Functor.map f) maybeChunk) (Functor.map f next)
1725+ )
1726+
1727+instance Apply (Strom ctx err) where
1728+ apply fs as = mkStrom
1729+ ( runStrom fs >>= \stepF ->
1730+ runStrom as >>= \stepA ->
1731+ case stepF, stepA of
1732+ Done (Just funcs), Done (Just vals) ->
1733+ let
1734+ results = do
1735+ f <- funcs
1736+ a <- vals
1737+ pure (f a)
1738+ in
1739+ pure $ Done $ Just results
1740+ Done Nothing, _ -> pure $ Done Nothing
1741+ _, Done Nothing -> pure $ Done Nothing
1742+ Loop (Tuple maybeChunkF nextF), Loop (Tuple maybeChunkA nextA) ->
1743+ -- Apply chunks if both present
1744+ case maybeChunkF, maybeChunkA of
1745+ Just chunkF, Just chunkA ->
1746+ let
1747+ applied = do
1748+ f <- chunkF
1749+ a <- chunkA
1750+ pure (f a)
1751+ in
1752+ pure $ Loop $ Tuple (Just applied) (nextF <*> nextA)
1753+ _, _ -> pure $ Loop $ Tuple Nothing (nextF <*> nextA)
1754+ Loop (Tuple _ nextF), _ -> pure $ Loop $ Tuple Nothing (nextF <*> as)
1755+ _, Loop (Tuple _ nextA) -> pure $ Loop $ Tuple Nothing (fs <*> nextA)
1756+ )
1757+
1758+instance Applicative (Strom ctx err) where
1759+ pure = succeed
1760+
1761+instance Bind (Strom ctx err) where
1762+ bind stream f = mkStrom
1763+ ( runStrom stream >>= \step ->
1764+ case step of
1765+ Done Nothing -> pure $ Done Nothing
1766+ Done (Just chunk) ->
1767+ let
1768+ streams = Functor.map f chunk
1769+ in
1770+ runStrom $ concatStrom streams
1771+ Loop (Tuple maybeChunk next) ->
1772+ -- When looping, if we have a chunk, we need to process it through f and concat
1773+ case maybeChunk of
1774+ Nothing -> pure $ Loop $ Tuple Nothing (next >>= f)
1775+ Just chunk ->
1776+ let
1777+ streams = Functor.map f chunk
1778+ prefixStream = concatStrom streams
1779+ in
1780+ runStrom $ appendStrom prefixStream (next >>= f)
1781+ )
1782+
1783+instance Monad (Strom ctx err)
1784+
1785+instance Semigroup (Strom ctx err a) where
1786+ append s1 s2 = mkStrom
1787+ ( runStrom s1 >>= \step ->
1788+ case step of
1789+ Done Nothing -> runStrom s2
1790+ Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) s2
1791+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (next <> s2)
1792+ )
1793+
1794+instance Monoid (Strom ctx err a) where
1795+ mempty = empty
1796+
1797+instance Alt (Strom ctx err) where
1798+ alt s1 s2 = mkStrom
1799+ ( runStrom s1 >>= \step ->
1800+ case step of
1801+ Done Nothing -> runStrom s2
1802+ Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) s2
1803+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (next <|> s2)
1804+ )
1805+
1806+instance Plus (Strom ctx err) where
1807+ empty = mkStrom $ pure $ Done Nothing
1808+
1809+instance Alternative (Strom ctx err)
src/Yoga/Om/Strom.test.purs+3−3
@@ -10,9 +10,9 @@ import Data.Time.Duration (Milliseconds(..))
10import Effect.Aff (Aff)
10import Effect.Aff (Aff)
11import Effect.Ref as Ref
11import Effect.Ref as Ref
12import Effect.Class (liftEffect)
12import Effect.Class (liftEffect)
13−import Effect.Exception (throwException, error)
13+import Effect.Exception (throwException)
14import Test.Spec (Spec, describe, it)
14import Test.Spec (Spec, describe, it)
15−import Test.Spec.Assertions (shouldEqual, fail)
15+import Test.Spec.Assertions (shouldEqual)
16import Yoga.Om (Om)
16import Yoga.Om (Om)
17import Yoga.Om as Om
17import Yoga.Om as Om
18import Yoga.Om.Strom as Strom
18import Yoga.Om.Strom as Strom
@@ -172,7 +172,7 @@ spec = do
172 result `shouldEqual` [ "1a", "2b", "3c" ]
172 result `shouldEqual` [ "1a", "2b", "3c" ]
173
173
174 it "tap observes without modifying" do
174 it "tap observes without modifying" do
175− sideEffectRef <- liftEffect $ Ref.new 0
175+ _sideEffectRef <- liftEffect $ Ref.new 0
176 result <- runOm $
176 result <- runOm $
177 Strom.fromArray [ 1, 2, 3 ]
177 Strom.fromArray [ 1, 2, 3 ]
178 # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
178 # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
src/Yoga/Om/Strom/Examples.purs+3−3
@@ -106,7 +106,7 @@ example4b = do
106example5 :: Om {} () (Array (Tuple Int String))
106example5 :: Om {} () (Array (Tuple Int String))
107example5 = do
107example5 = do
108 let numbers = Strom.rangeStrom 1 5
108 let numbers = Strom.rangeStrom 1 5
109− let letters = Strom.fromArray ["a", "b", "c", "d", "e"]
109+ let _letters = Strom.fromArray ["a", "b", "c", "d", "e"]
110 -- TODO: zipStrom not yet implemented
110 -- TODO: zipStrom not yet implemented
111 numbers # Strom.mapStrom (\n -> Tuple n "stub") # Strom.runCollect
111 numbers # Strom.mapStrom (\n -> Tuple n "stub") # Strom.runCollect
112
112
@@ -288,11 +288,11 @@ example11 = do
288 Om.delay (Milliseconds 100.0)
288 Om.delay (Milliseconds 100.0)
289 pure 1
289 pure 1
290
290
291− source2 = Strom.fromOm do
291+ _source2 = Strom.fromOm do
292 Om.delay (Milliseconds 50.0) -- This will win
292 Om.delay (Milliseconds 50.0) -- This will win
293 pure 2
293 pure 2
294
294
295− source3 = Strom.fromOm do
295+ _source3 = Strom.fromOm do
296 Om.delay (Milliseconds 200.0)
296 Om.delay (Milliseconds 200.0)
297 pure 3
297 pure 3
298
298
src/Yoga/Om/Strom/FileStream.jsnew file+17
@@ -0,0 +1,17 @@
1+import * as fs from "node:fs";
2+
3+export const _writeFile = (path) => (content) => () => {
4+ fs.writeFileSync(path, content);
5+};
6+
7+export const _appendFile = (path) => (content) => () => {
8+ fs.appendFileSync(path, content);
9+};
10+
11+export const _readFile = (path) => () => {
12+ return fs.readFileSync(path, "utf-8");
13+};
14+
15+export const _unlinkFile = (path) => () => {
16+ try { fs.unlinkSync(path); } catch (_) {}
17+};
src/Yoga/Om/Strom/WebStream.jsnew file+176
@@ -0,0 +1,176 @@
1+// FFI bindings for Web Streams API interop
2+
3+// Create a ReadableStream with a pull-based source
4+// pullFn :: Controller -> Effect Promise
5+export const _newReadableStream = (pullFn) => (cancelFn) => () => {
6+ return new ReadableStream({
7+ pull(controller) {
8+ return pullFn(controller)();
9+ },
10+ cancel() {
11+ cancelFn();
12+ }
13+ });
14+};
15+
16+// Enqueue a value into a ReadableStreamDefaultController
17+export const _enqueue = (controller) => (value) => () => {
18+ controller.enqueue(value);
19+};
20+
21+// Close a ReadableStreamDefaultController
22+export const _closeController = (controller) => () => {
23+ controller.close();
24+};
25+
26+// Signal an error on a ReadableStreamDefaultController
27+export const _errorController = (controller) => (err) => () => {
28+ controller.error(err);
29+};
30+
31+// Create a Promise from resolve/reject callbacks.
32+// Takes an Effect-wrapped function that receives resolve and reject as Effect callbacks.
33+// makePromise :: ((a -> Effect Unit) -> (Error -> Effect Unit) -> Effect Unit) -> Effect (Promise a)
34+export const _makePromise = (k) => () => {
35+ return new Promise((resolve, reject) => {
36+ k((a) => () => resolve(a))((e) => () => reject(e))();
37+ });
38+};
39+
40+// Get a reader from a ReadableStream
41+export const _getReader = (stream) => () => {
42+ return stream.getReader();
43+};
44+
45+// Release the reader lock
46+export const _releaseReader = (reader) => () => {
47+ reader.releaseLock();
48+};
49+
50+// Get a writer from a WritableStream
51+export const _getWriter = (stream) => () => {
52+ return stream.getWriter();
53+};
54+
55+// Write a value using a writer — returns EffectFnAff
56+export const _writeWriterImpl = (writer) => (value) => (onError, onSuccess) => {
57+ writer.write(value).then(
58+ () => onSuccess(),
59+ (err) => onError(err)
60+ );
61+ return (cancelError, onCancelerError, onCancelerSuccess) => {
62+ onCancelerSuccess();
63+ };
64+};
65+
66+// Close a writer — returns EffectFnAff
67+export const _closeWriterImpl = (writer) => (onError, onSuccess) => {
68+ writer.close().then(
69+ () => onSuccess(),
70+ (err) => onError(err)
71+ );
72+ return (cancelError, onCancelerError, onCancelerSuccess) => {
73+ onCancelerSuccess();
74+ };
75+};
76+
77+// Wait for writer.ready — returns EffectFnAff
78+export const _writerReadyImpl = (writer) => (onError, onSuccess) => {
79+ writer.ready.then(
80+ () => onSuccess(),
81+ (err) => onError(err)
82+ );
83+ return (cancelError, onCancelerError, onCancelerSuccess) => {
84+ onCancelerSuccess();
85+ };
86+};
87+
88+// Create a new TransformStream
89+export const _newTransformStream = () => {
90+ return new TransformStream();
91+};
92+
93+// Get the readable side of a TransformStream
94+export const _transformReadable = (ts) => {
95+ return ts.readable;
96+};
97+
98+// Get the writable side of a TransformStream
99+export const _transformWritable = (ts) => {
100+ return ts.writable;
101+};
102+
103+// Batch-read up to N elements from a Reader in one Aff call.
104+// Returns { values: Array, done: Boolean }
105+// Cancellation: sets a flag that stops the pump loop and suppresses callbacks.
106+export const _readBatchImpl = (reader) => (batchSize) => (onError, onSuccess) => {
107+ let cancelled = false;
108+ const values = [];
109+ function pump() {
110+ if (cancelled) return;
111+ if (values.length >= batchSize) {
112+ onSuccess({ values, done: false });
113+ return;
114+ }
115+ reader.read().then(
116+ ({ done, value }) => {
117+ if (cancelled) return;
118+ if (done) {
119+ onSuccess({ values, done: true });
120+ } else {
121+ values.push(value);
122+ pump();
123+ }
124+ },
125+ (err) => { if (!cancelled) onError(err); }
126+ );
127+ }
128+ pump();
129+ return (cancelError, onCancelerError, onCancelerSuccess) => {
130+ cancelled = true;
131+ onCancelerSuccess();
132+ };
133+};
134+
135+// Extract the values array from a batch result
136+export const _batchResultValues = (result) => {
137+ return result.values;
138+};
139+
140+// Check if a batch result indicates stream end
141+export const _batchResultDone = (result) => {
142+ return result.done;
143+};
144+
145+// Cancel a reader and release the lock (async, safe even with pending reads).
146+// reader.cancel() resolves pending reads, then releaseLock() drops the lock.
147+export const _cancelReaderImpl = (reader) => (onError, onSuccess) => {
148+ reader.cancel().then(
149+ () => { reader.releaseLock(); onSuccess(); },
150+ (err) => onError(err)
151+ );
152+ return (cancelError, onCancelerError, onCancelerSuccess) => { onCancelerSuccess(); };
153+};
154+
155+// Convert an Om Variant error to a JS Error, preserving info.
156+// Variant internal shape: { type: string, value: ... }
157+// If value is already an Error (exception tag), pass it through.
158+// Otherwise create an Error with descriptive message and attach original variant.
159+export const _variantToError = (variant) => {
160+ if (variant && typeof variant.type === "string") {
161+ if (variant.type === "exception" && variant.value instanceof Error) {
162+ return variant.value;
163+ }
164+ const tag = variant.type;
165+ let msg;
166+ try {
167+ msg = "Stream error [" + tag + "]: " + JSON.stringify(variant.value);
168+ } catch (_e) {
169+ msg = "Stream error [" + tag + "]";
170+ }
171+ const err = new Error(msg);
172+ err.originalVariant = variant;
173+ return err;
174+ }
175+ return new Error("Stream error: " + String(variant));
176+};
src/Yoga/Om/Strom/WebStream.pursnew file+360
@@ -0,0 +1,360 @@
1+module Yoga.Om.Strom.WebStream
2+ ( ReadableStream
3+ , WritableStream
4+ , TransformStream
5+ , toReadableStream
6+ , fromReadableStream
7+ , fromReadableStreamWith
8+ , useReadableStream
9+ , useReadableStreamWith
10+ , runWritable
11+ , pipeThrough
12+ ) where
13+
14+import Prelude
15+
16+import Control.Monad.Error.Class (throwError)
17+import Control.Monad.Rec.Class (Step(..))
18+import Data.Array as Array
19+import Data.Either (Either(..))
20+import Data.Maybe (Maybe(..), maybe)
21+import Data.Traversable (traverse_)
22+import Data.Tuple (Tuple(..))
23+import Data.Variant (Variant)
24+import Effect (Effect)
25+import Effect.Aff (Aff, Fiber, bracket, killFiber, launchAff, try)
26+import Effect.Aff.Class (liftAff)
27+import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff)
28+import Effect.Class (liftEffect)
29+import Effect.Exception as Exception
30+import Effect.Ref as Ref
31+import Yoga.Om (Om)
32+import Yoga.Om as Om
33+import Yoga.Om.Strom (Strom, mkStrom, runStrom)
34+
35+--------------------------------------------------------------------------------
36+-- Foreign Types
37+--------------------------------------------------------------------------------
38+
39+foreign import data ReadableStream :: Type -> Type
40+foreign import data WritableStream :: Type -> Type
41+foreign import data TransformStream :: Type -> Type -> Type
42+
43+-- Internal opaque types for FFI
44+foreign import data Controller :: Type -> Type
45+foreign import data Reader :: Type -> Type
46+foreign import data Writer :: Type -> Type
47+foreign import data Promise :: Type -> Type
48+foreign import data BatchResult :: Type -> Type
49+
50+--------------------------------------------------------------------------------
51+-- Foreign Imports
52+--------------------------------------------------------------------------------
53+
54+-- ReadableStream construction
55+foreign import _newReadableStream
56+ :: forall a
57+ . (Controller a -> Effect (Promise Unit))
58+ -> Effect Unit
59+ -> Effect (ReadableStream a)
60+
61+foreign import _enqueue :: forall a. Controller a -> a -> Effect Unit
62+foreign import _closeController :: forall a. Controller a -> Effect Unit
63+foreign import _errorController :: forall a. Controller a -> Exception.Error -> Effect Unit
64+
65+-- Promise construction from callbacks
66+foreign import _makePromise
67+ :: forall a
68+ . ((a -> Effect Unit) -> (Exception.Error -> Effect Unit) -> Effect Unit)
69+ -> Effect (Promise a)
70+
71+-- Reader operations
72+foreign import _getReader :: forall a. ReadableStream a -> Effect (Reader a)
73+foreign import _releaseReader :: forall a. Reader a -> Effect Unit
74+foreign import _cancelReaderImpl :: forall a. Reader a -> EffectFnAff Unit
75+
76+-- Batch reader operations
77+foreign import _readBatchImpl :: forall a. Reader a -> Int -> EffectFnAff (BatchResult a)
78+foreign import _batchResultValues :: forall a. BatchResult a -> Array a
79+foreign import _batchResultDone :: forall a. BatchResult a -> Boolean
80+
81+-- Error conversion
82+foreign import _variantToError :: forall err. Variant err -> Exception.Error
83+
84+-- Writer operations
85+foreign import _getWriter :: forall a. WritableStream a -> Effect (Writer a)
86+foreign import _writeWriterImpl :: forall a. Writer a -> a -> EffectFnAff Unit
87+foreign import _closeWriterImpl :: forall a. Writer a -> EffectFnAff Unit
88+foreign import _writerReadyImpl :: forall a. Writer a -> EffectFnAff Unit
89+
90+-- TransformStream
91+foreign import _newTransformStream :: forall a b. Effect (TransformStream a b)
92+foreign import _transformReadable :: forall a b. TransformStream a b -> ReadableStream b
93+foreign import _transformWritable :: forall a b. TransformStream a b -> WritableStream a
94+
95+--------------------------------------------------------------------------------
96+-- Internal helpers
97+--------------------------------------------------------------------------------
98+
99+readBatch :: forall a. Reader a -> Int -> Aff (BatchResult a)
100+readBatch reader batchSize = fromEffectFnAff (_readBatchImpl reader batchSize)
101+
102+writeWriter :: forall a. Writer a -> a -> Aff Unit
103+writeWriter writer value = fromEffectFnAff (_writeWriterImpl writer value)
104+
105+closeWriter :: forall a. Writer a -> Aff Unit
106+closeWriter writer = fromEffectFnAff (_closeWriterImpl writer)
107+
108+writerReady :: forall a. Writer a -> Aff Unit
109+writerReady writer = fromEffectFnAff (_writerReadyImpl writer)
110+
111+--------------------------------------------------------------------------------
112+-- Strom → ReadableStream (shared core)
113+--------------------------------------------------------------------------------
114+
115+-- | Internal: builds a ReadableStream from a Strom with a cleanup action
116+-- | that runs on close, error, or cancel. The cleanup action must be
117+-- | idempotent (it may be called more than once).
118+stromToReadableStream :: forall a ctx err. Effect Unit -> ctx -> Strom ctx err a -> Effect (ReadableStream a)
119+stromToReadableStream cleanup ctx strom = do
120+ stromRef <- Ref.new strom
121+ fiberRef <- Ref.new (Nothing :: Maybe (Fiber Unit))
122+ cancelledRef <- Ref.new false
123+ let
124+ -- Keep pulling until we enqueue data or the stream ends.
125+ -- ReadableStream does NOT re-call pull when nothing is enqueued,
126+ -- so we must loop internally for empty chunks (e.g. from filterStrom).
127+ pullLoop :: Controller a -> Aff Unit
128+ pullLoop controller = do
129+ cancelled <- liftEffect $ Ref.read cancelledRef
130+ when (not cancelled) do
131+ currentStrom <- liftEffect $ Ref.read stromRef
132+ result <- Om.runReader ctx (runStrom currentStrom)
133+ case result of
134+ Left omErr -> liftEffect do
135+ cleanup
136+ _errorController controller (_variantToError omErr)
137+ Right step -> case step of
138+ Done Nothing -> liftEffect do
139+ cleanup
140+ _closeController controller
141+ Done (Just chunk) -> liftEffect do
142+ traverse_ (_enqueue controller) chunk
143+ cleanup
144+ _closeController controller
145+ Loop (Tuple maybeChunk next) -> do
146+ liftEffect $ Ref.write next stromRef
147+ case maybeChunk of
148+ Nothing -> pullLoop controller
149+ Just chunk -> liftEffect $ traverse_ (_enqueue controller) chunk
150+
151+ pull :: Controller a -> Effect (Promise Unit)
152+ pull controller =
153+ _makePromise \resolve _reject -> do
154+ fiber <- launchAff do
155+ result <- try (pullLoop controller)
156+ liftEffect do
157+ case result of
158+ -- pullLoop already signalled the controller (error/close) before
159+ -- any Aff-level throw, or the controller is already in a terminal
160+ -- state. Avoid calling _errorController again — it would throw on
161+ -- an already-errored/closed controller and the Promise would hang.
162+ Left _err -> cleanup
163+ Right _ -> pure unit
164+ resolve unit
165+ Ref.write (Just fiber) fiberRef
166+
167+ cancel :: Effect Unit
168+ cancel = do
169+ Ref.write true cancelledRef
170+ -- Kill the fiber first so the JS cancelled flag in _readBatchImpl is set
171+ -- before cleanup calls releaseLock (which rejects pending reads).
172+ maybeFiber <- Ref.read fiberRef
173+ case maybeFiber of
174+ Nothing -> pure unit
175+ Just fiber ->
176+ void $ launchAff $ killFiber (Exception.error "cancelled") fiber
177+ _currentStrom <- Ref.read stromRef
178+ pure unit -- TODO: cancelStrom currentStrom
179+ cleanup
180+
181+ _newReadableStream pull cancel
182+
183+--------------------------------------------------------------------------------
184+-- Strom → ReadableStream (public)
185+--------------------------------------------------------------------------------
186+
187+-- | Convert a Strom to a ReadableStream. Elements are enqueued individually.
188+-- | Requires ctx to run the Om effects. Errors become stream errors with
189+-- | preserved error information. Supports cancellation — when the consumer
190+-- | cancels the ReadableStream, the internal fiber is killed.
191+toReadableStream :: forall a ctx err. ctx -> Strom ctx err a -> Effect (ReadableStream a)
192+toReadableStream = stromToReadableStream (pure unit)
193+
194+--------------------------------------------------------------------------------
195+-- ReadableStream → Strom
196+--------------------------------------------------------------------------------
197+
198+-- | Default batch size for `fromReadableStream` (matches rangeStrom's chunk size).
199+defaultChunkSize :: Int
200+defaultChunkSize = 1000
201+
202+-- | Convert a ReadableStream to a Strom using batched reads.
203+-- | Each pull reads up to 1000 elements from the underlying reader in a tight
204+-- | JS loop, producing properly-sized chunks that match Strom's batch model.
205+-- | The reader is released when the stream ends.
206+-- |
207+-- | Note: if the consumer stops pulling early (e.g. via `takeStrom`), the
208+-- | reader lock is not released until GC. Use `useReadableStream` for
209+-- | guaranteed cleanup on abandonment.
210+fromReadableStream :: forall a ctx err. ReadableStream a -> Strom ctx err a
211+fromReadableStream = fromReadableStreamWith { chunkSize: defaultChunkSize }
212+
213+-- | Convert a ReadableStream to a Strom with a configurable chunk (batch) size.
214+-- | Larger values reduce per-chunk overhead; smaller values reduce latency.
215+fromReadableStreamWith :: forall a ctx err. { chunkSize :: Int } -> ReadableStream a -> Strom ctx err a
216+fromReadableStreamWith { chunkSize } stream = mkStrom do
217+ reader <- liftEffect $ _getReader stream
218+ runStrom (readerToStrom chunkSize reader)
219+
220+-- | Internal: pull from a Reader in batches, releasing the reader on completion.
221+readerToStrom :: forall a ctx err. Int -> Reader a -> Strom ctx err a
222+readerToStrom batchSize reader =
223+ readerToStromWithCleanupAndSize batchSize (_releaseReader reader) reader
224+
225+--------------------------------------------------------------------------------
226+-- Strom → WritableStream sink
227+--------------------------------------------------------------------------------
228+
229+-- | Drain a Strom into a WritableStream. Resolves when the stream is fully written.
230+-- | Backpressure comes from the writer's ready promise.
231+runWritable :: forall a ctx err. WritableStream a -> Strom ctx err a -> Om ctx err Unit
232+runWritable writable strom = do
233+ writer <- liftEffect $ _getWriter writable
234+ Om.handleErrors'
235+ (\err -> do
236+ -- Try to close on error; swallow secondary failures
237+ _ <- liftAff $ try $ closeWriter writer
238+ throwError err
239+ )
240+ do
241+ go writer strom
242+ liftAff $ closeWriter writer
243+ where
244+ go :: Writer a -> Strom ctx err a -> Om ctx err Unit
245+ go writer s = do
246+ step <- runStrom s
247+ case step of
248+ Done Nothing -> pure unit
249+ Done (Just chunk) -> writeChunk writer chunk
250+ Loop (Tuple maybeChunk next) -> do
251+ maybe (pure unit) (writeChunk writer) maybeChunk
252+ go writer next
253+
254+ writeChunk :: Writer a -> Array a -> Om ctx err Unit
255+ writeChunk writer chunk = traverse_ (writeElement writer) chunk
256+
257+ writeElement :: Writer a -> a -> Om ctx err Unit
258+ writeElement writer value = liftAff do
259+ writerReady writer
260+ writeWriter writer value
261+
262+--------------------------------------------------------------------------------
263+-- Piping utilities
264+--------------------------------------------------------------------------------
265+
266+-- | Pipe a ReadableStream through a Strom transformation, producing a new
267+-- | ReadableStream. This is the primary integration point: take web streams,
268+-- | transform them with Strom combinators, and produce a web stream.
269+-- |
270+-- | Reader cleanup: the reader acquired from the source ReadableStream is
271+-- | released when the transformed stream ends naturally or when the output
272+-- | ReadableStream is cancelled by the consumer.
273+pipeThrough
274+ :: forall a b ctx err
275+ . ctx
276+ -> (Strom ctx err a -> Strom ctx err b)
277+ -> ReadableStream a
278+ -> Effect (ReadableStream b)
279+pipeThrough ctx transform readable = do
280+ reader <- _getReader readable
281+ -- Idempotent release — safe to call multiple times
282+ releasedRef <- Ref.new false
283+ let
284+ releaseOnce :: Effect Unit
285+ releaseOnce = do
286+ alreadyReleased <- Ref.read releasedRef
287+ when (not alreadyReleased) do
288+ Ref.write true releasedRef
289+ _releaseReader reader
290+
291+ inputStrom = readerToStromWithCleanupAndSize defaultChunkSize releaseOnce reader
292+
293+ stromToReadableStream releaseOnce ctx (transform inputStrom)
294+
295+-- | Internal: pull from a Reader in batches with configurable chunk size,
296+-- | calling a cleanup action on stream completion.
297+readerToStromWithCleanupAndSize :: forall a ctx err. Int -> Effect Unit -> Reader a -> Strom ctx err a
298+readerToStromWithCleanupAndSize batchSize _cleanup reader = go -- TODO: addCancel cleanup go
299+ where
300+ go :: Strom ctx err a
301+ go = mkStrom do
302+ result <- liftAff $ readBatch reader batchSize
303+ let values = _batchResultValues result
304+ let done = _batchResultDone result
305+ if done then
306+ if Array.null values then pure $ Done Nothing
307+ else pure $ Done $ Just values
308+ else
309+ pure $ Loop $ Tuple (Just values) go
310+
311+--------------------------------------------------------------------------------
312+-- Bracketed ReadableStream consumption
313+--------------------------------------------------------------------------------
314+
315+-- | Consume a ReadableStream with guaranteed reader cleanup via `Aff.bracket`.
316+-- | The reader is released when the callback completes, errors, or the fiber
317+-- | is cancelled — unlike `fromReadableStream` which leaks the reader on
318+-- | early abandonment.
319+-- |
320+-- | ```purescript
321+-- | useReadableStream stream \strom -> do
322+-- | Strom.runCollect (Strom.takeStrom 5 strom)
323+-- | ```
324+useReadableStream
325+ :: forall a b ctx err
326+ . ReadableStream a
327+ -> (Strom ctx err a -> Om ctx err b)
328+ -> Om ctx err b
329+useReadableStream = useReadableStreamWith { chunkSize: defaultChunkSize }
330+
331+-- | Like `useReadableStream` but with a configurable chunk (batch) size.
332+useReadableStreamWith
333+ :: forall a b ctx err
334+ . { chunkSize :: Int }
335+ -> ReadableStream a
336+ -> (Strom ctx err a -> Om ctx err b)
337+ -> Om ctx err b
338+useReadableStreamWith { chunkSize } stream callback = do
339+ ctx <- Om.ask
340+ result <- liftAff $ bracket
341+ (liftEffect do
342+ reader <- _getReader stream
343+ releasedRef <- Ref.new false
344+ pure { reader, releasedRef })
345+ (\{ reader, releasedRef } -> do
346+ alreadyReleased <- liftEffect $ Ref.read releasedRef
347+ when (not alreadyReleased) do
348+ liftEffect $ Ref.write true releasedRef
349+ fromEffectFnAff (_cancelReaderImpl reader))
350+ (\{ reader, releasedRef } -> do
351+ let releaseOnce = do
352+ alreadyReleased <- Ref.read releasedRef
353+ when (not alreadyReleased) do
354+ Ref.write true releasedRef
355+ _releaseReader reader
356+ inputStrom = readerToStromWithCleanupAndSize chunkSize releaseOnce reader
357+ Om.runReader ctx (callback inputStrom))
358+ case result of
359+ Left err -> throwError err
360+ Right value -> pure value
test/Test/Main.pursnew file+12
@@ -0,0 +1,12 @@
1+module Test.Main.Strom where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Test.Spec.Reporter (consoleReporter)
7+import Test.Spec.Runner.Node (runSpecAndExitProcess)
8+import Yoga.Om.Strom.Test as StromSpec
9+
10+main :: Effect Unit
11+main = runSpecAndExitProcess [consoleReporter] do
12+ StromSpec.spec
test/Test/Strom/Spec.purs+3−4
@@ -6,13 +6,12 @@ import Control.Alt ((<|>))
6import Data.Array as Array
6import Data.Array as Array
7import Data.Maybe (Maybe(..))
7import Data.Maybe (Maybe(..))
8import Data.Tuple (Tuple(..))
8import Data.Tuple (Tuple(..))
9−import Data.Time.Duration (Milliseconds(..))
10import Effect.Aff (Aff)
9import Effect.Aff (Aff)
11import Effect.Ref as Ref
10import Effect.Ref as Ref
12import Effect.Class (liftEffect)
11import Effect.Class (liftEffect)
13−import Effect.Exception (throwException, error)
12+import Effect.Exception (throwException)
14import Test.Spec (Spec, describe, it)
13import Test.Spec (Spec, describe, it)
15−import Test.Spec.Assertions (shouldEqual, fail)
14+import Test.Spec.Assertions (shouldEqual)
16import Yoga.Om (Om)
15import Yoga.Om (Om)
17import Yoga.Om as Om
16import Yoga.Om as Om
18import Yoga.Om.Strom as Strom
17import Yoga.Om.Strom as Strom
@@ -156,7 +155,7 @@ spec = do
156 result `shouldEqual` [ "1a", "2b", "3c" ]
155 result `shouldEqual` [ "1a", "2b", "3c" ]
157
156
158 it "tap observes without modifying" do
157 it "tap observes without modifying" do
159− sideEffectRef <- liftEffect $ Ref.new 0
158+ _sideEffectRef <- liftEffect $ Ref.new 0
160 result <- runOm $
159 result <- runOm $
161 Strom.fromArray [ 1, 2, 3 ]
160 Strom.fromArray [ 1, 2, 3 ]
162 # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
161 # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
test/Test/WebStream.jsnew file+106
@@ -0,0 +1,106 @@
1+// Test FFI helpers for WebStream tests
2+
3+// Collect all values from a ReadableStream into an array — returns Aff
4+export const _collectReadableStream = (stream) => (onError, onSuccess) => {
5+ const reader = stream.getReader();
6+ const values = [];
7+ function pump() {
8+ reader.read().then(
9+ ({ done, value }) => {
10+ if (done) {
11+ onSuccess(values);
12+ } else {
13+ values.push(value);
14+ pump();
15+ }
16+ },
17+ (err) => onError(err)
18+ );
19+ }
20+ pump();
21+ return (cancelError, onCancelerError, onCancelerSuccess) => {
22+ reader.releaseLock();
23+ onCancelerSuccess();
24+ };
25+};
26+
27+// Create a WritableStream that collects values into an array
28+export const _createWritableStreamCollector = () => {
29+ const values = [];
30+ const stream = new WritableStream({
31+ write(chunk) {
32+ values.push(chunk);
33+ }
34+ });
35+ return {
36+ stream: stream,
37+ getValues: () => values.slice()
38+ };
39+};
40+
41+// Create a ReadableStream that produces incrementing integers.
42+// Each read() produces one value after a 1ms delay.
43+// Returns { stream, getProducedCount } — count grows per read,
44+// so after pump stops, the count stabilises.
45+export const _createCountingStream = () => {
46+ let count = 0;
47+ let cancelled = false;
48+
49+ const stream = new ReadableStream({
50+ pull(controller) {
51+ if (cancelled) return;
52+ return new Promise(resolve => {
53+ setTimeout(() => {
54+ if (!cancelled) {
55+ count++;
56+ controller.enqueue(count);
57+ }
58+ resolve();
59+ }, 1);
60+ });
61+ },
62+ cancel() {
63+ cancelled = true;
64+ }
65+ });
66+
67+ return {
68+ stream,
69+ getProducedCount: () => count
70+ };
71+};
72+
73+// Check if a ReadableStream is locked (has an active reader)
74+export const _isStreamLocked = (stream) => () => {
75+ return stream.locked;
76+};
77+
78+// Read N elements from a ReadableStream then cancel — returns EffectFnAff
79+export const _readNThenCancel = (stream) => (n) => (onError, onSuccess) => {
80+ const reader = stream.getReader();
81+ const values = [];
82+ function pump() {
83+ if (values.length >= n) {
84+ reader.cancel("read enough").then(
85+ () => { reader.releaseLock(); onSuccess(values); },
86+ (err) => onError(err)
87+ );
88+ return;
89+ }
90+ reader.read().then(
91+ ({ done, value }) => {
92+ if (done) {
93+ onSuccess(values);
94+ } else {
95+ values.push(value);
96+ pump();
97+ }
98+ },
99+ (err) => onError(err)
100+ );
101+ }
102+ pump();
103+ return (cancelError, onCancelerError, onCancelerSuccess) => {
104+ onCancelerSuccess();
105+ };
106+};
test/Test/WebStream.pursnew file+223
@@ -0,0 +1,223 @@
1+module Test.WebStream where
2+
3+import Prelude
4+
5+import Data.Array as Array
6+import Effect (Effect)
7+import Effect.Aff (Aff, delay, forkAff, killFiber)
8+import Effect.Aff.Compat (EffectFnAff, fromEffectFnAff)
9+import Effect.Class (liftEffect)
10+import Effect.Exception as Exception
11+import Effect.Ref as Ref
12+import Test.Spec (Spec, describe, it)
13+import Test.Spec.Assertions (shouldEqual, shouldSatisfy)
14+import Yoga.Om (Om)
15+import Yoga.Om as Om
16+import Yoga.Om.Strom as Strom
17+import Yoga.Om.Strom.WebStream as WS
18+import Data.Time.Duration (Milliseconds(..))
19+
20+-- Helper to run Om in tests (same as in Strom tests)
21+runOm :: forall a. Om {} () a -> Aff a
22+runOm om = Om.runOm {} { exception: \err -> liftEffect (Exception.throwException err) } om
23+
24+-- FFI helpers for testing
25+foreign import _collectReadableStream :: forall a. WS.ReadableStream a -> EffectFnAff (Array a)
26+foreign import _createWritableStreamCollector :: forall a. Effect { stream :: WS.WritableStream a, getValues :: Effect (Array a) }
27+foreign import _readNThenCancel :: forall a. WS.ReadableStream a -> Int -> EffectFnAff (Array a)
28+foreign import _createCountingStream :: Effect { stream :: WS.ReadableStream Int, getProducedCount :: Effect Int }
29+foreign import _isStreamLocked :: forall a. WS.ReadableStream a -> Effect Boolean
30+
31+collectReadableStream :: forall a. WS.ReadableStream a -> Aff (Array a)
32+collectReadableStream = fromEffectFnAff <<< _collectReadableStream
33+
34+readNThenCancel :: forall a. WS.ReadableStream a -> Int -> Aff (Array a)
35+readNThenCancel stream n = fromEffectFnAff (_readNThenCancel stream n)
36+
37+spec :: Spec Unit
38+spec = do
39+ describe "WebStream" do
40+
41+ describe "toReadableStream" do
42+
43+ it "converts empty Strom to empty ReadableStream" do
44+ rs <- liftEffect $ WS.toReadableStream {} (Strom.empty :: Strom.Strom {} () Int)
45+ result <- collectReadableStream rs
46+ result `shouldEqual` ([] :: Array Int)
47+
48+ it "converts single-element Strom to ReadableStream" do
49+ rs <- liftEffect $ WS.toReadableStream {} (Strom.succeed 42)
50+ result <- collectReadableStream rs
51+ result `shouldEqual` [ 42 ]
52+
53+ it "converts multi-element Strom to ReadableStream" do
54+ rs <- liftEffect $ WS.toReadableStream {} (Strom.fromArray [ 1, 2, 3, 4, 5 ])
55+ result <- collectReadableStream rs
56+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
57+
58+ it "converts range Strom to ReadableStream" do
59+ rs <- liftEffect $ WS.toReadableStream {} (Strom.rangeStrom 1 6)
60+ result <- collectReadableStream rs
61+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
62+
63+ it "converts transformed Strom to ReadableStream" do
64+ let strom = Strom.fromArray [ 1, 2, 3 ] # Strom.mapStrom (_ * 10)
65+ rs <- liftEffect $ WS.toReadableStream {} strom
66+ result <- collectReadableStream rs
67+ result `shouldEqual` [ 10, 20, 30 ]
68+
69+ describe "fromReadableStream" do
70+
71+ it "converts ReadableStream back to Strom" do
72+ rs <- liftEffect $ WS.toReadableStream {} (Strom.fromArray [ 10, 20, 30 ])
73+ let strom = WS.fromReadableStream rs :: Strom.Strom {} () Int
74+ result <- runOm $ Strom.runCollect strom
75+ result `shouldEqual` [ 10, 20, 30 ]
76+
77+ it "converts empty ReadableStream to empty Strom" do
78+ rs <- liftEffect $ WS.toReadableStream {} (Strom.empty :: Strom.Strom {} () Int)
79+ let strom = WS.fromReadableStream rs :: Strom.Strom {} () Int
80+ result <- runOm $ Strom.runCollect strom
81+ result `shouldEqual` ([] :: Array Int)
82+
83+ describe "fromReadableStreamWith" do
84+
85+ it "allows custom chunk size" do
86+ rs <- liftEffect $ WS.toReadableStream {} (Strom.fromArray [ 1, 2, 3, 4, 5 ])
87+ let strom = WS.fromReadableStreamWith { chunkSize: 2 } rs :: Strom.Strom {} () Int
88+ result <- runOm $ Strom.runCollect strom
89+ result `shouldEqual` [ 1, 2, 3, 4, 5 ]
90+
91+ describe "roundtrip" do
92+
93+ it "Strom → ReadableStream → Strom preserves elements" do
94+ let original = [ 1, 2, 3, 4, 5 ]
95+ rs <- liftEffect $ WS.toReadableStream {} (Strom.fromArray original)
96+ let strom = WS.fromReadableStream rs :: Strom.Strom {} () Int
97+ result <- runOm $ Strom.runCollect strom
98+ result `shouldEqual` original
99+
100+ it "roundtrip with transformations" do
101+ let original = Strom.fromArray [ 1, 2, 3 ] # Strom.filterStrom (_ > 1) # Strom.mapStrom (_ * 2)
102+ rs <- liftEffect $ WS.toReadableStream {} original
103+ let strom = WS.fromReadableStream rs :: Strom.Strom {} () Int
104+ result <- runOm $ Strom.runCollect strom
105+ result `shouldEqual` [ 4, 6 ]
106+
107+ it "large roundtrip preserves all 10000 elements" do
108+ let original = Array.range 1 10000
109+ rs <- liftEffect $ WS.toReadableStream {} (Strom.rangeStrom 1 10001)
110+ let strom = WS.fromReadableStream rs :: Strom.Strom {} () Int
111+ result <- runOm $ Strom.runCollect strom
112+ result `shouldEqual` original
113+
114+ describe "cancellation" do
115+
116+ it "cancelling ReadableStream does not hang" do
117+ rs <- liftEffect $ WS.toReadableStream {} (Strom.rangeStrom 1 1000000)
118+ -- Read 5 elements then cancel
119+ result <- readNThenCancel rs 5
120+ Array.length result `shouldEqual` 5
121+
122+ describe "runWritable" do
123+
124+ it "writes Strom elements to WritableStream" do
125+ { stream: ws, getValues } <- liftEffect _createWritableStreamCollector
126+ runOm $ WS.runWritable ws (Strom.fromArray [ 1, 2, 3 ])
127+ result <- liftEffect getValues
128+ result `shouldEqual` [ 1, 2, 3 ]
129+
130+ it "handles empty Strom to WritableStream" do
131+ { stream: ws, getValues } <- liftEffect (_createWritableStreamCollector :: Effect { stream :: WS.WritableStream Int, getValues :: Effect (Array Int) })
132+ runOm $ WS.runWritable ws (Strom.empty :: Strom.Strom {} () Int)
133+ result <- liftEffect getValues
134+ result `shouldEqual` ([] :: Array Int)
135+
136+ describe "pipeThrough" do
137+
138+ it "transforms ReadableStream through Strom pipeline" do
139+ -- Create a source ReadableStream
140+ source <- liftEffect $ WS.toReadableStream {} (Strom.fromArray [ 1, 2, 3, 4, 5 ])
141+ -- Pipe through a Strom transformation
142+ transformed <- liftEffect $ WS.pipeThrough {} (Strom.filterStrom (_ > 2) >>> Strom.mapStrom (_ * 10)) source
143+ -- Collect the result
144+ result <- collectReadableStream transformed
145+ result `shouldEqual` [ 30, 40, 50 ]
146+
147+ it "pipes through with take" do
148+ source <- liftEffect $ WS.toReadableStream {} (Strom.rangeStrom 1 100)
149+ transformed <- liftEffect $ WS.pipeThrough {} (Strom.takeStrom 3) source
150+ result <- collectReadableStream transformed
151+ result `shouldEqual` [ 1, 2, 3 ]
152+
153+ it "cancelling piped stream releases reader" do
154+ source <- liftEffect $ WS.toReadableStream {} (Strom.rangeStrom 1 1000000)
155+ transformed <- liftEffect $ WS.pipeThrough {} (Strom.mapStrom (_ * 2)) source
156+ -- Read 5 elements then cancel
157+ result <- readNThenCancel transformed 5
158+ Array.length result `shouldEqual` 5
159+
160+ describe "cancellation (batch read)" do
161+
162+ it "killing fiber during batch read stops the pump loop" do
163+ { stream, getProducedCount } <- liftEffect _createCountingStream
164+ -- Start reading in a fiber — chunkSize 10000 means pump loops many times
165+ let strom = WS.fromReadableStreamWith { chunkSize: 10000 } stream :: Strom.Strom {} () Int
166+ fiber <- forkAff $ runOm $ Strom.runCollect strom
167+ -- Let it produce some values (~50 reads at 1ms each)
168+ delay (Milliseconds 50.0)
169+ -- Kill the fiber — the cancelled flag stops the pump from further reads
170+ killFiber (Exception.error "test cancel") fiber
171+ -- Record count shortly after kill (allow one in-flight read to complete)
172+ delay (Milliseconds 10.0)
173+ countAtKill <- liftEffect getProducedCount
174+ -- Wait and check that the producer has stopped (count stabilised)
175+ delay (Milliseconds 100.0)
176+ countLater <- liftEffect getProducedCount
177+ -- With cancellation fix, the pump stops. At most 1 in-flight read
178+ -- may complete after the flag is set.
179+ (countLater - countAtKill) `shouldSatisfy` (_ < 5)
180+
181+ describe "useReadableStream" do
182+
183+ it "releases reader after full consumption" do
184+ rs <- liftEffect $ WS.toReadableStream {} (Strom.fromArray [ 1, 2, 3 ])
185+ result <- runOm $ WS.useReadableStream rs \strom ->
186+ Strom.runCollect strom
187+ result `shouldEqual` [ 1, 2, 3 ]
188+
189+ it "releases reader after partial consumption (takeStrom)" do
190+ { stream, getProducedCount: _ } <- liftEffect _createCountingStream
191+ result <- runOm $ WS.useReadableStreamWith { chunkSize: 10 } stream \strom ->
192+ Strom.runCollect (Strom.takeStrom 5 strom)
193+ Array.length result `shouldEqual` 5
194+ -- Wait for async reader.cancel() to complete
195+ delay (Milliseconds 50.0)
196+ -- Stream should be unlocked after useReadableStream returns
197+ locked <- liftEffect $ _isStreamLocked stream
198+ locked `shouldEqual` false
199+
200+ it "releases reader on fiber cancellation" do
201+ { stream, getProducedCount: _ } <- liftEffect _createCountingStream
202+ -- Fork useReadableStream that reads forever
203+ fiber <- forkAff $ runOm $ WS.useReadableStreamWith { chunkSize: 10 } stream \strom ->
204+ Strom.runCollect strom
205+ -- Let it start reading
206+ delay (Milliseconds 30.0)
207+ -- Kill the fiber — bracket cleanup should cancel the reader
208+ killFiber (Exception.error "test cancel") fiber
209+ -- Wait for async reader.cancel() to complete
210+ delay (Milliseconds 50.0)
211+ locked <- liftEffect $ _isStreamLocked stream
212+ locked `shouldEqual` false
213+
214+ describe "cancelStrom integration" do
215+
216+ it "cancelling ReadableStream releases bracket resources" do
217+ ref <- liftEffect $ Ref.new false
218+ let strom = Strom.bracket (pure unit) (\_ -> liftEffect $ Ref.write true ref) (\_ -> Strom.rangeStrom 1 1000000)
219+ rs <- liftEffect $ WS.toReadableStream {} strom
220+ _ <- readNThenCancel rs 5
221+ delay (Milliseconds 50.0)
222+ released <- liftEffect $ Ref.read ref
223+ released `shouldEqual` true