git.iko.soy/rowtype-yoga/purescript-yoga-om-strom
on 11398b0 help sign in
22 files changed+574−5067
since splitsnapshots
unifiedsplit
README.md−1 _excluded/Examples.purs−523 _excluded/FileStream.purs−37 _excluded/Strom.prop.purs−429 _excluded/Strom.test.purs−669 bun.lock+69−3 package.json+1−1 spago.lock+172−409 spago.yaml+18−14 src/Benchmark.purs+2−1 _excluded/Test/Main.purs → src/Test/Main.purs−4 src/Yoga/Om/Strom.purs+302−264 src/Yoga/Om/Strom.purs.backup−1809 src/Yoga/Om/Strom.test.purs+3−3 src/Yoga/Om/Strom/Examples.purs+3−3 src/Yoga/Om/Strom/FileStream.js−17 src/Yoga/Om/Strom/WebStream.js−176 src/Yoga/Om/Strom/WebStream.purs−360 test/Test/Main.purs−12 test/Test/Strom/Spec.purs+4−3 test/Test/WebStream.js−106 test/Test/WebStream.purs−223
README.md−1
@@ -441,4 +441,3 @@ This design enables:
441441## Credits
442442
443443Inspired by [ZIO ZStream](https://zio.dev/reference/stream/zstream) from the Scala ZIO ecosystem.
444−
_excluded/Examples.pursdeleted−523
@@ -1,523 +0,0 @@
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.pursdeleted−37
@@ -1,37 +0,0 @@
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.pursdeleted−429
@@ -1,429 +0,0 @@
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.pursdeleted−669
@@ -1,669 +0,0 @@
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
bun.lock+69−3
@@ -5,7 +5,7 @@
55 "name": "purescript-yoga-om-strom",
66 "devDependencies": {
77 "purescript": "^0.15.15",
8− "spago": "^1.0.3",
8+ "spago": "next",
99 },
1010 },
1111 },
@@ -54,14 +54,24 @@
5454
5555 "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
5656
57+ "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
58+
5759 "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
5860
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+
5967 "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="],
6068
6169 "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
6270
6371 "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
6472
73+ "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
74+
6575 "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
6676
6777 "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="],
@@ -100,12 +110,18 @@
100110
101111 "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
102112
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+
103117 "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="],
104118
105119 "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
106120
107121 "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
108122
123+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
124+
109125 "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=="],
110126
111127 "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
@@ -126,10 +142,14 @@
126142
127143 "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=="],
128144
145+ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
146+
129147 "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
130148
131149 "figgy-pudding": ["figgy-pudding@3.5.2", "", {}, "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw=="],
132150
151+ "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
152+
133153 "filesize": ["filesize@4.2.1", "", {}, "sha512-bP82Hi8VRZX/TUBKfE24iiUGsB/sfm2WUrwTQyAzQrhO3V9IhcBBNBXMyzLY5orACxRyYJ3d2HeRVX+eFv4lmA=="],
134154
135155 "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -140,6 +160,8 @@
140160
141161 "from2": ["from2@2.3.0", "", { "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" } }, "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g=="],
142162
163+ "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
164+
143165 "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=="],
144166
145167 "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
@@ -152,6 +174,8 @@
152174
153175 "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="],
154176
177+ "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
178+
155179 "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=="],
156180
157181 "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
@@ -168,6 +192,8 @@
168192
169193 "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
170194
195+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
196+
171197 "iferr": ["iferr@0.1.5", "", {}, "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA=="],
172198
173199 "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
@@ -180,6 +206,8 @@
180206
181207 "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
182208
209+ "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
210+
183211 "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
184212
185213 "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
@@ -226,6 +254,8 @@
226254
227255 "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
228256
257+ "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
258+
229259 "minimatch": ["minimatch@10.1.2", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.1" } }, "sha512-fu656aJ0n2kcXwsnwnv9g24tkU5uSmOlTjd6WyyaKm2Z+h1qmY6bAjrcaIxF/BslFqbZ8UBtbJi7KgQOZD2PTw=="],
230260
231261 "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
@@ -248,14 +278,20 @@
248278
249279 "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
250280
281+ "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
282+
251283 "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=="],
252284
253285 "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
254286
255287 "nan": ["nan@2.25.0", "", {}, "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g=="],
256288
289+ "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
290+
257291 "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
258292
293+ "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="],
294+
259295 "npm-run-path": ["npm-run-path@3.1.0", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg=="],
260296
261297 "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
@@ -280,6 +316,8 @@
280316
281317 "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
282318
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+
283321 "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
284322
285323 "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
@@ -300,6 +338,8 @@
300338
301339 "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
302340
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+
303343 "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=="],
304344
305345 "readline-sync": ["readline-sync@1.4.10", "", {}, "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw=="],
@@ -318,7 +358,7 @@
318358
319359 "run-queue": ["run-queue@1.0.3", "", { "dependencies": { "aproba": "^1.1.1" } }, "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg=="],
320360
321− "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
361+ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
322362
323363 "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
324364
@@ -330,6 +370,10 @@
330370
331371 "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
332372
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+
333377 "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=="],
334378
335379 "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
@@ -338,7 +382,7 @@
338382
339383 "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=="],
340384
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=="],
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=="],
342386
343387 "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
344388
@@ -366,16 +410,24 @@
366410
367411 "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
368412
413+ "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
414+
369415 "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
370416
371417 "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=="],
372418
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+
373423 "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="],
374424
375425 "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
376426
377427 "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
378428
429+ "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
430+
379431 "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
380432
381433 "type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="],
@@ -424,6 +476,8 @@
424476
425477 "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
426478
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+
427481 "cacache/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
428482
429483 "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=="],
@@ -468,10 +522,18 @@
468522
469523 "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=="],
470524
525+ "readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
526+
471527 "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=="],
472528
473529 "slice-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
474530
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+
475537 "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
476538
477539 "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
@@ -484,6 +546,8 @@
484546
485547 "@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=="],
486548
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+
487551 "cacache/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
488552
489553 "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@@ -528,6 +592,8 @@
528592
529593 "slice-ansi/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
530594
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+
531597 "wrap-ansi-cjs/ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
532598
533599 "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 @@
99 },
1010 "devDependencies": {
1111 "purescript": "^0.15.15",
12− "spago": "^1.0.3"
12+ "spago": "next"
1313 }
1414}
spago.lock+172−409
@@ -6,17 +6,15 @@
66 "core": {
77 "dependencies": [
88 {
9− "aff": ">=8.0.0 <9.0.0"
9+ "aff": ">=7.1.0 <8.0.0"
1010 },
1111 {
1212 "arrays": ">=7.3.0 <8.0.0"
1313 },
1414 {
15− "avar": ">=5.0.1 <6.0.0"
16− },
17− {
18− "catenable-lists": ">=7.0.0 <8.0.0"
15+ "avar": ">=5.0.0 <6.0.0"
1916 },
17+ "catenable-lists",
2018 {
2119 "console": ">=6.1.0 <7.0.0"
2220 },
@@ -33,14 +31,12 @@
3331 "either": ">=6.1.0 <7.0.0"
3432 },
3533 {
36− "exceptions": ">=6.1.0 <7.0.0"
34+ "exceptions": ">=6.0.0 <7.0.0"
3735 },
3836 {
3937 "foldable-traversable": ">=6.0.0 <7.0.0"
4038 },
41− {
42− "integers": ">=6.0.0 <7.0.0"
43− },
39+ "integers",
4440 {
4541 "lists": ">=7.0.0 <8.0.0"
4642 },
@@ -48,45 +44,163 @@
4844 "maybe": ">=6.0.0 <7.0.0"
4945 },
5046 {
51− "now": ">=6.0.0 <7.0.0"
47+ "minibench": ">=4.0.1 <5.0.0"
5248 },
5349 {
54− "prelude": ">=6.0.2 <7.0.0"
50+ "newtype": ">=5.0.0 <6.0.0"
5551 },
5652 {
57− "refs": ">=6.0.0 <7.0.0"
53+ "now": ">=6.0.0 <7.0.0"
5854 },
5955 {
60− "spec": ">=8.1.2 <9.0.0"
56+ "partial": ">=4.0.0 <5.0.0"
6157 },
6258 {
63− "st": ">=6.2.0 <7.0.0"
59+ "prelude": ">=6.0.1 <7.0.0"
6460 },
6561 {
66− "tailrec": ">=6.1.0 <7.0.0"
62+ "refs": ">=6.0.0 <7.0.0"
6763 },
6864 {
69− "transformers": ">=6.1.0 <7.0.0"
65+ "spec": ">=7.6.0 <8.0.0"
7066 },
67+ "st",
68+ "tailrec",
69+ "transformers",
7170 {
7271 "tuples": ">=7.0.0 <8.0.0"
7372 },
73+ {
74+ "typelevel-prelude": ">=7.0.0 <8.0.0"
75+ },
7476 {
7577 "variant": ">=8.0.0 <9.0.0"
7678 },
7779 {
78− "yoga-om": ">=1.5.2 <2.0.0"
80+ "yoga-om-core": ">=1.0.5 <2.0.0"
7981 }
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"
80144 ]
81145 },
82146 "test": {
83147 "dependencies": [
84148 {
85− "spec": ">=8.0.0 <9.0.0"
86− },
87− {
88− "spec-node": ">=0.0.3 <0.1.0"
149+ "spec": ">=7.0.0 <8.0.0"
89150 }
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"
90204 ]
91205 }
92206 }
@@ -96,45 +210,28 @@
96210 "packages": {
97211 "aff": {
98212 "type": "registry",
99− "version": "8.0.0",
100− "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
213+ "version": "7.1.0",
214+ "integrity": "sha256-/nOpDQVnKIoP1pQ1ZZBYpadK0TuMlG4WwufGzJpsfD0=",
101215 "dependencies": [
216+ "arrays",
217+ "bifunctors",
102218 "control",
103219 "datetime",
104220 "effect",
105221 "either",
106222 "exceptions",
223+ "foldable-traversable",
107224 "functions",
225+ "maybe",
108226 "newtype",
109227 "parallel",
110− "partial",
111228 "prelude",
112− "st",
229+ "refs",
113230 "tailrec",
114231 "transformers",
115232 "unsafe-coerce"
116233 ]
117234 },
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− },
138235 "ansi": {
139236 "type": "registry",
140237 "version": "7.0.0",
@@ -142,56 +239,9 @@
142239 "dependencies": [
143240 "foldable-traversable",
144241 "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",
185− "strings",
186− "tailrec"
242+ "strings"
187243 ]
188244 },
189− "arraybuffer-types": {
190− "type": "registry",
191− "version": "3.0.2",
192− "integrity": "sha256-p05cJnSkyeoB7VHzMyc2Eb1RwUaB7Nl0sQSqEGNEUD8=",
193− "dependencies": []
194− },
195245 "arrays": {
196246 "type": "registry",
197247 "version": "7.3.0",
@@ -223,8 +273,7 @@
223273 "either",
224274 "exceptions",
225275 "functions",
226− "maybe",
227− "prelude"
276+ "maybe"
228277 ]
229278 },
230279 "bifunctors": {
@@ -383,16 +432,6 @@
383432 "unsafe-coerce"
384433 ]
385434 },
386− "exitcodes": {
387− "type": "registry",
388− "version": "4.0.0",
389− "integrity": "sha256-xBl4aFSEQ05Owp1ZvO6OxxvLK3LVzU35RSbj79SsePQ=",
390− "dependencies": [
391− "enums",
392− "maybe",
393− "prelude"
394− ]
395− },
396435 "foldable-traversable": {
397436 "type": "registry",
398437 "version": "6.0.0",
@@ -411,50 +450,12 @@
411450 "tuples"
412451 ]
413452 },
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− },
450453 "fork": {
451454 "type": "registry",
452455 "version": "6.0.0",
453456 "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
454457 "dependencies": [
455− "aff",
456− "prelude",
457− "transformers"
458+ "aff"
458459 ]
459460 },
460461 "free": {
@@ -567,21 +568,6 @@
567568 "prelude"
568569 ]
569570 },
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− },
585571 "lazy": {
586572 "type": "registry",
587573 "version": "6.0.0",
@@ -623,21 +609,27 @@
623609 "prelude"
624610 ]
625611 },
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+ },
626625 "mmorph": {
627626 "type": "registry",
628627 "version": "7.0.0",
629628 "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
630629 "dependencies": [
631− "bifunctors",
632− "either",
633630 "free",
634631 "functors",
635− "identity",
636− "maybe",
637− "newtype",
638− "prelude",
639− "transformers",
640− "tuples"
632+ "transformers"
641633 ]
642634 },
643635 "newtype": {
@@ -649,107 +641,6 @@
649641 "safe-coerce"
650642 ]
651643 },
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− },
753644 "nonempty": {
754645 "type": "registry",
755646 "version": "7.0.0",
@@ -769,18 +660,7 @@
769660 "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
770661 "dependencies": [
771662 "datetime",
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"
663+ "effect"
784664 ]
785665 },
786666 "numbers": {
@@ -789,58 +669,7 @@
789669 "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
790670 "dependencies": [
791671 "functions",
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"
672+ "maybe"
844673 ]
845674 },
846675 "ordered-collections": {
@@ -849,16 +678,13 @@
849678 "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
850679 "dependencies": [
851680 "arrays",
852− "control",
853681 "foldable-traversable",
854− "functions",
855682 "gen",
856683 "lists",
857684 "maybe",
858− "newtype",
859685 "partial",
860686 "prelude",
861− "safe-coerce",
687+ "st",
862688 "tailrec",
863689 "tuples",
864690 "unfoldable"
@@ -875,8 +701,8 @@
875701 },
876702 "parallel": {
877703 "type": "registry",
878− "version": "7.0.0",
879− "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=",
704+ "version": "6.0.0",
705+ "integrity": "sha256-1dpN/7kBct/2aONnyk4TU0AFN71GvfoRxrqvFGRU4Os=",
880706 "dependencies": [
881707 "control",
882708 "effect",
@@ -903,31 +729,14 @@
903729 "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
904730 "dependencies": [
905731 "aff",
906− "control",
907− "effect",
908− "either",
909− "foldable-traversable",
910− "identity",
911732 "lists",
912− "maybe",
913733 "mmorph",
914− "newtype",
915734 "prelude",
916735 "tailrec",
917736 "transformers",
918737 "tuples"
919738 ]
920739 },
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− },
931740 "prelude": {
932741 "type": "registry",
933742 "version": "6.0.2",
@@ -949,16 +758,6 @@
949758 "tuples"
950759 ]
951760 },
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− },
962761 "record": {
963762 "type": "registry",
964763 "version": "4.0.0",
@@ -1001,8 +800,8 @@
1001800 },
1002801 "spec": {
1003802 "type": "registry",
1004− "version": "8.1.2",
1005− "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=",
803+ "version": "7.6.1",
804+ "integrity": "sha256-5GysDOS1gVbbGX75YXUdhww3h2S246Ck0sS6yt+ig+0=",
1006805 "dependencies": [
1007806 "aff",
1008807 "ansi",
@@ -1033,44 +832,11 @@
1033832 "tuples"
1034833 ]
1035834 },
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− },
1068835 "st": {
1069836 "type": "registry",
1070837 "version": "6.2.0",
1071838 "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
1072839 "dependencies": [
1073− "effect",
1074840 "partial",
1075841 "prelude",
1076842 "tailrec",
@@ -1202,24 +968,22 @@
1202968 "version": "8.0.0",
1203969 "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1204970 "dependencies": [
1205− "control",
1206971 "enums",
1207− "foldable-traversable",
1208972 "lists",
1209973 "maybe",
1210974 "partial",
1211975 "prelude",
1212− "type-equality",
976+ "record",
977+ "tuples",
1213978 "unsafe-coerce"
1214979 ]
1215980 },
1216− "yoga-om": {
981+ "yoga-om-core": {
1217982 "type": "registry",
1218− "version": "1.5.2",
1219− "integrity": "sha256-2ys14BF6NPgzFDFUIFk0EDxmqkA8+yghIA9TXHlOmE0=",
983+ "version": "1.0.5",
984+ "integrity": "sha256-YMJKVwUsmLOoGE3sQr/+xc5F8OsV5j+xKJa4eDYBv48=",
1220985 "dependencies": [
1221986 "aff",
1222− "aff-retry",
1223987 "avar",
1224988 "console",
1225989 "control",
@@ -1228,14 +992,13 @@
1228992 "either",
1229993 "exceptions",
1230994 "functions",
1231− "heterogeneous",
1232995 "maybe",
1233996 "newtype",
1234997 "parallel",
1235998 "prelude",
1236999 "record",
12371000 "record-studio",
1238− "refs",
1001+ "spec",
12391002 "tailrec",
12401003 "transformers",
12411004 "tuples",
spago.yaml+18−14
@@ -1,8 +1,9 @@
11workspace: {}
2+
23package:
34 name: yoga-om-strom
45 publish:
5− version: 0.4.3
6+ version: 0.1.1
67 license: MIT-0
78 exclude:
89 - "src/**/*.test.purs"
@@ -11,32 +12,35 @@ package:
1112 githubOwner: rowtype-yoga
1213 githubRepo: purescript-yoga-om-strom
1314 dependencies:
14− - aff: ">=8.0.0 <9.0.0"
15+ - catenable-lists
16+ - integers
17+ - st
18+ - tailrec
19+ - transformers
20+ - aff: ">=7.1.0 <8.0.0"
1521 - arrays: ">=7.3.0 <8.0.0"
16− - avar: ">=5.0.1 <6.0.0"
17− - catenable-lists: ">=7.0.0 <8.0.0"
22+ - avar: ">=5.0.0 <6.0.0"
1823 - console: ">=6.1.0 <7.0.0"
1924 - control: ">=6.0.0 <7.0.0"
2025 - datetime: ">=6.1.0 <7.0.0"
2126 - effect: ">=4.0.0 <5.0.0"
2227 - either: ">=6.1.0 <7.0.0"
23− - exceptions: ">=6.1.0 <7.0.0"
28+ - exceptions: ">=6.0.0 <7.0.0"
2429 - foldable-traversable: ">=6.0.0 <7.0.0"
25− - integers: ">=6.0.0 <7.0.0"
2630 - lists: ">=7.0.0 <8.0.0"
2731 - maybe: ">=6.0.0 <7.0.0"
32+ - minibench: ">=4.0.1 <5.0.0"
33+ - newtype: ">=5.0.0 <6.0.0"
2834 - now: ">=6.0.0 <7.0.0"
29− - prelude: ">=6.0.2 <7.0.0"
35+ - partial: ">=4.0.0 <5.0.0"
36+ - prelude: ">=6.0.1 <7.0.0"
3037 - refs: ">=6.0.0 <7.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"
38+ - spec: ">=7.6.0 <8.0.0"
3539 - tuples: ">=7.0.0 <8.0.0"
40+ - typelevel-prelude: ">=7.0.0 <8.0.0"
3641 - variant: ">=8.0.0 <9.0.0"
37− - yoga-om: ">=1.5.2 <2.0.0"
42+ - yoga-om-core: ">=1.0.5 <2.0.0"
3843 test:
3944 main: Test.Main.Strom
4045 dependencies:
41− - spec: ">=8.0.0 <9.0.0"
42− - spec-node: ">=0.0.3 <0.1.0"
46+ - spec: ">=7.0.0 <8.0.0"
src/Benchmark.purs+2−1
@@ -10,9 +10,10 @@ import Data.Tuple (Tuple(..))
1010import Effect (Effect)
1111import Effect.Aff (Aff, launchAff_, throwError)
1212import Effect.Class (liftEffect)
13+import Effect.Exception (Error)
1314import Effect.Class.Console as Console
1415import Effect.Now (now)
15−import Data.DateTime.Instant (diff)
16+import Data.DateTime.Instant (unInstant, diff)
1617import Yoga.Om as Om
1718import Yoga.Om.Strom as Strom
1819
_excluded/Test/Main.purs → src/Test/Main.pursrenamed−4
@@ -6,12 +6,8 @@ import Effect (Effect)
66import Effect.Aff (launchAff_)
77import Test.Spec.Reporter (consoleReporter)
88import Test.Spec.Runner (runSpec)
9−import Test.WebStream as WebStreamSpec
109import Yoga.Om.Strom.Test as StromSpec
11−import Yoga.Om.Strom.PropTest as StromPropSpec
1210
1311main :: Effect Unit
1412main = launchAff_ $ runSpec [consoleReporter] do
1513 StromSpec.spec
16− WebStreamSpec.spec
17− StromPropSpec.spec
src/Yoga/Om/Strom.purs+302−264
@@ -65,7 +65,6 @@ module Yoga.Om.Strom
6565 , partitionMap
6666 -- Timing
6767 , debounce
68− , debounceEach
6968 , throttle
7069 , delayStrom
7170 -- Combining
@@ -109,9 +108,6 @@ module Yoga.Om.Strom
109108 , bracket
110109 , bracketExit
111110 , acquireRelease
112− -- Internal (needed by WebStream)
113− , mkStrom
114− , runStrom
115111 ) where
116112
117113import Prelude
@@ -133,16 +129,18 @@ import Data.List as List
133129import Data.Maybe (Maybe(..))
134130import Data.Time.Duration (Milliseconds(..))
135131import Data.Traversable (traverse, traverse_)
136−import Data.Tuple.Nested (type (/\), (/\))
132+import Data.Tuple (Tuple(..))
137133import Data.Variant (Variant)
138−import Effect.Aff (Aff, Error, delay, forkAff, killFiber)
134+import Effect.Aff (Aff, Error, delay, forkAff, joinFiber, killFiber)
139135import Effect.Aff.AVar as AVar
140136import Effect.Aff.Class (liftAff)
141137import Effect.Exception as Exception
138+import Effect.Now (now)
142139import Effect.Ref as Ref
143140import Effect.Class (liftEffect)
144141import Data.CatQueue as CatQueue
145142import Data.CatQueue (CatQueue)
143+import Data.DateTime.Instant (unInstant)
146144import Yoga.Om (Om)
147145import Yoga.Om as Om
148146
@@ -162,18 +160,18 @@ import Yoga.Om as Om
162160-- | - Loop (chunk, next) => emit chunk (if present) and continue with next
163161-- | - Done finalChunk => emit final chunk (if present) and stop
164162newtype Strom ctx err a = Strom
165− { pull :: Om ctx err (Step (((Maybe (Chunk a)) /\ (Strom ctx err a))) (Maybe (Chunk a)))
163+ { pull :: Om ctx err (Step (Tuple (Maybe (Chunk a)) (Strom ctx err a)) (Maybe (Chunk a)))
166164 }
167165
168166-- | Chunks for efficient batch processing
169167type Chunk a = Array a
170168
171169-- | Helper to create a Strom from a pull function
172−mkStrom :: forall ctx err a. Om ctx err (Step (((Maybe (Chunk a)) /\ (Strom ctx err a))) (Maybe (Chunk a))) -> Strom ctx err a
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
173171mkStrom pull = Strom { pull }
174172
175173-- | Helper to unwrap a Strom
176−runStrom :: forall ctx err a. Strom ctx err a -> Om ctx err (Step (((Maybe (Chunk a)) /\ (Strom ctx err a))) (Maybe (Chunk a)))
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)))
177175runStrom (Strom s) = s.pull
178176
179177--------------------------------------------------------------------------------
@@ -183,22 +181,22 @@ runStrom (Strom s) = s.pull
183181-- | Stream identifier for tracking which stream produced data
184182data StreamId = StreamId1 | StreamId2
185183
186−derive instance Eq StreamId
187−derive instance Ord StreamId
184+derive instance eqStreamId :: Eq StreamId
185+derive instance ordStreamId :: Ord StreamId
188186
189187-- | Messages passed through the coordination queue
190188data QueueItem a
191189 = DataChunk (Array a) StreamId
192190 | StreamDone StreamId
193191
194−derive instance Functor QueueItem
192+derive instance functorQueueItem :: Functor QueueItem
195193
196194-- | Messages for mergeAllND buffered queue
197195data MergeAllItem a
198196 = MergeChunk (Array a)
199197 | MergeDone
200198
201−derive instance Functor MergeAllItem
199+derive instance functorMergeAllItem :: Functor MergeAllItem
202200
203201-- | Merge termination strategy (like ZStream HaltStrategy)
204202data HaltStrategy
@@ -207,14 +205,14 @@ data HaltStrategy
207205 | HaltLeft
208206 | HaltRight
209207
210−derive instance Eq HaltStrategy
208+derive instance eqHaltStrategy :: Eq HaltStrategy
211209
212210-- | Messages for buffering queues
213211data BufferItem a
214212 = BufferChunk (Array a)
215213 | BufferDone
216214
217−derive instance Functor BufferItem
215+derive instance functorBufferItem :: Functor BufferItem
218216
219217--------------------------------------------------------------------------------
220218-- Construction
@@ -270,7 +268,7 @@ rangeStrom start end = rangeHelper start end
270268 chunkEnd = min (current + chunkSize) limit
271269 chunk = Array.range current (chunkEnd - 1)
272270 if chunkEnd >= limit then pure $ Done $ Just chunk -- Final chunk
273− else pure $ Loop $ ((Just chunk) /\ (rangeHelper chunkEnd limit)) -- Emit chunk and continue
271+ else pure $ Loop $ Tuple (Just chunk) (rangeHelper chunkEnd limit) -- Emit chunk and continue
274272
275273-- | Iterate producing values - limited to ~10,000 elements for stack safety
276274-- | For truly infinite streams, use `iterateStromInfinite` (with Aff overhead)
@@ -308,7 +306,7 @@ iterateStromInfinite f initial = iterateHelper initial
308306 finalVal <- STRef.read valRef
309307 frozen <- STArray.unsafeFreeze arr
310308 pure { arr: frozen, nextVal: finalVal }
311− pure $ Loop $ ((Just result.arr) /\ (iterateHelper result.nextVal))
309+ pure $ Loop $ Tuple (Just result.arr) (iterateHelper result.nextVal)
312310
313311-- | Repeat a value - limited to ~10,000 elements for stack safety
314312repeatStrom :: forall ctx err a. a -> Strom ctx err a
@@ -323,7 +321,7 @@ repeatStromInfinite a = repeatHelper unit
323321 -- Async boundary resets the stack!
324322 liftAff $ delay (Milliseconds 0.0)
325323 let chunk = Array.replicate 10000 a
326− pure $ Loop $ ((Just chunk) /\ (repeatHelper unit))
324+ pure $ Loop $ Tuple (Just chunk) (repeatHelper unit)
327325
328326-- | Repeat an Om computation - limited to 100 iterations for stack safety
329327-- | For truly infinite repetition, use `repeatOmStromInfinite` (with Aff overhead)
@@ -335,7 +333,7 @@ repeatOmStrom om = repeatOmHelper 0
335333 | count >= maxCount = empty
336334 | otherwise = mkStrom do
337335 value <- om
338− pure $ Loop $ ((Just [ value ]) /\ (repeatOmHelper (count + 1)))
336+ pure $ Loop $ Tuple (Just [ value ]) (repeatOmHelper (count + 1))
339337
340338-- | Truly infinite Om repetition - stack-safe via Aff async boundaries
341339-- | Adds tiny delay (0ms) at each pull to reset the stack
@@ -346,11 +344,11 @@ repeatOmStromInfinite om = repeatOmHelper unit
346344 -- Async boundary resets the stack!
347345 liftAff $ delay (Milliseconds 0.0)
348346 value <- om
349− pure $ Loop $ ((Just [ value ]) /\ (repeatOmHelper unit))
347+ pure $ Loop $ Tuple (Just [ value ]) (repeatOmHelper unit)
350348
351349-- | Unfold a stream from a seed value - can be infinite if f never returns Nothing
352350-- | Emits chunks lazily, terminates when f returns Nothing
353−unfoldStrom :: forall ctx err a b. (b -> Maybe ((a /\ b))) -> b -> Strom ctx err a
351+unfoldStrom :: forall ctx err a b. (b -> Maybe (Tuple a b)) -> b -> Strom ctx err a
354352unfoldStrom f seed = unfoldHelper seed
355353 where
356354 unfoldHelper currentSeed = mkStrom do
@@ -359,7 +357,7 @@ unfoldStrom f seed = unfoldHelper seed
359357 Nothing -> pure $ Done $ if Array.null chunk.values then Nothing else Just chunk.values
360358 Just nextSeed ->
361359 if Array.null chunk.values then runStrom (unfoldHelper nextSeed)
362− else pure $ Loop $ ((Just chunk.values) /\ (unfoldHelper nextSeed))
360+ else pure $ Loop $ Tuple (Just chunk.values) (unfoldHelper nextSeed)
363361
364362 buildChunk seed' size = ST.run do
365363 arr <- STArray.unsafeThaw []
@@ -372,7 +370,7 @@ unfoldStrom f seed = unfoldHelper seed
372370 s <- STRef.read seedRef
373371 case f s of
374372 Nothing -> void $ STRef.write true stoppedRef
375− Just ((value /\ nextS)) -> do
373+ Just (Tuple value nextS) -> do
376374 void $ STArray.push value arr
377375 void $ STRef.write nextS seedRef
378376 frozen <- STArray.unsafeFreeze arr
@@ -381,14 +379,14 @@ unfoldStrom f seed = unfoldHelper seed
381379 pure { values: frozen, finalSeed: if stopped then Nothing else Just finalSeed }
382380
383381-- | Unfold a stream with an effectful function
384−unfoldOmStrom :: forall ctx err a b. (b -> Om ctx err (Maybe ((a /\ b)))) -> b -> Strom ctx err a
382+unfoldOmStrom :: forall ctx err a b. (b -> Om ctx err (Maybe (Tuple a b))) -> b -> Strom ctx err a
385383unfoldOmStrom f seed = mkStrom go
386384 where
387385 go = do
388386 result <- f seed
389387 case result of
390388 Nothing -> pure $ Done Nothing
391− Just ((value /\ nextSeed)) -> pure $ Loop $ ((Just [ value ]) /\ (unfoldOmStrom f nextSeed))
389+ Just (Tuple value nextSeed) -> pure $ Loop $ Tuple (Just [ value ]) (unfoldOmStrom f nextSeed)
392390
393391--------------------------------------------------------------------------------
394392-- Transformations
@@ -401,7 +399,7 @@ mapStrom f stream = mkStrom do
401399 case step of
402400 Done Nothing -> pure $ Done Nothing
403401 Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
404− Loop ((maybeChunk /\ next)) -> pure $ Loop $ ((Functor.map (Functor.map f) maybeChunk) /\ (mapStrom f next))
402+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple (Functor.map (Functor.map f) maybeChunk) (mapStrom f next)
405403
406404-- | Map with a monadic effect
407405mapMStrom :: forall ctx err a b. (a -> Om ctx err b) -> Strom ctx err a -> Strom ctx err b
@@ -412,11 +410,11 @@ mapMStrom f stream = mkStrom do
412410 Done (Just chunk) -> do
413411 mapped <- traverse f chunk
414412 pure $ Done $ Just mapped
415− Loop ((maybeChunk /\ next)) -> do
413+ Loop (Tuple maybeChunk next) -> do
416414 mappedChunk <- case maybeChunk of
417415 Nothing -> pure Nothing
418416 Just chunk -> Just <$> traverse f chunk
419− pure $ Loop $ (mappedChunk /\ (mapMStrom f next))
417+ pure $ Loop $ Tuple mappedChunk (mapMStrom f next)
420418
421419-- | Tap (observe without modifying)
422420tapStrom :: forall ctx err a. (a -> Unit) -> Strom ctx err a -> Strom ctx err a
@@ -427,15 +425,16 @@ tapStrom f stream = mkStrom do
427425 Done (Just chunk) -> do
428426 let _ = Functor.map f chunk
429427 pure $ Done $ Just chunk
430− Loop ((maybeChunk /\ next)) -> do
428+ Loop (Tuple maybeChunk next) -> do
431429 let
432430 tappedChunk = case maybeChunk of
433431 Nothing -> Nothing
434432 Just chunk ->
435− do
436− let _ = Functor.map f chunk
433+ let
434+ _ = Functor.map f chunk
435+ in
437436 Just chunk
438− pure $ Loop $ (tappedChunk /\ (tapStrom f next))
437+ pure $ Loop $ Tuple tappedChunk (tapStrom f next)
439438
440439-- | Tap with monadic effect (observe with effects)
441440tapMStrom :: forall ctx err a. (a -> Om ctx err Unit) -> Strom ctx err a -> Strom ctx err a
@@ -446,20 +445,20 @@ tapMStrom f stream = mkStrom do
446445 Done (Just chunk) -> do
447446 _ <- traverse f chunk
448447 pure $ Done $ Just chunk
449− Loop ((maybeChunk /\ next)) -> do
448+ Loop (Tuple maybeChunk next) -> do
450449 tappedChunk <- case maybeChunk of
451450 Nothing -> pure Nothing
452451 Just chunk -> do
453452 _ <- traverse f chunk
454453 pure $ Just chunk
455− pure $ Loop $ (tappedChunk /\ (tapMStrom f next))
454+ pure $ Loop $ Tuple tappedChunk (tapMStrom f next)
456455
457456-- | Bind for streams (monadic flatMap/chain)
458457-- | Use >>= operator for cleaner syntax: `stream >>= f`
459458bindStrom :: forall ctx err a b. (a -> Strom ctx err b) -> Strom ctx err a -> Strom ctx err b
460459bindStrom f stream = stream >>= f
461460
462−-- | Scan with accumulator (like foldl but emits intermediate results)
461+-- | Scan with accumulator (like foldl but emits intermediate results)
463462scanStrom :: forall ctx err a b. (b -> a -> b) -> b -> Strom ctx err a -> Strom ctx err b
464463scanStrom f initial stream = scanHelper initial stream
465464 where
@@ -470,33 +469,35 @@ scanStrom f initial stream = scanHelper initial stream
470469 Done (Just chunk) -> do
471470 -- Build results by folding and collecting intermediate values
472471 let
473− (_ /\ results) = Array.foldl
474− ( \((prevAcc /\ accList)) item ->
475− do
476− let newAcc = f prevAcc item
477− (newAcc /\ (Array.snoc accList newAcc))
472+ Tuple _ results = Array.foldl
473+ ( \(Tuple prevAcc accList) item ->
474+ let
475+ newAcc = f prevAcc item
476+ in
477+ Tuple newAcc (Array.snoc accList newAcc)
478478 )
479− (acc /\ [])
479+ (Tuple acc [])
480480 chunk
481481 pure $ Done $ if Array.null results then Nothing else Just results
482− Loop ((maybeChunk /\ next)) ->
482+ Loop (Tuple maybeChunk next) ->
483483 case maybeChunk of
484− Nothing -> pure $ Loop $ (Nothing /\ (scanHelper acc next))
484+ Nothing -> pure $ Loop $ Tuple Nothing (scanHelper acc next)
485485 Just chunk -> do
486486 let
487− (newAcc /\ results) = Array.foldl
488− ( \((prevAcc /\ accList)) item ->
489− do
490− let currAcc = f prevAcc item
491− (currAcc /\ (Array.snoc accList currAcc))
487+ Tuple newAcc results = Array.foldl
488+ ( \(Tuple prevAcc accList) item ->
489+ let
490+ currAcc = f prevAcc item
491+ in
492+ Tuple currAcc (Array.snoc accList currAcc)
492493 )
493− (acc /\ [])
494+ (Tuple acc [])
494495 chunk
495− if Array.null results then pure $ Loop $ (Nothing /\ (scanHelper newAcc next))
496− else pure $ Loop $ ((Just results) /\ (scanHelper newAcc next))
496+ if Array.null results then pure $ Loop $ Tuple Nothing (scanHelper newAcc next)
497+ else pure $ Loop $ Tuple (Just results) (scanHelper newAcc next)
497498
498499-- | Map with accumulator (stateful map)
499−mapAccumStrom :: forall ctx err a b s. (s -> a -> (s /\ b)) -> s -> Strom ctx err a -> Strom ctx err b
500+mapAccumStrom :: forall ctx err a b s. (s -> a -> Tuple s b) -> s -> Strom ctx err a -> Strom ctx err b
500501mapAccumStrom f initial stream = mapAccumHelper initial stream
501502 where
502503 mapAccumHelper state s = mkStrom do
@@ -505,30 +506,32 @@ mapAccumStrom f initial stream = mapAccumHelper initial stream
505506 Done Nothing -> pure $ Done Nothing
506507 Done (Just chunk) -> do
507508 let
508− (_ /\ results) = Array.foldl
509− ( \((st /\ acc)) a ->
510− do
511− let (newSt /\ b) = f st a
512− (newSt /\ (Array.snoc acc b))
509+ Tuple finalState results = Array.foldl
510+ ( \(Tuple st acc) a ->
511+ let
512+ Tuple newSt b = f st a
513+ in
514+ Tuple newSt (Array.snoc acc b)
513515 )
514− (state /\ [])
516+ (Tuple state [])
515517 chunk
516518 pure $ Done $ if Array.null results then Nothing else Just results
517− Loop ((maybeChunk /\ next)) ->
519+ Loop (Tuple maybeChunk next) ->
518520 case maybeChunk of
519− Nothing -> pure $ Loop $ (Nothing /\ (mapAccumHelper state next))
521+ Nothing -> pure $ Loop $ Tuple Nothing (mapAccumHelper state next)
520522 Just chunk -> do
521523 let
522− (newState /\ results) = Array.foldl
523− ( \((st /\ acc)) a ->
524− do
525− let (newSt /\ b) = f st a
526− (newSt /\ (Array.snoc acc b))
524+ Tuple newState results = Array.foldl
525+ ( \(Tuple st acc) a ->
526+ let
527+ Tuple newSt b = f st a
528+ in
529+ Tuple newSt (Array.snoc acc b)
527530 )
528− (state /\ [])
531+ (Tuple state [])
529532 chunk
530− if Array.null results then pure $ Loop $ (Nothing /\ (mapAccumHelper newState next))
531− else pure $ Loop $ ((Just results) /\ (mapAccumHelper newState next))
533+ if Array.null results then pure $ Loop $ Tuple Nothing (mapAccumHelper newState next)
534+ else pure $ Loop $ Tuple (Just results) (mapAccumHelper newState next)
532535
533536-- | Filter elements
534537filterStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -539,15 +542,16 @@ filterStrom predicate stream = mkStrom do
539542 Done (Just chunk) -> do
540543 let filtered = Array.filter predicate chunk
541544 pure $ Done $ if Array.null filtered then Nothing else Just filtered
542− Loop ((maybeChunk /\ next)) -> do
545+ Loop (Tuple maybeChunk next) -> do
543546 let
544547 filteredChunk = case maybeChunk of
545548 Nothing -> Nothing
546549 Just chunk ->
547− do
548− let filtered = Array.filter predicate chunk
550+ let
551+ filtered = Array.filter predicate chunk
552+ in
549553 if Array.null filtered then Nothing else Just filtered
550− pure $ Loop $ (filteredChunk /\ (filterStrom predicate next))
554+ pure $ Loop $ Tuple filteredChunk (filterStrom predicate next)
551555
552556--------------------------------------------------------------------------------
553557-- Selection
@@ -569,17 +573,18 @@ takeStrom n stream = takeHelper n stream
569573 numTaken = Array.length taken
570574 if numTaken == 0 then pure $ Done Nothing
571575 else pure $ Done $ Just taken
572− Loop ((maybeChunk /\ next)) ->
576+ Loop (Tuple maybeChunk next) ->
573577 case maybeChunk of
574− Nothing -> pure $ Loop $ (Nothing /\ (takeHelper remaining next))
578+ Nothing -> pure $ Loop $ Tuple Nothing (takeHelper remaining next)
575579 Just chunk -> do
576580 let
581+ chunkLen = Array.length chunk
577582 taken = Array.take remaining chunk
578583 numTaken = Array.length taken
579584 newRemaining = remaining - numTaken
580585 if numTaken == 0 then pure $ Done Nothing
581586 else if newRemaining <= 0 then pure $ Done $ Just taken
582− else pure $ Loop $ ((Just taken) /\ (takeHelper newRemaining next))
587+ else pure $ Loop $ Tuple (Just taken) (takeHelper newRemaining next)
583588
584589-- | Take while predicate is true
585590takeWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -591,7 +596,7 @@ takeWhileStrom predicate stream = mkStrom do
591596 let taken = Array.takeWhile predicate chunk
592597 if Array.length taken < Array.length chunk then pure $ Done $ Just taken
593598 else pure $ Done $ Just taken
594− Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (takeWhileStrom predicate next))
599+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (takeWhileStrom predicate next)
595600
596601-- | Drop n elements (with proper state tracking across chunks)
597602dropStrom :: forall ctx err a. Int -> Strom ctx err a -> Strom ctx err a
@@ -608,17 +613,19 @@ dropStrom n stream
608613 Done (Just chunk) -> do
609614 let dropped = Array.drop remaining chunk
610615 pure $ Done $ if Array.null dropped then Nothing else Just dropped
611− Loop ((maybeChunk /\ next)) ->
616+ Loop (Tuple maybeChunk next) ->
612617 case maybeChunk of
613− Nothing -> pure $ Loop $ (Nothing /\ (dropHelper remaining next))
618+ Nothing -> pure $ Loop $ Tuple Nothing (dropHelper remaining next)
614619 Just chunk ->
615− do
616− let chunkLen = Array.length chunk
617− if remaining >= chunkLen then pure $ Loop $ (Nothing /\ (dropHelper (remaining - chunkLen) next))
620+ let
621+ chunkLen = Array.length chunk
622+ in
623+ if remaining >= chunkLen then pure $ Loop $ Tuple Nothing (dropHelper (remaining - chunkLen) next)
618624 else
619− do
620− let dropped = Array.drop remaining chunk
621− pure $ Loop $ ((Just dropped) /\ next)
625+ let
626+ dropped = Array.drop remaining chunk
627+ in
628+ pure $ Loop $ Tuple (Just dropped) next
622629
623630-- | Take until predicate is true (includes the matching element)
624631takeUntilStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -630,12 +637,12 @@ takeUntilStrom predicate stream = mkStrom do
630637 case Array.findIndex predicate chunk of
631638 Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Include the matching element
632639 Nothing -> pure $ Done $ Just chunk
633− Loop ((maybeChunk /\ next)) ->
640+ Loop (Tuple maybeChunk next) ->
634641 case maybeChunk of
635− Nothing -> pure $ Loop $ (Nothing /\ (takeUntilStrom predicate next))
642+ Nothing -> pure $ Loop $ Tuple Nothing (takeUntilStrom predicate next)
636643 Just chunk -> case Array.findIndex predicate chunk of
637644 Just idx -> pure $ Done $ Just $ Array.take (idx + 1) chunk -- Found match, stop here
638− Nothing -> pure $ Loop $ ((Just chunk) /\ (takeUntilStrom predicate next)) -- Keep going
645+ Nothing -> pure $ Loop $ Tuple (Just chunk) (takeUntilStrom predicate next) -- Keep going
639646
640647-- | Drop while predicate is true
641648dropWhileStrom :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -650,13 +657,13 @@ dropWhileStrom predicate stream = dropWhileHelper true stream
650657 Done (Just chunk) -> do
651658 let dropped = Array.dropWhile predicate chunk
652659 pure $ Done $ if Array.null dropped then Nothing else Just dropped
653− Loop ((maybeChunk /\ next)) ->
660+ Loop (Tuple maybeChunk next) ->
654661 case maybeChunk of
655− Nothing -> pure $ Loop $ (Nothing /\ (dropWhileHelper stillDropping next))
662+ Nothing -> pure $ Loop $ Tuple Nothing (dropWhileHelper stillDropping next)
656663 Just chunk -> do
657664 let dropped = Array.dropWhile predicate chunk
658− if Array.null dropped then pure $ Loop $ (Nothing /\ (dropWhileHelper true next))
659− else pure $ Loop $ ((Just dropped) /\ next) -- Found elements to keep, stop dropping
665+ if Array.null dropped then pure $ Loop $ Tuple Nothing (dropWhileHelper true next)
666+ else pure $ Loop $ Tuple (Just dropped) next -- Found elements to keep, stop dropping
660667
661668-- | Filter with monadic predicate
662669filterMStrom :: forall ctx err a. (a -> Om ctx err Boolean) -> Strom ctx err a -> Strom ctx err a
@@ -667,13 +674,13 @@ filterMStrom predicate stream = mkStrom do
667674 Done (Just chunk) -> do
668675 filtered <- Array.filterA predicate chunk
669676 pure $ Done $ if Array.null filtered then Nothing else Just filtered
670− Loop ((maybeChunk /\ next)) -> do
677+ Loop (Tuple maybeChunk next) -> do
671678 filteredChunk <- case maybeChunk of
672679 Nothing -> pure Nothing
673680 Just chunk -> do
674681 filtered <- Array.filterA predicate chunk
675682 pure $ if Array.null filtered then Nothing else Just filtered
676− pure $ Loop $ (filteredChunk /\ (filterMStrom predicate next))
683+ pure $ Loop $ Tuple filteredChunk (filterMStrom predicate next)
677684
678685-- | Collect with partial function (mapMaybe)
679686collectStrom :: forall ctx err a b. (a -> Maybe b) -> Strom ctx err a -> Strom ctx err b
@@ -684,16 +691,17 @@ collectStrom f stream = mkStrom do
684691 Done (Just chunk) -> do
685692 let collected = Array.mapMaybe f chunk
686693 pure $ Done $ if Array.null collected then Nothing else Just collected
687− Loop ((maybeChunk /\ next)) ->
688− do
689− let
690− collected = case maybeChunk of
691− Nothing -> Nothing
692− Just chunk -> do
693− let c = Array.mapMaybe f chunk
694+ Loop (Tuple maybeChunk next) ->
695+ let
696+ collected = case maybeChunk of
697+ Nothing -> Nothing
698+ Just chunk ->
699+ let
700+ c = Array.mapMaybe f chunk
701+ in
694702 if Array.null c then Nothing else Just c
695−
696− pure $ Loop $ (collected /\ (collectStrom f next))
703+ in
704+ pure $ Loop $ Tuple collected (collectStrom f next)
697705
698706-- | Collect with monadic partial function
699707collectMStrom :: forall ctx err a b. (a -> Om ctx err (Maybe b)) -> Strom ctx err a -> Strom ctx err b
@@ -705,16 +713,16 @@ collectMStrom f stream = mkStrom do
705713 maybes <- traverse f chunk
706714 let collected = Array.mapMaybe identity maybes
707715 pure $ Done $ if Array.null collected then Nothing else Just collected
708− Loop ((maybeChunk /\ next)) -> do
716+ Loop (Tuple maybeChunk next) -> do
709717 collected <- case maybeChunk of
710718 Nothing -> pure Nothing
711719 Just chunk -> do
712720 maybes <- traverse f chunk
713721 let c = Array.mapMaybe identity maybes
714722 pure $ if Array.null c then Nothing else Just c
715− pure $ Loop $ (collected /\ (collectMStrom f next))
723+ pure $ Loop $ Tuple collected (collectMStrom f next)
716724
717−-- | Remove consecutive duplicates
725+-- | Remove consecutive duplicates
718726changesStrom :: forall ctx err a. Eq a => Strom ctx err a -> Strom ctx err a
719727changesStrom stream = changesHelper Nothing stream
720728 where
@@ -724,28 +732,28 @@ changesStrom stream = changesHelper Nothing stream
724732 Done Nothing -> pure $ Done Nothing
725733 Done (Just chunk) -> do
726734 let
727− (_ /\ result) = Array.foldl
728− ( \((prev /\ acc)) curr ->
729− if Just curr == prev then ((Just curr) /\ acc)
730− else ((Just curr) /\ (Array.snoc acc curr))
735+ Tuple _ result = Array.foldl
736+ ( \(Tuple prev acc) curr ->
737+ if Just curr == prev then Tuple (Just curr) acc
738+ else Tuple (Just curr) (Array.snoc acc curr)
731739 )
732− (lastSeen /\ [])
740+ (Tuple lastSeen [])
733741 chunk
734742 pure $ Done $ if Array.null result then Nothing else Just result
735− Loop ((maybeChunk /\ next)) ->
743+ Loop (Tuple maybeChunk next) ->
736744 case maybeChunk of
737− Nothing -> pure $ Loop $ (Nothing /\ (changesHelper lastSeen next))
745+ Nothing -> pure $ Loop $ Tuple Nothing (changesHelper lastSeen next)
738746 Just chunk -> do
739747 let
740− (newLast /\ result) = Array.foldl
741− ( \((prev /\ acc)) curr ->
742− if Just curr == prev then ((Just curr) /\ acc)
743− else ((Just curr) /\ (Array.snoc acc curr))
748+ Tuple newLast result = Array.foldl
749+ ( \(Tuple prev acc) curr ->
750+ if Just curr == prev then Tuple (Just curr) acc
751+ else Tuple (Just curr) (Array.snoc acc curr)
744752 )
745− (lastSeen /\ [])
753+ (Tuple lastSeen [])
746754 chunk
747− if Array.null result then pure $ Loop $ (Nothing /\ (changesHelper newLast next))
748− else pure $ Loop $ ((Just result) /\ (changesHelper newLast next))
755+ if Array.null result then pure $ Loop $ Tuple Nothing (changesHelper newLast next)
756+ else pure $ Loop $ Tuple (Just result) (changesHelper newLast next)
749757
750758--------------------------------------------------------------------------------
751759-- Grouping
@@ -757,16 +765,16 @@ groupedStrom size stream
757765 | size <= 0 = empty
758766 | otherwise = groupedHelper [] stream
759767 where
760− groupedHelper buf s = mkStrom do
768+ groupedHelper buffer s = mkStrom do
761769 step <- runStrom s
762770 case step of
763771 Done Nothing ->
764772 -- End of stream, emit remaining buffer if non-empty
765− pure $ Done $ if Array.null buf then Nothing else Just [ buf ]
773+ pure $ Done $ if Array.null buffer then Nothing else Just [ buffer ]
766774 Done (Just chunk) -> do
767775 -- Process final chunk with buffer
768776 let
769− combined = buf <> chunk
777+ combined = buffer <> chunk
770778 groups = groupsOf size combined
771779 completeGroups = Array.dropEnd 1 groups
772780 lastGroup = case Array.last groups of
@@ -777,28 +785,28 @@ groupedStrom size stream
777785 Nothing -> []
778786 if Array.null remainder then pure $ Done $ if Array.null (completeGroups <> lastGroup) then Nothing else Just (completeGroups <> lastGroup)
779787 else pure $ Done $ if Array.null (completeGroups <> [ remainder ]) then Nothing else Just (completeGroups <> [ remainder ])
780− Loop ((maybeChunk /\ next)) ->
788+ Loop (Tuple maybeChunk next) ->
781789 case maybeChunk of
782− Nothing -> pure $ Loop $ (Nothing /\ (groupedHelper buf next))
790+ Nothing -> pure $ Loop $ Tuple Nothing (groupedHelper buffer next)
783791 Just chunk -> do
784792 let
785− combined = buf <> chunk
793+ combined = buffer <> chunk
786794 groups = groupsOf size combined
787795 completeGroups = if Array.null groups then [] else Array.dropEnd 1 groups
788796 newBuffer = case Array.last groups of
789797 Just g -> if Array.length g < size then g else []
790798 Nothing -> []
791− if Array.null completeGroups then pure $ Loop $ (Nothing /\ (groupedHelper newBuffer next))
792− else pure $ Loop $ ((Just completeGroups) /\ (groupedHelper newBuffer next))
799+ if Array.null completeGroups then pure $ Loop $ Tuple Nothing (groupedHelper newBuffer next)
800+ else pure $ Loop $ Tuple (Just completeGroups) (groupedHelper newBuffer next)
793801
794802 groupsOf :: forall b. Int -> Array b -> Array (Array b)
795803 groupsOf n arr
796804 | n <= 0 || Array.null arr = []
797805 | otherwise =
798− do
799− let
800− group = Array.take n arr
801− rest = Array.drop n arr
806+ let
807+ group = Array.take n arr
808+ rest = Array.drop n arr
809+ in
802810 if Array.null group then [] else Array.cons group (groupsOf n rest)
803811
804812-- | Alias for groupedStrom
@@ -810,70 +818,74 @@ chunkedStrom = groupedStrom
810818groupByStrom :: forall ctx err a k. Eq k => (a -> k) -> Strom ctx err a -> Strom ctx err (Array a)
811819groupByStrom keyFn stream = groupByHelper Nothing [] stream
812820 where
813− groupByHelper lastKey buf s = mkStrom do
821+ groupByHelper lastKey buffer s = mkStrom do
814822 step <- runStrom s
815823 case step of
816824 Done Nothing ->
817− pure $ Done $ if Array.null buf then Nothing else Just [ buf ]
825+ pure $ Done $ if Array.null buffer then Nothing else Just [ buffer ]
818826 Done (Just chunk) -> do
819827 let
820− (_ /\ result) = Array.foldl
821− ( \((currentKey /\ acc)) item ->
822− do
823− let itemKey = keyFn item
828+ Tuple _ result = Array.foldl
829+ ( \(Tuple currentKey acc) item ->
830+ let
831+ itemKey = keyFn item
832+ in
824833 case currentKey of
825− Nothing -> ((Just itemKey) /\ { groups: [], buffer: [ item ] })
834+ Nothing -> Tuple (Just itemKey) { groups: [], buffer: [ item ] }
826835 Just k ->
827− if k == itemKey then ((Just itemKey) /\ { groups: acc.groups, buffer: Array.snoc acc.buffer item })
828− else ((Just itemKey) /\ { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] })
836+ if k == itemKey then Tuple (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 ] }
829838 )
830− (lastKey /\ { groups: [], buffer: buf })
839+ (Tuple lastKey { groups: [], buffer })
831840 chunk
832841 allGroups =
833842 if Array.null result.buffer then result.groups
834843 else Array.snoc result.groups result.buffer
835844 pure $ Done $ if Array.null allGroups then Nothing else Just allGroups
836− Loop ((maybeChunk /\ next)) ->
845+ Loop (Tuple maybeChunk next) ->
837846 case maybeChunk of
838− Nothing -> pure $ Loop $ (Nothing /\ (groupByHelper lastKey buf next))
847+ Nothing -> pure $ Loop $ Tuple Nothing (groupByHelper lastKey buffer next)
839848 Just chunk -> do
840849 let
841− (newKey /\ result) = Array.foldl
842− ( \((currentKey /\ acc)) item ->
843− do
844− let itemKey = keyFn item
850+ Tuple newKey result = Array.foldl
851+ ( \(Tuple currentKey acc) item ->
852+ let
853+ itemKey = keyFn item
854+ in
845855 case currentKey of
846− Nothing -> ((Just itemKey) /\ { groups: [], buffer: [ item ] })
856+ Nothing -> Tuple (Just itemKey) { groups: [], buffer: [ item ] }
847857 Just k ->
848− if k == itemKey then ((Just itemKey) /\ { groups: acc.groups, buffer: Array.snoc acc.buffer item })
849− else ((Just itemKey) /\ { groups: Array.snoc acc.groups acc.buffer, buffer: [ item ] })
858+ if k == itemKey then Tuple (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 ] }
850860 )
851− (lastKey /\ { groups: [], buffer: buf })
861+ (Tuple lastKey { groups: [], buffer })
852862 chunk
853− if Array.null result.groups then pure $ Loop $ (Nothing /\ (groupByHelper newKey result.buffer next))
854− else pure $ Loop $ ((Just result.groups) /\ (groupByHelper newKey result.buffer next))
863+ if Array.null result.groups then pure $ Loop $ Tuple Nothing (groupByHelper newKey result.buffer next)
864+ else pure $ Loop $ Tuple (Just result.groups) (groupByHelper newKey result.buffer next)
855865
856866-- | Partition a stream by a predicate into two separate operations
857867-- | Returns a tuple of (trues, falses)
858868-- | Note: This requires running the stream twice, so it's not lazy
859−partition :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> ((Strom ctx err a) /\ (Strom ctx err a))
869+partition :: forall ctx err a. (a -> Boolean) -> Strom ctx err a -> Tuple (Strom ctx err a) (Strom ctx err a)
860870partition predicate stream =
861− (filterStrom predicate stream)
862− /\ (filterStrom (not <<< predicate) stream)
871+ Tuple
872+ (filterStrom predicate stream)
873+ (filterStrom (not <<< predicate) stream)
863874
864875-- | Partition and map in one pass
865−partitionMap :: forall ctx err a b c. (a -> Either b c) -> Strom ctx err a -> ((Strom ctx err b) /\ (Strom ctx err c))
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)
866877partitionMap f stream =
867− do
868− let
869− leftOnly x = case f x of
870− Left b -> Just b
871− Right _ -> Nothing
872− rightOnly x = case f x of
873− Left _ -> Nothing
874− Right c -> Just c
875− (collectStrom leftOnly stream)
876− /\ (collectStrom rightOnly stream)
878+ let
879+ leftOnly x = case f x of
880+ Left b -> Just b
881+ Right _ -> Nothing
882+ rightOnly x = case f x of
883+ Left _ -> Nothing
884+ Right c -> Just c
885+ in
886+ Tuple
887+ (collectStrom leftOnly stream)
888+ (collectStrom rightOnly stream)
877889
878890--------------------------------------------------------------------------------
879891-- Timing
@@ -889,24 +901,49 @@ debounce duration stream = mkStrom do
889901 Done (Just chunk) -> do
890902 Om.delay duration
891903 pure $ Done $ Just chunk
892− Loop (maybeChunk /\ next) -> do
904+ Loop (Tuple maybeChunk next) -> do
893905 case maybeChunk of
894− Nothing -> pure $ Loop $ (Nothing /\ (debounce duration next))
906+ Nothing -> pure $ Loop $ Tuple Nothing (debounce duration next)
895907 Just chunk -> do
896908 Om.delay duration
897− pure $ Loop $ ((Just chunk) /\ (debounce duration next))
909+ pure $ Loop $ Tuple (Just chunk) (debounce duration next)
898910
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−
905−-- | Throttle a stream - emits at most one element per duration period
911+-- | Throttle a stream - emit at most once per duration period
912+-- | Tracks last emission time and skips elements that arrive too quickly
913+-- | Note: Uses wall-clock time, not processing time
906914throttle :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
907−throttle duration stream = stream # bindStrom \a -> mkStrom do
908− Om.delay duration
909− pure $ Done $ Just [ a ]
915+throttle (Milliseconds duration) stream = throttleWithState 0.0 stream
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
940+ pure $ Tuple currentMillis (Array.snoc filtered value)
941+ else
942+ pure $ Tuple currentLast filtered
943+ )
944+ (pure $ Tuple lastEmit [])
945+ chunk
946+ pure $ Tuple newLastEmit results
910947
911948-- | Delay each element by a duration
912949delayStrom :: forall ctx err a. Milliseconds -> Strom ctx err a -> Strom ctx err a
@@ -920,33 +957,33 @@ delayStrom duration stream = mkStrom do
920957
921958-- | Fold over all elements
922959runFold :: forall ctx err a b. b -> (b -> a -> b) -> Strom ctx err a -> Om ctx err b
923−runFold initial f stream = tailRecM go ((initial /\ stream))
960+runFold initial f stream = tailRecM go (Tuple initial stream)
924961 where
925− go ((acc /\ s)) = do
962+ go (Tuple acc s) = do
926963 step <- runStrom s
927964 case step of
928965 Done Nothing -> pure $ Done acc
929966 Done (Just chunk) -> pure $ Done $ foldl f acc chunk
930− Loop ((maybeChunk /\ next)) ->
967+ Loop (Tuple maybeChunk next) ->
931968 -- Process chunk if present and continue
932969 case maybeChunk of
933− Nothing -> pure $ Loop $ (acc /\ next)
934− Just chunk -> pure $ Loop $ ((foldl f acc chunk) /\ next)
970+ Nothing -> pure $ Loop $ Tuple acc next
971+ Just chunk -> pure $ Loop $ Tuple (foldl f acc chunk) next
935972
936973-- | Collect all elements into an array (O(n) using chunk accumulation)
937974runCollect :: forall ctx err a. Strom ctx err a -> Om ctx err (Array a)
938−runCollect stream = tailRecM go (List.Nil /\ stream)
975+runCollect stream = tailRecM go (Tuple List.Nil stream)
939976 where
940− go ((chunkList /\ s)) = do
977+ go (Tuple chunkList s) = do
941978 step <- runStrom s
942979 case step of
943980 Done Nothing -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse chunkList
944981 Done (Just chunk) -> pure $ Done $ Array.concat $ Array.fromFoldable $ List.reverse $ List.Cons chunk chunkList
945− Loop ((maybeChunk /\ next)) ->
982+ Loop (Tuple maybeChunk next) ->
946983 -- Emit the chunk (if present) and continue
947984 case maybeChunk of
948− Nothing -> pure $ Loop $ (chunkList /\ next)
949− Just chunk -> pure $ Loop $ ((List.Cons chunk chunkList) /\ next)
985+ Nothing -> pure $ Loop $ Tuple chunkList next
986+ Just chunk -> pure $ Loop $ Tuple (List.Cons chunk chunkList) next
950987
951988-- | Run the stream and discard all elements
952989runDrain :: forall ctx err a. Strom ctx err a -> Om ctx err Unit
@@ -963,7 +1000,7 @@ traverseStrom_ f stream = tailRecM go stream
9631000 Done (Just chunk) -> do
9641001 _ <- traverse f chunk
9651002 pure $ Done unit
966− Loop ((maybeChunk /\ next)) -> do
1003+ Loop (Tuple maybeChunk next) -> do
9671004 -- Process chunk if present, then continue
9681005 case maybeChunk of
9691006 Nothing -> pure unit
@@ -987,20 +1024,20 @@ traverseMStrom_ = traverseStrom_
9871024--------------------------------------------------------------------------------
9881025
9891026-- | Run a stream in the background, returning a Fiber that can be cancelled
990−-- |
1027+-- |
9911028-- | To use cancellable streams, convert your stream to Om and use Om.launchOm:
992−-- |
1029+-- |
9931030-- | Example:
9941031-- | ```purescript
9951032-- | fiber <- Om.launchOm ctx handlers (Strom.runCollect myStream)
9961033-- | -- ... later ...
9971034-- | killFiber (error "Cancelled") fiber
9981035-- | ```
999−-- |
1036+-- |
10001037-- | Or for subscription-style:
10011038-- | ```purescript
10021039-- | fiber <- Om.launchOm ctx handlers (Strom.traverseStrom_ callback myStream)
1003−-- | -- ... later ...
1040+-- | -- ... later ...
10041041-- | killFiber (error "Unsubscribed") fiber
10051042-- | ```
10061043-- |
@@ -1023,8 +1060,8 @@ appendStrom toAppend baseStream = mkStrom do
10231060 step <- runStrom baseStream
10241061 case step of
10251062 Done Nothing -> runStrom toAppend
1026− Done (Just chunk) -> pure $ Loop $ ((Just chunk) /\ toAppend) -- Emit chunk and continue with toAppend
1027− Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (appendStrom toAppend next))
1063+ Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) toAppend -- Emit chunk and continue with toAppend
1064+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (appendStrom toAppend next)
10281065
10291066-- | Concatenate an array of streams
10301067-- | Uses foldr with flip to match appendStrom's pipeline-friendly parameter order
@@ -1032,8 +1069,8 @@ concatStrom :: forall ctx err a. Array (Strom ctx err a) -> Strom ctx err a
10321069concatStrom streams = Array.foldr (flip appendStrom) empty streams
10331070
10341071-- | Zip two streams together
1035−zipStrom :: forall ctx err a b. Strom ctx err a -> Strom ctx err b -> Strom ctx err ((a /\ b))
1036−zipStrom = zipWithStrom (/\)
1072+zipStrom :: forall ctx err a b. Strom ctx err a -> Strom ctx err b -> Strom ctx err (Tuple a b)
1073+zipStrom = zipWithStrom Tuple
10371074
10381075-- | Zip two streams with a function
10391076zipWithStrom :: forall ctx err a b c. (a -> b -> c) -> Strom ctx err a -> Strom ctx err b -> Strom ctx err c
@@ -1046,13 +1083,13 @@ zipWithStrom f s1 s2 = mkStrom do
10461083 pure $ Done $ Just zipped
10471084 Done Nothing, _ -> pure $ Done Nothing
10481085 _, Done Nothing -> pure $ Done Nothing
1049− Loop ((maybeChunk1 /\ next1)), Loop ((maybeChunk2 /\ next2)) ->
1086+ Loop (Tuple maybeChunk1 next1), Loop (Tuple maybeChunk2 next2) ->
10501087 -- Both looping: zip chunks if both present, continue with both nexts
10511088 case maybeChunk1, maybeChunk2 of
1052− Just chunk1, Just chunk2 -> pure $ Loop $ ((Just $ Array.zipWith f chunk1 chunk2) /\ (zipWithStrom f next1 next2))
1053− _, _ -> pure $ Loop $ (Nothing /\ (zipWithStrom f next1 next2))
1054− Loop ((_ /\ next1)), _ -> pure $ Loop $ (Nothing /\ (zipWithStrom f next1 s2))
1055− _, Loop ((_ /\ next2)) -> pure $ Loop $ (Nothing /\ (zipWithStrom f s1 next2))
1089+ Just chunk1, Just chunk2 -> pure $ Loop $ Tuple (Just $ Array.zipWith f chunk1 chunk2) (zipWithStrom f next1 next2)
1090+ _, _ -> pure $ Loop $ Tuple Nothing (zipWithStrom f next1 next2)
1091+ Loop (Tuple _ next1), _ -> pure $ Loop $ Tuple Nothing (zipWithStrom f next1 s2)
1092+ _, Loop (Tuple _ next2) -> pure $ Loop $ Tuple Nothing (zipWithStrom f s1 next2)
10561093
10571094-- | Interleave two streams - alternates between elements from each stream
10581095-- | Similar to merge but deterministic - always alternates
@@ -1064,16 +1101,16 @@ interleave s1 s2 = interleaveHelper true s1 s2
10641101 step1 <- runStrom first
10651102 case step1 of
10661103 Done Nothing -> runStrom second
1067− Done (Just chunk1) -> pure $ Loop $ ((Just chunk1) /\ (interleaveHelper false second first))
1068− Loop ((maybeChunk1 /\ next1)) ->
1069− pure $ Loop $ (maybeChunk1 /\ (interleaveHelper false second next1))
1104+ Done (Just chunk1) -> pure $ Loop $ Tuple (Just chunk1) (interleaveHelper false second first)
1105+ Loop (Tuple maybeChunk1 next1) ->
1106+ pure $ Loop $ Tuple maybeChunk1 (interleaveHelper false second next1)
10701107 else do
10711108 step2 <- runStrom second
10721109 case step2 of
10731110 Done Nothing -> runStrom first
1074− Done (Just chunk2) -> pure $ Loop $ ((Just chunk2) /\ (interleaveHelper true first second))
1075− Loop ((maybeChunk2 /\ next2)) ->
1076− pure $ Loop $ (maybeChunk2 /\ (interleaveHelper true first next2))
1111+ Done (Just chunk2) -> pure $ Loop $ Tuple (Just chunk2) (interleaveHelper true first second)
1112+ Loop (Tuple maybeChunk2 next2) ->
1113+ pure $ Loop $ Tuple maybeChunk2 (interleaveHelper true first next2)
10771114
10781115-- | Intersperse a separator element between each element of the stream
10791116intersperse :: forall ctx err a. a -> Strom ctx err a -> Strom ctx err a
@@ -1089,15 +1126,15 @@ intersperse separator stream = intersperseHelper true stream
10891126 if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
10901127 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
10911128 pure $ Done $ Just withSeparators
1092− Loop ((maybeChunk /\ next)) ->
1129+ Loop (Tuple maybeChunk next) ->
10931130 case maybeChunk of
1094− Nothing -> pure $ Loop $ (Nothing /\ (intersperseHelper isFirst next))
1131+ Nothing -> pure $ Loop $ Tuple Nothing (intersperseHelper isFirst next)
10951132 Just chunk -> do
10961133 let
10971134 withSeparators =
10981135 if isFirst then Array.intercalate [ separator ] (Array.singleton <$> chunk)
10991136 else Array.cons separator (Array.intercalate [ separator ] (Array.singleton <$> chunk))
1100− pure $ Loop $ ((Just withSeparators) /\ (intersperseHelper false next))
1137+ pure $ Loop $ Tuple (Just withSeparators) (intersperseHelper false next)
11011138
11021139--------------------------------------------------------------------------------
11031140-- Buffering
@@ -1138,7 +1175,7 @@ bufferChunks capacity stream
11381175 Nothing -> do
11391176 AVar.put unit mutex
11401177 dequeue
1141− Just ((head /\ tail)) -> do
1178+ Just (Tuple head tail) -> do
11421179 liftEffect $ Ref.write tail bufferRef
11431180 liftEffect $ Ref.write (size - 1) sizeRef
11441181 let hasMore = (size - 1) > 0
@@ -1156,7 +1193,7 @@ bufferChunks capacity stream
11561193 Done (Just chunk) -> do
11571194 enqueue (BufferChunk chunk)
11581195 enqueue BufferDone
1159− Loop ((maybeChunk /\ next)) -> do
1196+ Loop (Tuple maybeChunk next) -> do
11601197 case maybeChunk of
11611198 Just chunk -> enqueue (BufferChunk chunk)
11621199 Nothing -> pure unit
@@ -1172,7 +1209,7 @@ bufferChunks capacity stream
11721209 killFiber (Exception.error "done") fiber
11731210 pure $ Done Nothing
11741211 BufferChunk chunk ->
1175− pure $ Loop $ ((Just chunk) /\ consumer)
1212+ pure $ Loop $ Tuple (Just chunk) consumer
11761213
11771214 stepResult <- Om.runReader ctx (runStrom consumer)
11781215 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1212,7 +1249,7 @@ buffer capacity stream
12121249 Nothing -> do
12131250 AVar.put unit mutex
12141251 dequeue
1215− Just ((head /\ tail)) -> do
1252+ Just (Tuple head tail) -> do
12161253 liftEffect $ Ref.write tail bufferRef
12171254 liftEffect $ Ref.write (size - 1) sizeRef
12181255 let hasMore = (size - 1) > 0
@@ -1230,7 +1267,7 @@ buffer capacity stream
12301267 Done (Just chunk) -> do
12311268 traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
12321269 enqueue BufferDone
1233− Loop ((maybeChunk /\ next)) -> do
1270+ Loop (Tuple maybeChunk next) -> do
12341271 case maybeChunk of
12351272 Just chunk -> traverse_ (\a -> enqueue (BufferChunk [ a ])) chunk
12361273 Nothing -> pure unit
@@ -1246,7 +1283,7 @@ buffer capacity stream
12461283 killFiber (Exception.error "done") fiber
12471284 pure $ Done Nothing
12481285 BufferChunk chunk ->
1249− pure $ Loop $ ((Just chunk) /\ consumer)
1286+ pure $ Loop $ Tuple (Just chunk) consumer
12501287
12511288 stepResult <- Om.runReader ctx (runStrom consumer)
12521289 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1355,7 +1392,7 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
13551392 Nothing -> do
13561393 AVar.put unit mutex
13571394 dequeue
1358− Just ((head /\ tail)) -> do
1395+ Just (Tuple head tail) -> do
13591396 liftEffect $ Ref.write tail bufferRef
13601397 liftEffect $ Ref.write (size - 1) sizeRef
13611398 let hasMore = (size - 1) > 0
@@ -1370,14 +1407,14 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
13701407 let
13711408 producer s = do
13721409 stepResult <- Om.runReader ctx (runStrom s)
1373− step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1410+ step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
13741411 case step of
13751412 Done Nothing ->
13761413 enqueue (StreamDone StreamId1)
13771414 Done (Just chunk) -> do
13781415 enqueue (DataChunk chunk StreamId1)
13791416 enqueue (StreamDone StreamId1)
1380− Loop ((maybeChunk /\ next)) -> do
1417+ Loop (Tuple maybeChunk next) -> do
13811418 case maybeChunk of
13821419 Just chunk -> enqueue (DataChunk chunk StreamId1)
13831420 Nothing -> pure unit
@@ -1389,14 +1426,14 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
13891426 let
13901427 producer s = do
13911428 stepResult <- Om.runReader ctx (runStrom s)
1392− step <- either (\_ -> throwError (Exception.error "Stream error")) pure stepResult
1429+ step <- either (\e -> throwError (Exception.error "Stream error")) pure stepResult
13931430 case step of
13941431 Done Nothing ->
13951432 enqueue (StreamDone StreamId2)
13961433 Done (Just chunk) -> do
13971434 enqueue (DataChunk chunk StreamId2)
13981435 enqueue (StreamDone StreamId2)
1399− Loop ((maybeChunk /\ next)) -> do
1436+ Loop (Tuple maybeChunk next) -> do
14001437 case maybeChunk of
14011438 Just chunk -> enqueue (DataChunk chunk StreamId2)
14021439 Nothing -> pure unit
@@ -1423,7 +1460,7 @@ mergeNDWithStrategy bufferCapacity strategy stream1 stream2 = mkStrom do
14231460 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
14241461 DataChunk chunk _streamId ->
14251462 -- Got a chunk
1426− pure $ Loop $ ((Just chunk) /\ (consumer doneLeft doneRight))
1463+ pure $ Loop $ Tuple (Just chunk) (consumer doneLeft doneRight)
14271464
14281465 stepResult <- Om.runReader ctx (runStrom $ consumer false false)
14291466 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1468,7 +1505,7 @@ mergeAllNDWith bufferCapacity streams
14681505 Nothing -> do
14691506 AVar.put unit mutex
14701507 dequeue
1471− Just ((head /\ tail)) -> do
1508+ Just (Tuple head tail) -> do
14721509 liftEffect $ Ref.write tail bufferRef
14731510 liftEffect $ Ref.write (size - 1) sizeRef
14741511 let hasMore = (size - 1) > 0
@@ -1486,7 +1523,7 @@ mergeAllNDWith bufferCapacity streams
14861523 Done (Just chunk) -> do
14871524 enqueue (MergeChunk chunk)
14881525 enqueue MergeDone
1489− Loop ((maybeChunk /\ next)) -> do
1526+ Loop (Tuple maybeChunk next) -> do
14901527 case maybeChunk of
14911528 Just chunk -> enqueue (MergeChunk chunk)
14921529 Nothing -> pure unit
@@ -1507,7 +1544,7 @@ mergeAllNDWith bufferCapacity streams
15071544 stepResult <- Om.runReader ctx (runStrom $ consumer (doneCount + 1))
15081545 either (\_ -> throwError (Exception.error "Consumer error")) pure stepResult
15091546 MergeChunk chunk ->
1510− pure $ Loop $ ((Just chunk) /\ (consumer doneCount))
1547+ pure $ Loop $ Tuple (Just chunk) (consumer doneCount)
15111548
15121549 stepResult <- Om.runReader ctx (runStrom $ consumer 0)
15131550 either (\_ -> throwError (Exception.error "Consumer init error")) pure stepResult
@@ -1531,23 +1568,23 @@ mapPar concurrency f stream
15311568 let groups = chunksOf concurrency chunk
15321569 results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
15331570 pure $ Done $ Just $ Array.concat results
1534− Loop ((maybeChunk /\ next)) -> do
1571+ Loop (Tuple maybeChunk next) -> do
15351572 mappedChunk <- case maybeChunk of
15361573 Nothing -> pure Nothing
15371574 Just chunk -> do
15381575 let groups = chunksOf concurrency chunk
15391576 results <- traverse (\group -> Om.inParallel (Functor.map f group)) groups
15401577 pure $ Just $ Array.concat results
1541− pure $ Loop $ (mappedChunk /\ (mapPar concurrency f next))
1578+ pure $ Loop $ Tuple mappedChunk (mapPar concurrency f next)
15421579 where
15431580 chunksOf :: forall x. Int -> Array x -> Array (Array x)
15441581 chunksOf n arr
15451582 | n <= 0 || Array.null arr = []
15461583 | otherwise =
1547− do
1548− let
1549− group = Array.take n arr
1550− rest = Array.drop n arr
1584+ let
1585+ group = Array.take n arr
1586+ rest = Array.drop n arr
1587+ in
15511588 if Array.null group then [] else Array.cons group (chunksOf n rest)
15521589
15531590-- | Unordered parallel map (currently same as mapPar)
@@ -1683,7 +1720,7 @@ instance Functor (Strom ctx err) where
16831720 case step of
16841721 Done Nothing -> pure $ Done Nothing
16851722 Done (Just chunk) -> pure $ Done $ Just $ Functor.map f chunk
1686− Loop ((maybeChunk /\ next)) -> pure $ Loop $ ((Functor.map (Functor.map f) maybeChunk) /\ (Functor.map f next))
1723+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple (Functor.map (Functor.map f) maybeChunk) (Functor.map f next)
16871724 )
16881725
16891726instance Apply (Strom ctx err) where
@@ -1692,29 +1729,29 @@ instance Apply (Strom ctx err) where
16921729 runStrom as >>= \stepA ->
16931730 case stepF, stepA of
16941731 Done (Just funcs), Done (Just vals) ->
1695− do
1696− let
1697− results = do
1698− f <- funcs
1699− a <- vals
1700− pure (f a)
1732+ let
1733+ results = do
1734+ f <- funcs
1735+ a <- vals
1736+ pure (f a)
1737+ in
17011738 pure $ Done $ Just results
17021739 Done Nothing, _ -> pure $ Done Nothing
17031740 _, Done Nothing -> pure $ Done Nothing
1704− Loop ((maybeChunkF /\ nextF)), Loop ((maybeChunkA /\ nextA)) ->
1741+ Loop (Tuple maybeChunkF nextF), Loop (Tuple maybeChunkA nextA) ->
17051742 -- Apply chunks if both present
17061743 case maybeChunkF, maybeChunkA of
17071744 Just chunkF, Just chunkA ->
1708− do
1709− let
1710− applied = do
1711− f <- chunkF
1712− a <- chunkA
1713− pure (f a)
1714− pure $ Loop $ ((Just applied) /\ (nextF <*> nextA))
1715− _, _ -> pure $ Loop $ (Nothing /\ (nextF <*> nextA))
1716− Loop ((_ /\ nextF)), _ -> pure $ Loop $ (Nothing /\ (nextF <*> as))
1717− _, Loop ((_ /\ nextA)) -> pure $ Loop $ (Nothing /\ (fs <*> nextA))
1745+ let
1746+ applied = do
1747+ f <- chunkF
1748+ a <- chunkA
1749+ pure (f a)
1750+ in
1751+ pure $ Loop $ Tuple (Just applied) (nextF <*> nextA)
1752+ _, _ -> pure $ Loop $ Tuple Nothing (nextF <*> nextA)
1753+ Loop (Tuple _ nextF), _ -> pure $ Loop $ Tuple Nothing (nextF <*> as)
1754+ _, Loop (Tuple _ nextA) -> pure $ Loop $ Tuple Nothing (fs <*> nextA)
17181755 )
17191756
17201757instance Applicative (Strom ctx err) where
@@ -1726,18 +1763,19 @@ instance Bind (Strom ctx err) where
17261763 case step of
17271764 Done Nothing -> pure $ Done Nothing
17281765 Done (Just chunk) ->
1729− do
1730− let streams = Functor.map f chunk
1766+ let
1767+ streams = Functor.map f chunk
1768+ in
17311769 runStrom $ concatStrom streams
1732− Loop ((maybeChunk /\ next)) ->
1770+ Loop (Tuple maybeChunk next) ->
17331771 -- When looping, if we have a chunk, we need to process it through f and concat
17341772 case maybeChunk of
1735− Nothing -> pure $ Loop $ (Nothing /\ (next >>= f))
1773+ Nothing -> pure $ Loop $ Tuple Nothing (next >>= f)
17361774 Just chunk ->
1737− do
1738− let
1739− streams = Functor.map f chunk
1740− prefixStream = concatStrom streams
1775+ let
1776+ streams = Functor.map f chunk
1777+ prefixStream = concatStrom streams
1778+ in
17411779 runStrom $ appendStrom prefixStream (next >>= f)
17421780 )
17431781
@@ -1748,8 +1786,8 @@ instance Semigroup (Strom ctx err a) where
17481786 ( runStrom s1 >>= \step ->
17491787 case step of
17501788 Done Nothing -> runStrom s2
1751− Done (Just chunk) -> pure $ Loop $ ((Just chunk) /\ s2)
1752− Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (next <> s2))
1789+ Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) s2
1790+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (next <> s2)
17531791 )
17541792
17551793instance Monoid (Strom ctx err a) where
@@ -1760,8 +1798,8 @@ instance Alt (Strom ctx err) where
17601798 ( runStrom s1 >>= \step ->
17611799 case step of
17621800 Done Nothing -> runStrom s2
1763− Done (Just chunk) -> pure $ Loop $ ((Just chunk) /\ s2)
1764− Loop ((maybeChunk /\ next)) -> pure $ Loop $ (maybeChunk /\ (next <|> s2))
1801+ Done (Just chunk) -> pure $ Loop $ Tuple (Just chunk) s2
1802+ Loop (Tuple maybeChunk next) -> pure $ Loop $ Tuple maybeChunk (next <|> s2)
17651803 )
17661804
17671805instance Plus (Strom ctx err) where
src/Yoga/Om/Strom.purs.backupdeleted−1809
@@ -1,1809 +0,0 @@
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(..))
1010import Effect.Aff (Aff)
1111import Effect.Ref as Ref
1212import Effect.Class (liftEffect)
13−import Effect.Exception (throwException)
13+import Effect.Exception (throwException, error)
1414import Test.Spec (Spec, describe, it)
15−import Test.Spec.Assertions (shouldEqual)
15+import Test.Spec.Assertions (shouldEqual, fail)
1616import Yoga.Om (Om)
1717import Yoga.Om as Om
1818import Yoga.Om.Strom as Strom
@@ -172,7 +172,7 @@ spec = do
172172 result `shouldEqual` [ "1a", "2b", "3c" ]
173173
174174 it "tap observes without modifying" do
175− _sideEffectRef <- liftEffect $ Ref.new 0
175+ sideEffectRef <- liftEffect $ Ref.new 0
176176 result <- runOm $
177177 Strom.fromArray [ 1, 2, 3 ]
178178 # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
src/Yoga/Om/Strom/Examples.purs+3−3
@@ -106,7 +106,7 @@ example4b = do
106106example5 :: Om {} () (Array (Tuple Int String))
107107example5 = do
108108 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"]
110110 -- TODO: zipStrom not yet implemented
111111 numbers # Strom.mapStrom (\n -> Tuple n "stub") # Strom.runCollect
112112
@@ -288,11 +288,11 @@ example11 = do
288288 Om.delay (Milliseconds 100.0)
289289 pure 1
290290
291− _source2 = Strom.fromOm do
291+ source2 = Strom.fromOm do
292292 Om.delay (Milliseconds 50.0) -- This will win
293293 pure 2
294294
295− _source3 = Strom.fromOm do
295+ source3 = Strom.fromOm do
296296 Om.delay (Milliseconds 200.0)
297297 pure 3
298298
src/Yoga/Om/Strom/FileStream.jsdeleted−17
@@ -1,17 +0,0 @@
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.jsdeleted−176
@@ -1,176 +0,0 @@
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.pursdeleted−360
@@ -1,360 +0,0 @@
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.pursdeleted−12
@@ -1,12 +0,0 @@
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+4−3
@@ -6,12 +6,13 @@ import Control.Alt ((<|>))
66import Data.Array as Array
77import Data.Maybe (Maybe(..))
88import Data.Tuple (Tuple(..))
9+import Data.Time.Duration (Milliseconds(..))
910import Effect.Aff (Aff)
1011import Effect.Ref as Ref
1112import Effect.Class (liftEffect)
12−import Effect.Exception (throwException)
13+import Effect.Exception (throwException, error)
1314import Test.Spec (Spec, describe, it)
14−import Test.Spec.Assertions (shouldEqual)
15+import Test.Spec.Assertions (shouldEqual, fail)
1516import Yoga.Om (Om)
1617import Yoga.Om as Om
1718import Yoga.Om.Strom as Strom
@@ -155,7 +156,7 @@ spec = do
155156 result `shouldEqual` [ "1a", "2b", "3c" ]
156157
157158 it "tap observes without modifying" do
158− _sideEffectRef <- liftEffect $ Ref.new 0
159+ sideEffectRef <- liftEffect $ Ref.new 0
159160 result <- runOm $
160161 Strom.fromArray [ 1, 2, 3 ]
161162 # Strom.tapStrom (\n -> let _ = n in unit) -- Pure observation
test/Test/WebStream.jsdeleted−106
@@ -1,106 +0,0 @@
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.pursdeleted−223
@@ -1,223 +0,0 @@
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