git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
26 files changed+2844−1022
since splitsnapshots
unifiedsplit
.psc-ide-port+1 .spec-results+296 README.md+1 compile-fail-tests/CombineDuplicateProv.purs+13 compile-fail-tests/CombineOverlappingProv.purs+15 compile-fail-tests/ProvideWrongType.purs+16 compile-fail-tests/RunLayerMissingDep.purs+15 compile-fail-tests/RunLayerOpenRowMissingDep.purs+19 compile-fail-tests/RunLayerPartialDep.purs+13 compile-fail-tests/WireLayersMissingDep.purs+16 compile-fail-tests/run.sh+38 spago.lock+458−721 spago.yaml+26−15 src/Yoga/Om/Layer.js+4 src/Yoga/Om/Layer.purs+921−130 test/Playground.purs+6−43 test/Test/Main.purs+5−113 test/Test/Yoga/Om/Layer/CompositionSpec.purs+59 test/Test/Yoga/Om/Layer/ErrorChannelSpec.purs+167 test/Test/Yoga/Om/Layer/InstancesSpec.purs+163 test/Test/Yoga/Om/Layer/MemoizationSpec.purs+114 test/Test/Yoga/Om/Layer/PureInferenceSpec.purs+22 test/Test/Yoga/Om/Layer/RecoveringSpec.purs+128 test/Test/Yoga/Om/Layer/RepeatingSpec.purs+49 test/Test/Yoga/Om/Layer/ScopedSpec.purs+146 test/Test/Yoga/Om/Layer/WireLayersSpec.purs+133
.psc-ide-portnew file+1
@@ -0,0 +1 @@
1+15500
.spec-resultsnew file+296
@@ -0,0 +1,296 @@
1+[
2+ [
3+ "Alt falls back to second layer on failure",
4+ {
5+ "timestamp": "1775221839085.0",
6+ "success": true
7+ }
8+ ],
9+ [
10+ "Automatic Memoization builds the same layer only once when used in two branches",
11+ {
12+ "timestamp": "1775221839085.0",
13+ "success": true
14+ }
15+ ],
16+ [
17+ "Automatic Memoization memoized bracketLayer acquires and releases only once",
18+ {
19+ "timestamp": "1775221839085.0",
20+ "success": true
21+ }
22+ ],
23+ [
24+ "Automatic Memoization memoized scoped layer runs finalizer only once",
25+ {
26+ "timestamp": "1775221839085.0",
27+ "success": true
28+ }
29+ ],
30+ [
31+ "Composition proves Row.Nub deduplicates shared requirements at type level",
32+ {
33+ "timestamp": "1775221839085.0",
34+ "success": true
35+ }
36+ ],
37+ [
38+ "Composition proves deduplication at runtime via shared Ref",
39+ {
40+ "timestamp": "1775221839085.0",
41+ "success": true
42+ }
43+ ],
44+ [
45+ "Error Channel composes layers with different error types",
46+ {
47+ "timestamp": "1775221839085.0",
48+ "success": true
49+ }
50+ ],
51+ [
52+ "Error Channel propagates errors from the first layer",
53+ {
54+ "timestamp": "1775221839085.0",
55+ "success": true
56+ }
57+ ],
58+ [
59+ "Error Channel propagates errors from the second layer",
60+ {
61+ "timestamp": "1775221839085.0",
62+ "success": true
63+ }
64+ ],
65+ [
66+ "Error Channel propagates errors through vertical composition",
67+ {
68+ "timestamp": "1775221839085.0",
69+ "success": true
70+ }
71+ ],
72+ [
73+ "Error Channel scoped layer error triggers finalizers",
74+ {
75+ "timestamp": "1775221839085.0",
76+ "success": true
77+ }
78+ ],
79+ [
80+ "Error Channel scoped layers with errors run via runScopedWith",
81+ {
82+ "timestamp": "1775221839085.0",
83+ "success": true
84+ }
85+ ],
86+ [
87+ "Functor / Apply / Bind applies two layers",
88+ {
89+ "timestamp": "1775221839085.0",
90+ "success": true
91+ }
92+ ],
93+ [
94+ "Functor / Apply / Bind binds layers sequentially",
95+ {
96+ "timestamp": "1775221839085.0",
97+ "success": true
98+ }
99+ ],
100+ [
101+ "Functor / Apply / Bind maps over layer output",
102+ {
103+ "timestamp": "1775221839085.0",
104+ "success": true
105+ }
106+ ],
107+ [
108+ "Functor / Apply / Bind runs layers against a superset context",
109+ {
110+ "timestamp": "1775221839085.0",
111+ "success": true
112+ }
113+ ],
114+ [
115+ "Layer recovering does not retry on non-matching errors",
116+ {
117+ "timestamp": "1775221839085.0",
118+ "success": true
119+ }
120+ ],
121+ [
122+ "Layer recovering only retries the specified error in a multi-error layer",
123+ {
124+ "timestamp": "1775221839085.0",
125+ "success": true
126+ }
127+ ],
128+ [
129+ "Layer recovering preserves the error when retries are exhausted",
130+ {
131+ "timestamp": "1775221839085.0",
132+ "success": true
133+ }
134+ ],
135+ [
136+ "Layer recovering retries layer construction on matching errors",
137+ {
138+ "timestamp": "1775221839085.0",
139+ "success": true
140+ }
141+ ],
142+ [
143+ "Layer recovering uses RetryStatus in the check",
144+ {
145+ "timestamp": "1775221839085.0",
146+ "success": true
147+ }
148+ ],
149+ [
150+ "Layer repeating repeats layer construction while condition holds",
151+ {
152+ "timestamp": "1775221839085.0",
153+ "success": true
154+ }
155+ ],
156+ [
157+ "Layer repeating stops when policy exhausted",
158+ {
159+ "timestamp": "1775221839085.0",
160+ "success": true
161+ }
162+ ],
163+ [
164+ "MonadAsk / MonadReader ask reads the context directly",
165+ {
166+ "timestamp": "1775221839085.0",
167+ "success": true
168+ }
169+ ],
170+ [
171+ "MonadAsk / MonadReader local modifies context for a layer",
172+ {
173+ "timestamp": "1775221839085.0",
174+ "success": true
175+ }
176+ ],
177+ [
178+ "MonadEffect / MonadAff liftEffect works directly in layer bind",
179+ {
180+ "timestamp": "1775221839085.0",
181+ "success": true
182+ }
183+ ],
184+ [
185+ "MonadThrow / MonadError throwError raises into the error channel",
186+ {
187+ "timestamp": "1775221839085.0",
188+ "success": true
189+ }
190+ ],
191+ [
192+ "Parallel runs layers in parallel via parApply",
193+ {
194+ "timestamp": "1775221839085.0",
195+ "success": true
196+ }
197+ ],
198+ [
199+ "Scoped Layers combines scoped and non-scoped layers",
200+ {
201+ "timestamp": "1775221839085.0",
202+ "success": true
203+ }
204+ ],
205+ [
206+ "Scoped Layers fresh layer builds twice even when same base layer",
207+ {
208+ "timestamp": "1775221839085.0",
209+ "success": true
210+ }
211+ ],
212+ [
213+ "Scoped Layers runs finalizers in reverse order on success",
214+ {
215+ "timestamp": "1775221839085.0",
216+ "success": true
217+ }
218+ ],
219+ [
220+ "Scoped Layers runs finalizers when the callback throws",
221+ {
222+ "timestamp": "1775221839085.0",
223+ "success": true
224+ }
225+ ],
226+ [
227+ "Scoped Layers threads finalizers through vertical composition (provide)",
228+ {
229+ "timestamp": "1775221839085.0",
230+ "success": true
231+ }
232+ ],
233+ [
234+ "Scoped Layers works with bracketLayer",
235+ {
236+ "timestamp": "1775221839085.0",
237+ "success": true
238+ }
239+ ],
240+ [
241+ "Semigroup appends layer outputs",
242+ {
243+ "timestamp": "1775221839085.0",
244+ "success": true
245+ }
246+ ],
247+ [
248+ "combinePure combines a plain record with a layer without needing VTA",
249+ {
250+ "timestamp": "1775214062703.0",
251+ "success": true
252+ }
253+ ],
254+ [
255+ "wireLayers memoizes shared dependencies within wireLayers",
256+ {
257+ "timestamp": "1775221839085.0",
258+ "success": true
259+ }
260+ ],
261+ [
262+ "wireLayers wireLayersDebug emits dependency graph as compiler warning",
263+ {
264+ "timestamp": "1775221839085.0",
265+ "success": true
266+ }
267+ ],
268+ [
269+ "wireLayers wires a diamond dependency",
270+ {
271+ "timestamp": "1775221839085.0",
272+ "success": true
273+ }
274+ ],
275+ [
276+ "wireLayers wires a simple linear chain (A -> B -> C)",
277+ {
278+ "timestamp": "1775221839085.0",
279+ "success": true
280+ }
281+ ],
282+ [
283+ "wireLayers wires independent layers with no inter-dependencies",
284+ {
285+ "timestamp": "1775221839085.0",
286+ "success": true
287+ }
288+ ],
289+ [
290+ "withValue combines a plain record with a layer without needing VTA",
291+ {
292+ "timestamp": "1775221839085.0",
293+ "success": true
294+ }
295+ ]
296+]
README.md+1
@@ -240,3 +240,4 @@ combineRequirements (OmLayer build1) (OmLayer build2) = makeLayer do
240240
241241**Status**: Proof of concept complete ✓ with working runtime implementation
242242**Next**: Implement full instance-resolution based layer system with provideLayers API
243+
compile-fail-tests/CombineDuplicateProv.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: TypesDoNotUnify
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, combineRequirements, runScoped)
6+
7+layer1 :: OmLayer (scope :: Scope) () { value :: Int }
8+layer1 = makeLayer (pure { value: 1 })
9+
10+layer2 :: OmLayer (scope :: Scope) () { value :: Int }
11+layer2 = makeLayer (pure { value: 2 })
12+
13+bad = runScoped (combineRequirements layer1 layer2)
compile-fail-tests/CombineOverlappingProv.pursnew file+15
@@ -0,0 +1,15 @@
1+-- EXPECT: TypesDoNotUnify
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, combineRequirements, runScoped)
6+import Effect.Aff (Aff)
7+
8+layer1 :: OmLayer (scope :: Scope) () { value :: Int }
9+layer1 = makeLayer (pure { value: 1 })
10+
11+layer2 :: OmLayer (scope :: Scope) () { value :: String }
12+layer2 = makeLayer (pure { value: "two" })
13+
14+bad :: Aff { value :: Int, value :: String }
15+bad = runScoped (combineRequirements layer1 layer2)
compile-fail-tests/ProvideWrongType.pursnew file+16
@@ -0,0 +1,16 @@
1+-- EXPECT: TypesDoNotUnify
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, makeLayer, provide)
7+
8+base :: OmLayer () () { value :: Int }
9+base = makeLayer (pure { value: 42 })
10+
11+upper :: OmLayer (value :: String) () { out :: String }
12+upper = makeLayer do
13+ { value } <- Om.ask
14+ pure { out: value }
15+
16+bad = upper `provide` base
compile-fail-tests/RunLayerMissingDep.pursnew file+15
@@ -0,0 +1,15 @@
1+-- EXPECT: Missing required dependency
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
7+
8+type Database = { query :: String -> Array String }
9+
10+dbLayer :: OmLayer (config :: { host :: String }) () { db :: Database }
11+dbLayer = makeLayer do
12+ { config } <- Om.ask
13+ pure { db: { query: \_ -> [ config.host ] } }
14+
15+bad = runLayer {} dbLayer
compile-fail-tests/RunLayerOpenRowMissingDep.pursnew file+19
@@ -0,0 +1,19 @@
1+-- EXPECT: Prim.RowList.RowToList
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+
6+import Yoga.Om as Om
7+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
8+
9+type Fastify = String
10+type Connection = String
11+
12+apiLayer :: forall r. OmLayer (fastify :: Fastify, sqlite :: Connection | r) () {}
13+apiLayer = makeLayer do
14+ { fastify, sqlite } <- Om.ask
15+ let _ = fastify <> sqlite
16+ pure {}
17+
18+bad :: Om.Om { fastify :: Fastify } () {}
19+bad = runLayer { fastify: "server" } apiLayer
compile-fail-tests/RunLayerPartialDep.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: Missing required dependency
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
7+
8+layer :: OmLayer (a :: Int, b :: String) () { out :: String }
9+layer = makeLayer do
10+ { a, b } <- Om.ask
11+ pure { out: show a <> b }
12+
13+bad = runLayer { a: 42 } layer
compile-fail-tests/WireLayersMissingDep.pursnew file+16
@@ -0,0 +1,16 @@
1+-- EXPECT: Dependency not provided!
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, wireLayers)
7+
8+a_base :: OmLayer (scope :: Scope) () { base :: String }
9+a_base = makeLayer (pure { base: "ok" })
10+
11+b_upper :: OmLayer (scope :: Scope, port :: Int) () { upper :: String }
12+b_upper = makeLayer do
13+ { port } <- Om.ask
14+ pure { upper: show port }
15+
16+bad = wireLayers { a_base, b_upper }
compile-fail-tests/run.shnew file+38
@@ -0,0 +1,38 @@
1+#!/bin/bash
2+cd "$(dirname "$0")/.."
3+
4+# Build the library first
5+bunx spago build > /dev/null 2>&1
6+
7+PASS=0
8+FAIL=0
9+TOTAL=0
10+
11+for f in compile-fail-tests/*.purs; do
12+ EXPECT=$(head -1 "$f" | sed 's/-- EXPECT: //')
13+ NAME=$(basename "$f" .purs)
14+ TOTAL=$((TOTAL + 1))
15+
16+ cp "$f" "src/_CompileFailTest.purs"
17+ OUTPUT=$(bunx spago build 2>&1) || true
18+ rm -f "src/_CompileFailTest.purs"
19+
20+ if echo "$OUTPUT" | grep -q "Build succeeded"; then
21+ echo "FAIL $NAME — compiled successfully (should have failed)"
22+ FAIL=$((FAIL + 1))
23+ elif echo "$OUTPUT" | grep -q "$EXPECT"; then
24+ echo "PASS $NAME — failed with expected: $EXPECT"
25+ PASS=$((PASS + 1))
26+ else
27+ echo "FAIL $NAME — failed but without expected '$EXPECT'"
28+ echo " Got: $(echo "$OUTPUT" | grep "ERROR\|error" | head -3)"
29+ FAIL=$((FAIL + 1))
30+ fi
31+done
32+
33+echo ""
34+echo "$PASS passed, $FAIL failed out of $TOTAL compile-fail tests"
35+
36+if [ "$FAIL" -ne 0 ]; then
37+ exit 1
38+fi
spago.lock+458−721
@@ -6,86 +6,71 @@
66 "core": {
77 "dependencies": [
88 {
9− "aff": ">=7.0.0 <8.0.0"
9+ "aff": ">=8.0.0 <9.0.0"
1010 },
1111 {
12− "newtype": ">=5.0.0 <6.0.0"
12+ "aff-retry": ">=2.0.0 <3.0.0"
1313 },
1414 {
15− "prelude": ">=6.0.0 <7.0.0"
15+ "arrays": ">=7.3.0 <8.0.0"
16+ },
17+ {
18+ "control": ">=6.0.0 <7.0.0"
19+ },
20+ {
21+ "effect": ">=4.0.0 <5.0.0"
22+ },
23+ {
24+ "exceptions": ">=6.1.0 <7.0.0"
25+ },
26+ {
27+ "foldable-traversable": ">=6.0.0 <7.0.0"
28+ },
29+ {
30+ "maybe": ">=6.0.0 <7.0.0"
31+ },
32+ {
33+ "nullable": ">=6.0.0 <7.0.0"
34+ },
35+ {
36+ "ordered-collections": ">=3.2.0 <4.0.0"
37+ },
38+ {
39+ "parallel": ">=7.0.0 <8.0.0"
40+ },
41+ {
42+ "prelude": ">=6.0.2 <7.0.0"
1643 },
1744 {
1845 "record": ">=4.0.0 <5.0.0"
1946 },
20− "record-studio",
2147 {
22− "type-equality": ">=4.0.0 <5.0.0"
48+ "record-studio": ">=1.0.4 <2.0.0"
49+ },
50+ {
51+ "refs": ">=6.0.0 <7.0.0"
52+ },
53+ {
54+ "tailrec": ">=6.1.0 <7.0.0"
2355 },
24− "unsafe-coerce",
2556 {
26− "yoga-om-core": ">=1.0.0 <2.0.0"
57+ "transformers": ">=6.1.0 <7.0.0"
58+ },
59+ {
60+ "type-equality": ">=4.0.1 <5.0.0"
61+ },
62+ {
63+ "typelevel-prelude": ">=7.0.0 <8.0.0"
64+ },
65+ {
66+ "unsafe-coerce": ">=6.0.0 <7.0.0"
67+ },
68+ {
69+ "variant": ">=8.0.0 <9.0.0"
70+ },
71+ {
72+ "yoga-om": ">=2.0.0 <3.0.0"
2773 }
28− ],
29− "build_plan": [
30− "aff",
31− "ansi",
32− "arrays",
33− "avar",
34− "bifunctors",
35− "catenable-lists",
36− "console",
37− "const",
38− "contravariant",
39− "control",
40− "datetime",
41− "distributive",
42− "effect",
43− "either",
44− "enums",
45− "exceptions",
46− "exists",
47− "foldable-traversable",
48− "fork",
49− "free",
50− "functions",
51− "functors",
52− "gen",
53− "heterogeneous",
54− "identity",
55− "integers",
56− "invariant",
57− "lazy",
58− "lists",
59− "maybe",
60− "mmorph",
61− "newtype",
62− "nonempty",
63− "now",
64− "numbers",
65− "ordered-collections",
66− "orders",
67− "parallel",
68− "partial",
69− "pipes",
70− "prelude",
71− "profunctor",
72− "record",
73− "record-studio",
74− "refs",
75− "safe-coerce",
76− "spec",
77− "st",
78− "strings",
79− "tailrec",
80− "transformers",
81− "tuples",
82− "type-equality",
83− "typelevel-prelude",
84− "uncurried-transformers",
85− "unfoldable",
86− "unsafe-coerce",
87− "variant",
88− "yoga-om-core"
8974 ]
9075 },
9176 "test": {
@@ -97,645 +82,19 @@
9782 "refs": ">=6.0.0 <7.0.0"
9883 },
9984 {
100− "spec": ">=7.0.0 <8.0.0"
85+ "spec": ">=7.0.0 <9.0.0"
86+ },
87+ {
88+ "spec-discovery": ">=8.0.0 <9.0.0"
89+ },
90+ {
91+ "spec-node": ">=0.0.3 <1.0.0"
10192 }
102− ],
103− "build_plan": [
104− "aff",
105− "ansi",
106− "arrays",
107− "avar",
108− "bifunctors",
109− "catenable-lists",
110− "console",
111− "const",
112− "contravariant",
113− "control",
114− "datetime",
115− "distributive",
116− "effect",
117− "either",
118− "enums",
119− "exceptions",
120− "exists",
121− "foldable-traversable",
122− "fork",
123− "free",
124− "functions",
125− "functors",
126− "gen",
127− "identity",
128− "integers",
129− "invariant",
130− "lazy",
131− "lists",
132− "maybe",
133− "mmorph",
134− "newtype",
135− "nonempty",
136− "now",
137− "numbers",
138− "ordered-collections",
139− "orders",
140− "parallel",
141− "partial",
142− "pipes",
143− "prelude",
144− "profunctor",
145− "refs",
146− "safe-coerce",
147− "spec",
148− "st",
149− "strings",
150− "tailrec",
151− "transformers",
152− "tuples",
153− "type-equality",
154− "unfoldable",
155− "unsafe-coerce"
15693 ]
15794 }
15895 }
15996 },
160− "package_set": {
161− "address": {
162− "registry": "72.0.1"
163− },
164− "compiler": ">=0.15.15 <0.16.0",
165− "content": {
166− "abc-parser": "2.1.0",
167− "ace": "9.1.0",
168− "address-rfc2821": "0.1.1",
169− "aff": "8.0.0",
170− "aff-bus": "6.0.0",
171− "aff-coroutines": "9.0.0",
172− "aff-promise": "4.0.0",
173− "aff-retry": "2.0.0",
174− "affjax": "13.0.0",
175− "affjax-node": "1.0.0",
176− "affjax-web": "1.0.0",
177− "ansi": "7.0.0",
178− "apexcharts": "0.5.0",
179− "applicative-phases": "1.0.0",
180− "argonaut": "9.0.0",
181− "argonaut-aeson-generic": "0.4.1",
182− "argonaut-codecs": "9.1.0",
183− "argonaut-core": "7.0.0",
184− "argonaut-generic": "8.0.0",
185− "argonaut-traversals": "10.0.0",
186− "argparse-basic": "2.0.0",
187− "array-builder": "0.1.2",
188− "array-search": "0.6.0",
189− "arraybuffer": "13.2.0",
190− "arraybuffer-builder": "3.1.0",
191− "arraybuffer-types": "3.0.2",
192− "arrays": "7.3.0",
193− "arrays-extra": "0.6.1",
194− "arrays-zipper": "2.0.1",
195− "ask": "1.0.0",
196− "assert": "6.0.0",
197− "assert-multiple": "0.4.0",
198− "avar": "5.0.1",
199− "axon": "0.0.3",
200− "b64": "0.0.8",
201− "barbies": "1.0.1",
202− "barlow-lens": "0.9.0",
203− "benchlib": "0.0.4",
204− "bifunctors": "6.1.0",
205− "bigints": "7.0.1",
206− "blessed": "1.0.0",
207− "bolson": "0.3.9",
208− "bookhound": "0.1.7",
209− "bower-json": "3.0.0",
210− "call-by-name": "4.0.1",
211− "canvas": "6.0.0",
212− "canvas-action": "9.0.0",
213− "cartesian": "1.0.6",
214− "catenable-lists": "7.0.0",
215− "cbor-stream": "1.3.0",
216− "chameleon": "1.0.0",
217− "chameleon-halogen": "1.0.3",
218− "chameleon-react-basic": "1.1.0",
219− "chameleon-styled": "2.5.0",
220− "chameleon-transformers": "1.0.0",
221− "channel": "1.0.0",
222− "checked-exceptions": "3.1.1",
223− "choku": "1.0.2",
224− "classless": "0.1.1",
225− "classless-arbitrary": "0.1.1",
226− "classless-decode-json": "0.1.1",
227− "classless-encode-json": "0.1.3",
228− "classnames": "2.0.0",
229− "codec": "6.1.0",
230− "codec-argonaut": "10.0.0",
231− "codec-json": "2.0.0",
232− "colors": "7.0.1",
233− "concur-core": "0.5.0",
234− "concur-react": "0.5.0",
235− "concurrent-queues": "3.0.0",
236− "console": "6.1.0",
237− "const": "6.0.0",
238− "contravariant": "6.0.0",
239− "control": "6.0.0",
240− "convertable-options": "1.0.0",
241− "coroutines": "7.0.0",
242− "css": "6.0.0",
243− "css-class-name-extractor": "0.0.4",
244− "css-frameworks": "1.1.0",
245− "csv-stream": "2.3.0",
246− "data-mvc": "0.0.2",
247− "datetime": "6.1.0",
248− "datetime-parsing": "0.2.0",
249− "debounce": "0.1.0",
250− "debug": "6.0.2",
251− "decimals": "7.1.0",
252− "default-values": "1.0.1",
253− "deku": "0.9.23",
254− "deno": "0.0.5",
255− "dissect": "1.0.0",
256− "distributive": "6.0.0",
257− "dodo-printer": "2.2.3",
258− "dom-filereader": "7.0.0",
259− "dom-indexed": "13.0.0",
260− "dom-simple": "0.4.0",
261− "dotenv": "4.0.3",
262− "droplet": "0.6.0",
263− "dts": "1.0.0",
264− "dual-numbers": "1.0.3",
265− "dynamic-buffer": "3.0.1",
266− "echarts-simple": "0.0.1",
267− "effect": "4.0.0",
268− "either": "6.1.0",
269− "elmish": "0.13.0",
270− "elmish-enzyme": "0.1.1",
271− "elmish-hooks": "0.11.0",
272− "elmish-html": "0.10.0",
273− "elmish-testing-library": "0.3.2",
274− "elmish-time-machine": "0.4.2",
275− "email-validate": "7.0.0",
276− "encoding": "0.0.9",
277− "enums": "6.0.1",
278− "env-names": "0.4.0",
279− "environment": "2.0.0",
280− "error": "2.0.0",
281− "eta-conversion": "0.3.2",
282− "exceptions": "6.1.0",
283− "exists": "6.0.0",
284− "exitcodes": "4.0.0",
285− "expect-inferred": "3.0.0",
286− "express": "0.9.1",
287− "ezfetch": "1.1.1",
288− "fahrtwind": "2.0.0",
289− "fakerjs": "0.0.1",
290− "fallback": "0.1.0",
291− "fast-vect": "1.2.0",
292− "fetch": "4.1.0",
293− "fetch-argonaut": "1.0.1",
294− "fetch-core": "5.1.0",
295− "fetch-yoga-json": "1.1.0",
296− "ffi-simple": "0.5.1",
297− "fft": "0.3.0",
298− "fft-js": "0.1.0",
299− "filterable": "5.0.0",
300− "fix-functor": "0.1.0",
301− "fixed-points": "7.0.0",
302− "fixed-precision": "5.0.0",
303− "flame": "1.6.0",
304− "float32": "2.0.0",
305− "fmt": "0.2.1",
306− "foldable-traversable": "6.0.0",
307− "foldable-traversable-extra": "0.0.6",
308− "foreign": "7.0.0",
309− "foreign-object": "4.1.0",
310− "foreign-readwrite": "3.4.0",
311− "forgetmenot": "0.1.0",
312− "fork": "6.0.0",
313− "form-urlencoded": "7.0.0",
314− "formatters": "7.0.0",
315− "framer-motion": "2.0.1",
316− "free": "7.1.0",
317− "freeap": "7.0.0",
318− "freer-free": "0.0.1",
319− "freet": "7.0.0",
320− "functions": "6.0.0",
321− "functor1": "3.0.0",
322− "functors": "5.0.0",
323− "fuzzy": "0.4.0",
324− "gen": "4.0.0",
325− "generate-values": "1.0.1",
326− "generic-router": "0.0.1",
327− "geojson": "0.0.5",
328− "geometria": "2.2.0",
329− "gesso": "1.0.1",
330− "gojs": "0.1.1",
331− "golem-fetch": "0.1.0",
332− "grain": "3.0.0",
333− "grain-router": "3.0.0",
334− "grain-virtualized": "3.0.0",
335− "graphql-client": "10.1.1",
336− "graphs": "8.1.0",
337− "group": "4.1.1",
338− "halogen": "7.0.0",
339− "halogen-bootstrap5": "5.3.2",
340− "halogen-canvas": "1.0.0",
341− "halogen-css": "10.0.0",
342− "halogen-declarative-canvas": "0.0.8",
343− "halogen-echarts-simple": "0.0.4",
344− "halogen-formless": "4.0.3",
345− "halogen-helix": "1.1.0",
346− "halogen-hooks": "0.6.3",
347− "halogen-hooks-extra": "0.9.0",
348− "halogen-infinite-scroll": "1.1.0",
349− "halogen-store": "0.5.4",
350− "halogen-storybook": "2.0.0",
351− "halogen-subscriptions": "2.0.0",
352− "halogen-svg-elems": "8.0.0",
353− "halogen-typewriter": "1.0.4",
354− "halogen-use-trigger-hooks": "1.0.0",
355− "halogen-vdom": "8.0.0",
356− "halogen-vdom-string-renderer": "0.5.0",
357− "halogen-xterm": "2.0.0",
358− "heckin": "2.0.1",
359− "heterogeneous": "0.7.0",
360− "homogeneous": "0.4.0",
361− "http-methods": "6.0.0",
362− "httpurple": "4.0.0",
363− "huffman": "0.4.0",
364− "humdrum": "0.0.1",
365− "hyrule": "2.3.8",
366− "identity": "6.0.0",
367− "identy": "4.0.1",
368− "indexed-db": "1.0.0",
369− "indexed-monad": "3.0.0",
370− "ink": "0.0.1",
371− "int64": "3.0.0",
372− "integers": "6.0.0",
373− "interpolate": "5.0.2",
374− "intersection-observer": "1.0.1",
375− "invariant": "6.0.0",
376− "jarilo": "1.0.1",
377− "jelly": "0.10.0",
378− "jelly-router": "0.3.0",
379− "jelly-signal": "0.4.0",
380− "jest": "1.0.0",
381− "js-abort-controller": "1.0.0",
382− "js-bigints": "2.2.1",
383− "js-date": "8.0.0",
384− "js-fetch": "0.2.1",
385− "js-fileio": "3.0.0",
386− "js-intl": "1.1.4",
387− "js-iterators": "0.1.1",
388− "js-maps": "0.1.2",
389− "js-promise": "1.0.0",
390− "js-promise-aff": "1.0.0",
391− "js-timers": "6.1.0",
392− "js-uri": "3.1.0",
393− "jsdom": "1.0.0",
394− "json": "1.1.0",
395− "json-codecs": "5.0.0",
396− "justifill": "0.5.0",
397− "jwt": "0.0.9",
398− "labeled-data": "0.2.0",
399− "language-cst-parser": "0.14.1",
400− "lazy": "6.0.0",
401− "lazy-joe": "1.0.0",
402− "lcg": "4.0.0",
403− "leibniz": "5.0.0",
404− "leveldb": "1.0.1",
405− "liminal": "1.0.1",
406− "linalg": "6.0.0",
407− "lists": "7.0.0",
408− "literals": "1.0.2",
409− "logging": "3.0.0",
410− "logging-journald": "0.4.0",
411− "lumi-components": "18.0.0",
412− "machines": "7.0.0",
413− "maps-eager": "0.5.0",
414− "marionette": "1.0.0",
415− "marionette-react-basic-hooks": "0.1.1",
416− "marked": "0.1.0",
417− "matrices": "5.0.1",
418− "matryoshka": "1.0.0",
419− "maybe": "6.0.0",
420− "media-types": "6.0.0",
421− "meowclient": "1.0.0",
422− "midi": "4.0.0",
423− "milkis": "9.0.0",
424− "mimetype": "0.0.1",
425− "minibench": "4.0.1",
426− "mmorph": "7.0.0",
427− "monad-control": "5.0.0",
428− "monad-logger": "1.3.1",
429− "monad-loops": "0.5.0",
430− "monad-unlift": "1.0.1",
431− "monoid-extras": "0.0.1",
432− "monoidal": "0.16.0",
433− "morello": "0.4.0",
434− "mote": "3.0.0",
435− "motsunabe": "2.0.0",
436− "mvc": "0.0.1",
437− "mysql": "6.0.1",
438− "n3": "0.1.0",
439− "nano-id": "1.1.0",
440− "nanoid": "0.1.0",
441− "naturals": "3.0.0",
442− "nested-functor": "0.2.1",
443− "newtype": "5.0.0",
444− "nextjs": "0.1.1",
445− "nextui": "0.2.0",
446− "node-buffer": "9.0.0",
447− "node-child-process": "11.1.0",
448− "node-event-emitter": "3.0.0",
449− "node-execa": "5.0.0",
450− "node-fs": "9.2.0",
451− "node-glob-basic": "2.0.0",
452− "node-http": "9.1.0",
453− "node-http2": "1.1.1",
454− "node-human-signals": "1.0.0",
455− "node-net": "5.1.0",
456− "node-os": "5.1.0",
457− "node-path": "5.0.1",
458− "node-process": "11.2.0",
459− "node-readline": "8.1.1",
460− "node-sqlite3": "8.0.0",
461− "node-stream-pipes": "2.1.6",
462− "node-streams": "9.0.1",
463− "node-tls": "0.3.1",
464− "node-url": "7.0.1",
465− "node-workerbees": "0.3.1",
466− "node-zlib": "0.4.0",
467− "nonempty": "7.0.0",
468− "now": "6.0.0",
469− "npm-package-json": "2.0.0",
470− "nullable": "6.0.0",
471− "numberfield": "0.2.2",
472− "numbers": "9.0.1",
473− "oak": "3.1.1",
474− "oak-debug": "1.2.2",
475− "object-maps": "0.3.0",
476− "ocarina": "1.5.4",
477− "oooooooooorrrrrrrmm-lib": "0.0.1",
478− "open-colors-scales-and-schemes": "1.0.0",
479− "open-drawing": "6.0.4",
480− "open-folds": "6.4.0",
481− "open-foreign-generic": "11.0.3",
482− "open-memoize": "6.2.0",
483− "open-mkdirp-aff": "1.2.0",
484− "open-pairing": "6.2.0",
485− "open-smolder": "12.0.2",
486− "options": "7.0.0",
487− "optparse": "5.0.1",
488− "ordered-collections": "3.2.0",
489− "ordered-set": "0.4.0",
490− "orders": "6.0.0",
491− "org-doc": "0.1.0",
492− "owoify": "1.2.0",
493− "pairs": "9.0.1",
494− "parallel": "7.0.0",
495− "parsing": "11.0.0",
496− "parsing-dataview": "3.2.4",
497− "partial": "4.0.0",
498− "pathy": "9.0.0",
499− "pha": "0.13.0",
500− "phaser": "0.7.0",
501− "phylio": "1.1.2",
502− "pipes": "8.0.0",
503− "pirates-charm": "0.0.1",
504− "play": "1.0.0",
505− "pmock": "0.9.0",
506− "point-free": "1.0.0",
507− "pointed-list": "0.5.1",
508− "polymorphic-vectors": "4.0.0",
509− "posix-types": "6.0.0",
510− "postgresql": "2.0.21",
511− "precise": "6.0.0",
512− "precise-datetime": "7.0.0",
513− "prelude": "6.0.2",
514− "prettier-printer": "3.0.0",
515− "printf": "0.1.0",
516− "priority-queue": "0.1.2",
517− "profunctor": "6.0.1",
518− "profunctor-lenses": "8.0.0",
519− "protobuf": "4.4.0",
520− "psa-utils": "8.0.0",
521− "psci-support": "6.0.0",
522− "punycode": "1.0.0",
523− "pursfmt": "0.17.0",
524− "qualified-do": "2.2.0",
525− "quantities": "12.2.0",
526− "quickcheck": "8.0.1",
527− "quickcheck-combinators": "0.1.3",
528− "quickcheck-laws": "7.0.0",
529− "quickcheck-utf8": "0.0.0",
530− "random": "6.0.0",
531− "rationals": "6.0.0",
532− "rdf": "0.1.0",
533− "react": "11.0.0",
534− "react-aria": "0.2.0",
535− "react-basic": "17.0.0",
536− "react-basic-classic": "3.0.0",
537− "react-basic-dnd": "10.1.0",
538− "react-basic-dom": "7.0.0",
539− "react-basic-dom-beta": "0.1.1",
540− "react-basic-emotion": "7.1.0",
541− "react-basic-hooks": "9.0.0",
542− "react-basic-storybook": "2.0.0",
543− "react-dom": "8.0.0",
544− "react-halo": "3.0.0",
545− "react-icons": "1.1.5",
546− "react-markdown": "0.1.0",
547− "react-testing-library": "4.0.1",
548− "react-virtuoso": "1.0.0",
549− "reactix": "0.6.1",
550− "read": "1.0.1",
551− "recharts": "1.1.0",
552− "record": "4.0.0",
553− "record-extra": "5.0.1",
554− "record-extra-srghma": "0.2.8",
555− "record-ptional-fields": "0.1.2",
556− "record-studio": "1.0.4",
557− "refs": "6.0.0",
558− "remotedata": "5.0.1",
559− "repr": "0.5.0",
560− "resize-arrays": "0.0.1",
561− "resize-observer": "1.0.0",
562− "resource": "2.0.1",
563− "resourcet": "1.0.0",
564− "result": "1.0.3",
565− "return": "0.2.0",
566− "ring-modules": "5.0.1",
567− "rito": "0.3.4",
568− "roman": "0.4.0",
569− "rough-notation": "1.0.2",
570− "routing": "11.0.0",
571− "routing-duplex": "0.7.0",
572− "run": "5.0.0",
573− "safe-coerce": "2.0.0",
574− "safely": "4.0.1",
575− "school-of-music": "1.3.0",
576− "selection-foldable": "0.2.0",
577− "selective-functors": "1.0.1",
578− "semirings": "7.0.0",
579− "shuffle": "2.0.0",
580− "signal": "13.0.0",
581− "simple-emitter": "3.0.1",
582− "simple-i18n": "2.0.1",
583− "simple-json": "9.0.0",
584− "simple-json-generics": "0.2.1",
585− "simple-ulid": "3.0.0",
586− "sized-matrices": "1.0.0",
587− "sized-vectors": "5.0.2",
588− "slug": "3.2.0",
589− "small-ffi": "4.0.1",
590− "soundfonts": "4.1.0",
591− "sparse-matrices": "2.0.1",
592− "sparse-polynomials": "3.0.1",
593− "spec": "8.1.1",
594− "spec-discovery": "8.4.0",
595− "spec-mocha": "5.1.1",
596− "spec-node": "0.0.3",
597− "spec-quickcheck": "5.0.2",
598− "spec-reporter-xunit": "0.7.1",
599− "splitmix": "2.1.0",
600− "ssrs": "1.0.0",
601− "st": "6.2.0",
602− "statistics": "0.3.2",
603− "strictlypositiveint": "1.0.1",
604− "string-parsers": "8.0.0",
605− "strings": "6.0.1",
606− "strings-extra": "4.0.0",
607− "stringutils": "0.0.12",
608− "structured-logging": "1.0.0",
609− "substitute": "0.2.3",
610− "supply": "0.2.0",
611− "svg-parser": "3.0.0",
612− "systemd-journald": "0.3.0",
613− "tagged": "4.0.2",
614− "tailrec": "6.1.0",
615− "tanstack-query": "2.0.0",
616− "tecton": "0.2.1",
617− "tecton-halogen": "0.2.0",
618− "test-unit": "17.0.0",
619− "text-formatting": "0.1.0",
620− "thermite": "6.3.1",
621− "thermite-dom": "0.3.1",
622− "these": "6.0.0",
623− "threading": "0.0.3",
624− "tidy": "0.11.1",
625− "tidy-codegen": "4.0.1",
626− "tldr": "0.0.0",
627− "toestand": "0.9.0",
628− "transformation-matrix": "1.0.1",
629− "transformers": "6.1.0",
630− "transit": "1.0.0",
631− "tree-rose": "4.0.2",
632− "trivial-unfold": "0.5.0",
633− "ts-bridge": "4.0.0",
634− "tuples": "7.0.0",
635− "two-or-more": "1.0.0",
636− "type-equality": "4.0.1",
637− "typedenv": "2.0.1",
638− "typelevel": "6.0.0",
639− "typelevel-lists": "2.1.0",
640− "typelevel-peano": "1.0.1",
641− "typelevel-prelude": "7.0.0",
642− "typelevel-regex": "0.0.3",
643− "typelevel-rows": "0.1.0",
644− "typisch": "0.4.0",
645− "uint": "7.0.0",
646− "ulid": "3.0.1",
647− "uncurried-transformers": "1.1.0",
648− "undefined": "2.0.0",
649− "undefined-is-not-a-problem": "1.1.0",
650− "unfoldable": "6.0.0",
651− "unicode": "6.0.0",
652− "unique": "0.6.1",
653− "unlift": "1.0.1",
654− "unordered-collections": "3.1.0",
655− "unsafe-coerce": "6.0.0",
656− "unsafe-reference": "5.0.0",
657− "untagged-to-tagged": "0.1.4",
658− "untagged-union": "1.0.0",
659− "uri": "9.0.0",
660− "url-immutable": "1.0.0",
661− "url-regex-safe": "0.1.1",
662− "uuid": "9.0.0",
663− "uuidv4": "1.0.0",
664− "validation": "6.0.0",
665− "variant": "8.0.0",
666− "variant-encodings": "2.0.0",
667− "variant-gen": "1.0.0",
668− "vectorfield": "1.0.1",
669− "vectors": "2.1.0",
670− "versions": "7.0.0",
671− "visx": "0.0.2",
672− "vitest": "1.0.0",
673− "web-clipboard": "6.0.0",
674− "web-cssom": "2.0.0",
675− "web-cssom-view": "0.1.0",
676− "web-dom": "6.0.0",
677− "web-dom-parser": "8.0.0",
678− "web-dom-xpath": "3.0.0",
679− "web-encoding": "3.0.0",
680− "web-events": "4.0.0",
681− "web-fetch": "4.0.1",
682− "web-file": "4.0.0",
683− "web-geometry": "0.1.0",
684− "web-html": "4.1.1",
685− "web-pointerevents": "2.0.0",
686− "web-proletarian": "1.0.0",
687− "web-promise": "3.2.0",
688− "web-resize-observer": "2.1.0",
689− "web-router": "1.0.0",
690− "web-socket": "4.0.0",
691− "web-storage": "5.0.0",
692− "web-streams": "4.0.0",
693− "web-touchevents": "4.0.0",
694− "web-uievents": "5.0.0",
695− "web-url": "2.0.0",
696− "web-workers": "1.1.0",
697− "web-xhr": "5.0.1",
698− "webb-aff-list": "0.0.2",
699− "webb-array": "0.0.2",
700− "webb-channel": "0.0.3",
701− "webb-commandline": "0.0.3",
702− "webb-directory": "0.0.7",
703− "webb-file": "0.0.1",
704− "webb-map": "0.0.1",
705− "webb-monad": "0.0.2",
706− "webb-mutex": "0.0.7",
707− "webb-parsing": "0.0.5",
708− "webb-random": "0.0.1",
709− "webb-refer": "0.0.3",
710− "webb-set": "0.0.1",
711− "webb-slot": "0.0.1",
712− "webb-stateful": "0.0.3",
713− "webb-string": "0.0.1",
714− "webb-test": "0.0.1",
715− "webb-thread": "0.0.2",
716− "webb-writer": "0.0.3",
717− "webextension-polyfill": "0.1.0",
718− "webgpu": "0.0.1",
719− "which": "2.0.0",
720− "whine-core": "0.0.33",
721− "xterm": "1.0.0",
722− "yaml-next": "3.1.1",
723− "yoga-fetch": "1.0.1",
724− "yoga-json": "5.1.0",
725− "yoga-om": "0.1.0",
726− "yoga-postgres": "6.0.0",
727− "yoga-react-dom": "1.0.1",
728− "yoga-subtlecrypto": "0.1.0",
729− "yoga-tree": "1.0.0",
730− "yoga-tree-svg": "0.1.0",
731− "yoga-tree-utils": "1.0.0",
732− "z3": "0.0.2",
733− "zipperarray": "2.0.0"
734− }
735− },
736− "extra_packages": {
737− "yoga-om-core": "1.0.4"
738− }
97+ "extra_packages": {}
73998 },
74099 "packages": {
741100 "aff": {
@@ -743,24 +102,42 @@
743102 "version": "8.0.0",
744103 "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
745104 "dependencies": [
746− "bifunctors",
747105 "control",
748106 "datetime",
749107 "effect",
750108 "either",
751109 "exceptions",
752− "foldable-traversable",
753110 "functions",
754− "maybe",
755111 "newtype",
756112 "parallel",
113+ "partial",
757114 "prelude",
758− "refs",
115+ "st",
759116 "tailrec",
760117 "transformers",
761118 "unsafe-coerce"
762119 ]
763120 },
121+ "aff-retry": {
122+ "type": "registry",
123+ "version": "2.0.0",
124+ "integrity": "sha256-rKZNcP4IKSyBFQBVzrCvE0ULoJt0N/uDLLJ0dPotr4k=",
125+ "dependencies": [
126+ "aff",
127+ "arrays",
128+ "datetime",
129+ "effect",
130+ "either",
131+ "exceptions",
132+ "integers",
133+ "maybe",
134+ "newtype",
135+ "numbers",
136+ "prelude",
137+ "random",
138+ "transformers"
139+ ]
140+ },
764141 "ansi": {
765142 "type": "registry",
766143 "version": "7.0.0",
@@ -768,9 +145,56 @@
768145 "dependencies": [
769146 "foldable-traversable",
770147 "lists",
771− "strings"
148+ "prelude"
772149 ]
773150 },
151+ "argonaut-codecs": {
152+ "type": "registry",
153+ "version": "9.1.0",
154+ "integrity": "sha256-K910SBrmYallESsm8pxJYThs7lig6hxX3M4PW33Cgew=",
155+ "dependencies": [
156+ "argonaut-core",
157+ "arrays",
158+ "bifunctors",
159+ "either",
160+ "foldable-traversable",
161+ "foreign-object",
162+ "identity",
163+ "integers",
164+ "lists",
165+ "maybe",
166+ "nonempty",
167+ "ordered-collections",
168+ "prelude",
169+ "record",
170+ "strings",
171+ "tuples"
172+ ]
173+ },
174+ "argonaut-core": {
175+ "type": "registry",
176+ "version": "7.0.0",
177+ "integrity": "sha256-gYZihtBIKthVuXXoiPphMookSf8M5aHyFYkcIWqlxTM=",
178+ "dependencies": [
179+ "arrays",
180+ "control",
181+ "either",
182+ "foreign-object",
183+ "functions",
184+ "gen",
185+ "maybe",
186+ "nonempty",
187+ "prelude",
188+ "strings",
189+ "tailrec"
190+ ]
191+ },
192+ "arraybuffer-types": {
193+ "type": "registry",
194+ "version": "3.0.2",
195+ "integrity": "sha256-p05cJnSkyeoB7VHzMyc2Eb1RwUaB7Nl0sQSqEGNEUD8=",
196+ "dependencies": []
197+ },
774198 "arrays": {
775199 "type": "registry",
776200 "version": "7.3.0",
@@ -802,7 +226,8 @@
802226 "either",
803227 "exceptions",
804228 "functions",
805− "maybe"
229+ "maybe",
230+ "prelude"
806231 ]
807232 },
808233 "bifunctors": {
@@ -961,6 +386,16 @@
961386 "unsafe-coerce"
962387 ]
963388 },
389+ "exitcodes": {
390+ "type": "registry",
391+ "version": "4.0.0",
392+ "integrity": "sha256-xBl4aFSEQ05Owp1ZvO6OxxvLK3LVzU35RSbj79SsePQ=",
393+ "dependencies": [
394+ "enums",
395+ "maybe",
396+ "prelude"
397+ ]
398+ },
964399 "foldable-traversable": {
965400 "type": "registry",
966401 "version": "6.0.0",
@@ -979,12 +414,50 @@
979414 "tuples"
980415 ]
981416 },
417+ "foreign": {
418+ "type": "registry",
419+ "version": "7.0.0",
420+ "integrity": "sha256-jiDRMVQfovGsbbA/FzbzYf6iWk9i2b5gNrIjz+gFfsI=",
421+ "dependencies": [
422+ "either",
423+ "functions",
424+ "integers",
425+ "lists",
426+ "maybe",
427+ "prelude",
428+ "strings",
429+ "transformers",
430+ "unsafe-coerce"
431+ ]
432+ },
433+ "foreign-object": {
434+ "type": "registry",
435+ "version": "4.1.0",
436+ "integrity": "sha256-x/Q7r80z/vmHRKvPwhYmHP/DnJciPtsyGfRTMzayKIU=",
437+ "dependencies": [
438+ "arrays",
439+ "foldable-traversable",
440+ "functions",
441+ "gen",
442+ "lists",
443+ "maybe",
444+ "prelude",
445+ "st",
446+ "tailrec",
447+ "tuples",
448+ "typelevel-prelude",
449+ "unfoldable",
450+ "unsafe-coerce"
451+ ]
452+ },
982453 "fork": {
983454 "type": "registry",
984455 "version": "6.0.0",
985456 "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
986457 "dependencies": [
987− "aff"
458+ "aff",
459+ "prelude",
460+ "transformers"
988461 ]
989462 },
990463 "free": {
@@ -1097,6 +570,21 @@
1097570 "prelude"
1098571 ]
1099572 },
573+ "js-date": {
574+ "type": "registry",
575+ "version": "8.0.0",
576+ "integrity": "sha256-DNQrp4xYc8o80jtxo0aOkUeXGi6h0Xidkk2ZQ8Y4l5Y=",
577+ "dependencies": [
578+ "datetime",
579+ "effect",
580+ "enums",
581+ "foreign",
582+ "functions",
583+ "integers",
584+ "maybe",
585+ "prelude"
586+ ]
587+ },
1100588 "lazy": {
1101589 "type": "registry",
1102590 "version": "6.0.0",
@@ -1143,9 +631,16 @@
1143631 "version": "7.0.0",
1144632 "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
1145633 "dependencies": [
634+ "bifunctors",
635+ "either",
1146636 "free",
1147637 "functors",
1148− "transformers"
638+ "identity",
639+ "maybe",
640+ "newtype",
641+ "prelude",
642+ "transformers",
643+ "tuples"
1149644 ]
1150645 },
1151646 "newtype": {
@@ -1157,6 +652,107 @@
1157652 "safe-coerce"
1158653 ]
1159654 },
655+ "node-buffer": {
656+ "type": "registry",
657+ "version": "9.0.0",
658+ "integrity": "sha256-RdQjilmEHqsT4fwpCr8Mavw8KTafYrS3ZM6zeAAVAl8=",
659+ "dependencies": [
660+ "arraybuffer-types",
661+ "effect",
662+ "functions",
663+ "maybe",
664+ "nullable",
665+ "partial",
666+ "prelude",
667+ "st",
668+ "unsafe-coerce"
669+ ]
670+ },
671+ "node-event-emitter": {
672+ "type": "registry",
673+ "version": "3.0.0",
674+ "integrity": "sha256-wodx71NxJBPXOaawFn01V1ByYB2vT+I3RuV2giOVKMg=",
675+ "dependencies": [
676+ "effect",
677+ "either",
678+ "functions",
679+ "maybe",
680+ "nullable",
681+ "prelude",
682+ "unsafe-coerce"
683+ ]
684+ },
685+ "node-fs": {
686+ "type": "registry",
687+ "version": "9.2.0",
688+ "integrity": "sha256-fxioTSm9y47WuYs9F/7nxCh/RVDCl3Kr3Hg779J4SJA=",
689+ "dependencies": [
690+ "aff",
691+ "datetime",
692+ "effect",
693+ "either",
694+ "enums",
695+ "exceptions",
696+ "functions",
697+ "integers",
698+ "js-date",
699+ "maybe",
700+ "node-buffer",
701+ "node-path",
702+ "node-streams",
703+ "nullable",
704+ "partial",
705+ "prelude",
706+ "strings"
707+ ]
708+ },
709+ "node-path": {
710+ "type": "registry",
711+ "version": "5.0.1",
712+ "integrity": "sha256-j7n/SmpVz0FOMJl4XZop3ofJ+MeIPkMNqUEUKHEL6Us=",
713+ "dependencies": [
714+ "effect"
715+ ]
716+ },
717+ "node-process": {
718+ "type": "registry",
719+ "version": "11.2.0",
720+ "integrity": "sha256-onzKeNmaeYfN3HSDPqSFTrrP2Yl9tnVsfKKhnf2plxM=",
721+ "dependencies": [
722+ "effect",
723+ "exceptions",
724+ "foreign",
725+ "foreign-object",
726+ "maybe",
727+ "node-event-emitter",
728+ "node-streams",
729+ "nullable",
730+ "posix-types",
731+ "prelude",
732+ "strings"
733+ ]
734+ },
735+ "node-streams": {
736+ "type": "registry",
737+ "version": "9.0.1",
738+ "integrity": "sha256-yuVGm/R/tBr4Nphi+a1a984Z0fkmN9Q5wWtSmbmJTpA=",
739+ "dependencies": [
740+ "aff",
741+ "arrays",
742+ "effect",
743+ "either",
744+ "exceptions",
745+ "maybe",
746+ "node-buffer",
747+ "node-event-emitter",
748+ "nullable",
749+ "prelude",
750+ "refs",
751+ "st",
752+ "tailrec",
753+ "unsafe-coerce"
754+ ]
755+ },
1160756 "nonempty": {
1161757 "type": "registry",
1162758 "version": "7.0.0",
@@ -1176,7 +772,18 @@
1176772 "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
1177773 "dependencies": [
1178774 "datetime",
1179− "effect"
775+ "effect",
776+ "prelude"
777+ ]
778+ },
779+ "nullable": {
780+ "type": "registry",
781+ "version": "6.0.0",
782+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
783+ "dependencies": [
784+ "functions",
785+ "maybe",
786+ "prelude"
1180787 ]
1181788 },
1182789 "numbers": {
@@ -1185,7 +792,58 @@
1185792 "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
1186793 "dependencies": [
1187794 "functions",
1188− "maybe"
795+ "maybe",
796+ "prelude"
797+ ]
798+ },
799+ "open-memoize": {
800+ "type": "registry",
801+ "version": "6.2.0",
802+ "integrity": "sha256-yJLuVYX8FocHIJ+6q7efH/Pj4S+e1Y57DZ5Dn7qf4ww=",
803+ "dependencies": [
804+ "either",
805+ "integers",
806+ "lazy",
807+ "lists",
808+ "maybe",
809+ "partial",
810+ "prelude",
811+ "strings",
812+ "tuples"
813+ ]
814+ },
815+ "optparse": {
816+ "type": "registry",
817+ "version": "5.0.1",
818+ "integrity": "sha256-2btDjHPRR0uujUQJuZAN9AzvVoXe7yG6IwnY5evERhQ=",
819+ "dependencies": [
820+ "arrays",
821+ "bifunctors",
822+ "control",
823+ "effect",
824+ "either",
825+ "enums",
826+ "exists",
827+ "exitcodes",
828+ "foldable-traversable",
829+ "free",
830+ "integers",
831+ "lazy",
832+ "lists",
833+ "maybe",
834+ "newtype",
835+ "node-buffer",
836+ "node-process",
837+ "node-streams",
838+ "nonempty",
839+ "numbers",
840+ "open-memoize",
841+ "partial",
842+ "prelude",
843+ "strings",
844+ "tailrec",
845+ "transformers",
846+ "tuples"
1189847 ]
1190848 },
1191849 "ordered-collections": {
@@ -1194,13 +852,16 @@
1194852 "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
1195853 "dependencies": [
1196854 "arrays",
855+ "control",
1197856 "foldable-traversable",
857+ "functions",
1198858 "gen",
1199859 "lists",
1200860 "maybe",
861+ "newtype",
1201862 "partial",
1202863 "prelude",
1203− "st",
864+ "safe-coerce",
1204865 "tailrec",
1205866 "tuples",
1206867 "unfoldable"
@@ -1245,14 +906,31 @@
1245906 "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
1246907 "dependencies": [
1247908 "aff",
909+ "control",
910+ "effect",
911+ "either",
912+ "foldable-traversable",
913+ "identity",
1248914 "lists",
915+ "maybe",
1249916 "mmorph",
917+ "newtype",
1250918 "prelude",
1251919 "tailrec",
1252920 "transformers",
1253921 "tuples"
1254922 ]
1255923 },
924+ "posix-types": {
925+ "type": "registry",
926+ "version": "6.0.0",
927+ "integrity": "sha256-Je5jG439F/SFO/IvVevZnWVxDIonrehlKuJkefP2lHE=",
928+ "dependencies": [
929+ "maybe",
930+ "newtype",
931+ "prelude"
932+ ]
933+ },
1256934 "prelude": {
1257935 "type": "registry",
1258936 "version": "6.0.2",
@@ -1274,6 +952,16 @@
1274952 "tuples"
1275953 ]
1276954 },
955+ "random": {
956+ "type": "registry",
957+ "version": "6.0.0",
958+ "integrity": "sha256-7sZRo6P9UIiXDZ9s2QL9T8h3wlIwLdQrR4+oZKOijQw=",
959+ "dependencies": [
960+ "effect",
961+ "integers",
962+ "prelude"
963+ ]
964+ },
1277965 "record": {
1278966 "type": "registry",
1279967 "version": "4.0.0",
@@ -1316,8 +1004,8 @@
13161004 },
13171005 "spec": {
13181006 "type": "registry",
1319− "version": "8.1.1",
1320− "integrity": "sha256-b6qjDb7lUJGLV+WEoywLu6BCXC4zGpfbN43mTL+vBeI=",
1007+ "version": "8.1.2",
1008+ "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=",
13211009 "dependencies": [
13221010 "aff",
13231011 "ansi",
@@ -1348,11 +1036,57 @@
13481036 "tuples"
13491037 ]
13501038 },
1039+ "spec-discovery": {
1040+ "type": "registry",
1041+ "version": "8.4.0",
1042+ "integrity": "sha256-aFp8bllT/wrxQu8hMlDchQQY08dQAz+Xdq2hZi5z3yY=",
1043+ "dependencies": [
1044+ "aff",
1045+ "effect",
1046+ "foldable-traversable",
1047+ "prelude",
1048+ "spec",
1049+ "spec-node"
1050+ ]
1051+ },
1052+ "spec-node": {
1053+ "type": "registry",
1054+ "version": "0.0.3",
1055+ "integrity": "sha256-8/9+g0obbamV0YtqzmttbWs8HaWF7CEeW+eDtqzuAAo=",
1056+ "dependencies": [
1057+ "aff",
1058+ "argonaut-codecs",
1059+ "argonaut-core",
1060+ "arrays",
1061+ "control",
1062+ "datetime",
1063+ "effect",
1064+ "either",
1065+ "foldable-traversable",
1066+ "identity",
1067+ "integers",
1068+ "maybe",
1069+ "newtype",
1070+ "node-buffer",
1071+ "node-fs",
1072+ "node-process",
1073+ "now",
1074+ "numbers",
1075+ "optparse",
1076+ "ordered-collections",
1077+ "partial",
1078+ "prelude",
1079+ "spec",
1080+ "strings",
1081+ "tuples"
1082+ ]
1083+ },
13511084 "st": {
13521085 "type": "registry",
13531086 "version": "6.2.0",
13541087 "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
13551088 "dependencies": [
1089+ "effect",
13561090 "partial",
13571091 "prelude",
13581092 "tailrec",
@@ -1484,22 +1218,24 @@
14841218 "version": "8.0.0",
14851219 "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
14861220 "dependencies": [
1221+ "control",
14871222 "enums",
1223+ "foldable-traversable",
14881224 "lists",
14891225 "maybe",
14901226 "partial",
14911227 "prelude",
1492− "record",
1493− "tuples",
1228+ "type-equality",
14941229 "unsafe-coerce"
14951230 ]
14961231 },
1497− "yoga-om-core": {
1232+ "yoga-om": {
14981233 "type": "registry",
1499− "version": "1.0.4",
1500− "integrity": "sha256-kgDxdG17MCIOOqHx8+ggwbkbix2qflhOi3kOx3QTxkA=",
1234+ "version": "2.0.0",
1235+ "integrity": "sha256-eenoujD2rJzR6gYJbbIiqLhXHC1C5dagu91HZkAEt/Y=",
15011236 "dependencies": [
15021237 "aff",
1238+ "aff-retry",
15031239 "avar",
15041240 "console",
15051241 "control",
@@ -1508,13 +1244,14 @@
15081244 "either",
15091245 "exceptions",
15101246 "functions",
1247+ "heterogeneous",
15111248 "maybe",
15121249 "newtype",
15131250 "parallel",
15141251 "prelude",
15151252 "record",
15161253 "record-studio",
1517− "spec",
1254+ "refs",
15181255 "tailrec",
15191256 "transformers",
15201257 "tuples",
spago.yaml+26−15
@@ -1,29 +1,40 @@
1−workspace:
2− packageSet:
3− registry: 72.0.1
4− extraPackages:
5− yoga-om-core: 1.0.4
6−
1+workspace: {}
72package:
83 name: yoga-om-layer
94 publish:
10− version: 1.0.4
5+ version: 2.1.0
116 license: MIT
127 location:
138 githubOwner: rowtype-yoga
149 githubRepo: purescript-yoga-om-layer
1510 dependencies:
16− - record-studio
17− - unsafe-coerce
18− - aff: ">=7.0.0 <8.0.0"
19− - newtype: ">=5.0.0 <6.0.0"
20− - prelude: ">=6.0.0 <7.0.0"
11+ - aff: ">=8.0.0 <9.0.0"
12+ - aff-retry: ">=2.0.0 <3.0.0"
13+ - arrays: ">=7.3.0 <8.0.0"
14+ - control: ">=6.0.0 <7.0.0"
15+ - effect: ">=4.0.0 <5.0.0"
16+ - exceptions: ">=6.1.0 <7.0.0"
17+ - foldable-traversable: ">=6.0.0 <7.0.0"
18+ - maybe: ">=6.0.0 <7.0.0"
19+ - nullable: ">=6.0.0 <7.0.0"
20+ - ordered-collections: ">=3.2.0 <4.0.0"
21+ - parallel: ">=7.0.0 <8.0.0"
22+ - prelude: ">=6.0.2 <7.0.0"
2123 - record: ">=4.0.0 <5.0.0"
22− - type-equality: ">=4.0.0 <5.0.0"
23− - yoga-om-core: ">=1.0.0 <2.0.0"
24+ - record-studio: ">=1.0.4 <2.0.0"
25+ - refs: ">=6.0.0 <7.0.0"
26+ - tailrec: ">=6.1.0 <7.0.0"
27+ - transformers: ">=6.1.0 <7.0.0"
28+ - type-equality: ">=4.0.1 <5.0.0"
29+ - typelevel-prelude: ">=7.0.0 <8.0.0"
30+ - unsafe-coerce: ">=6.0.0 <7.0.0"
31+ - variant: ">=8.0.0 <9.0.0"
32+ - yoga-om: ">=2.0.0 <3.0.0"
2433 test:
2534 main: Test.Main
2635 dependencies:
27− - spec: ">=7.0.0 <8.0.0"
36+ - spec: ">=7.0.0 <9.0.0"
37+ - spec-discovery: ">=8.0.0 <9.0.0"
38+ - spec-node: ">=0.0.3 <1.0.0"
2839 - console: ">=6.0.0 <7.0.0"
2940 - refs: ">=6.0.0 <7.0.0"
src/Yoga/Om/Layer.jsnew file+4
@@ -0,0 +1,4 @@
1+let counter = 0;
2+export const nextId = () => ++counter;
3+
4+export const tryGetScope = (ctx) => ctx.scope || null;
src/Yoga/Om/Layer.purs+921−130
@@ -1,206 +1,997 @@
1−-- @inline export makeLayer arity=1
21-- @inline export runLayer arity=2
3−module Yoga.Om.Layer
4− ( OmLayer(..)
2+module Yoga.Om.Layer
3+ ( OmLayer
4+ , ParOmLayer
5+ , Scope
56 , makeLayer
7+ , makeScopedLayer
8+ , bracketLayer
9+ , acquireRelease
10+ , fresh
611 , runLayer
12+ , runScoped
13+ , runScopedWith
14+ , withScoped
15+ , withScopedWith
16+ , scoped
717 , combineRequirements
18+ , withValue
819 , provide
920 , (>->)
21+ , recovering
22+ , repeating
23+ , wireLayers
24+ , wireLayersDebug
25+ , class FilterScope
26+ , class FindProvider
27+ , class FindProviderMatch
28+ , class HasLabel
29+ , class EmitEdges
30+ , class DashesFor
31+ , class BoxTop, class BoxMid, class BoxBot
32+ , class RenderDepChildNode
33+ , class IsNotProvided
34+ , class HasDirectMissing, class HasDirectMissingOr
35+ , class FindLayerReqs
36+ , class RenderProviderDeps
37+ , class RenderDepTree
38+ , class FirstProvLabel
39+ , class PrintLayerDispatch
40+ , class PrintLayersRL
41+ , class PrintEdgesRL
42+ , class PrintLayersASCII
43+ , class PrintEdgesASCII
44+ , NotProvided
1045 , class CheckAllProvided
1146 , class CheckAllLabelsExist
1247 , class CheckLabelExists
48+ , class TopoWire, topoWire
49+ , class AllLabelsIn
50+ , class FindReady
51+ , class FindReadyDispatch
1352 ) where
1453
1554import Prelude
1655
17−import Data.Newtype (class Newtype)
18−import Effect.Aff.Class (liftAff)
56+import Control.Alt (class Alt)
57+import Control.Alt as Alt
58+import Control.Alternative (class Alternative)
59+import Control.Monad.Error.Class (class MonadError, class MonadThrow, catchError, throwError)
60+import Control.Monad.Reader (class MonadAsk, class MonadReader)
61+import Control.Monad.Reader as Reader
62+import Control.Monad.Rec.Class (class MonadRec, Step(..), tailRecM)
63+import Control.Parallel (class Parallel)
64+import Control.Parallel as Parallel
65+import Control.Plus (class Plus)
66+import Control.Plus as Plus
67+import Data.Symbol (class IsSymbol)
68+import Data.Variant (Variant)
69+import Data.Array as Array
70+import Data.Foldable (for_)
71+import Data.Map (Map)
72+import Data.Map as Map
73+import Data.Maybe (Maybe(..), maybe)
74+import Data.Nullable (Nullable, toMaybe)
75+import Data.Variant (class VariantMatchCases, onMatch)
76+import Effect (Effect)
77+import Effect.Aff (Aff, bracket)
78+import Effect.Aff.Retry (RetryPolicyM, RetryStatus, applyAndDelay, defaultRetryStatus)
79+import Effect.Aff.Class (class MonadAff)
80+import Effect.Aff.Class as Aff
81+import Effect.Class (class MonadEffect, liftEffect)
82+import Effect.Class (class MonadEffect, liftEffect) as Effect
83+import Effect.Exception (Error)
84+import Effect.Ref (Ref)
85+import Effect.Ref as Ref
86+import Effect.Unsafe (unsafePerformEffect)
1987import Prim.Row (class Nub, class Union)
88+import Prim.Row as Row
89+import Type.Proxy (Proxy(..))
2090import Prim.RowList (class RowToList, Cons, Nil, RowList)
21−import Prim.TypeError (class Fail, Above, Quote, Text)
91+import Prim.Boolean (True, False)
92+import Prim.Symbol (class Append, class Cons)
93+import Type.Data.Boolean (class And)
94+import Type.RowList (class RowListAppend)
95+import Prim.TypeError (class Fail, class Warn, Above, Beside, Doc, Quote, Text)
2296import Record as Record
2397import Record.Studio (class Keys)
2498import Type.Equality (class TypeEquals)
2599import Unsafe.Coerce (unsafeCoerce)
26−import Yoga.Om (Om)
100+import Yoga.Om (Om, ParOm)
27101import Yoga.Om as Om
102+import Yoga.Om.Error (Exception)
28103
29−-- | A layer that requires dependencies (req) and provides services (prov)
30−newtype OmLayer req prov err = OmLayer (Om (Record req) err (Record prov))
104+-- =============================================================================
105+-- Scope — a first-class representation of resource lifetime
106+-- =============================================================================
107+
108+-- | A first-class representation of resource lifetime and memoization.
109+-- | Finalizers are registered via `acquireRelease` and run in reverse order
110+-- | when the scope is closed. Layers are automatically memoized by identity
111+-- | within a scope — the same layer value builds only once.
112+newtype Scope = Scope
113+ { finalizers :: Ref (Array (Aff Unit))
114+ , cache :: Ref (Map Int Dynamic)
115+ }
116+
117+-- | Opaque box for cached values, retrieved via `unsafeCoerce`.
118+foreign import data Dynamic :: Type
119+
120+toDynamic :: forall a. a -> Dynamic
121+toDynamic = unsafeCoerce
122+
123+fromDynamic :: forall a. Dynamic -> a
124+fromDynamic = unsafeCoerce
125+
126+-- | Create a fresh, empty scope.
127+newScope :: Effect Scope
128+newScope = do
129+ finalizers <- Ref.new []
130+ cache <- Ref.new Map.empty
131+ pure (Scope { finalizers, cache })
132+
133+-- | Register a finalizer to run when this scope closes.
134+addFinalizer :: Scope -> Aff Unit -> Effect Unit
135+addFinalizer (Scope s) fin = Ref.modify_ (_ <> [ fin ]) s.finalizers
136+
137+-- | Close the scope, running all registered finalizers in reverse order.
138+closeScope :: Scope -> Aff Unit
139+closeScope (Scope s) = do
140+ fins <- liftEffect $ Ref.read s.finalizers
141+ for_ (Array.reverse fins) identity
142+
143+-- =============================================================================
144+-- Unique layer IDs
145+-- =============================================================================
146+
147+foreign import nextId :: Effect Int
148+foreign import tryGetScope :: forall r. Record r -> Nullable Scope
149+
150+-- | Generate a unique layer ID. Uses unsafePerformEffect since layer
151+-- | construction is at module init time (like Scala's `object` identity).
152+-- | This is a function (Unit -> Int) to ensure each call site gets a unique ID.
153+freshId :: Unit -> Int
154+freshId _ = unsafePerformEffect nextId
155+
156+-- =============================================================================
157+-- OmLayer
158+-- =============================================================================
159+
160+-- | A layer that requires dependencies (req), may fail with (err),
161+-- | and produces a value (typically Record prov).
162+-- | Each layer has a unique identity used for automatic memoization within a Scope.
163+data OmLayer req err a = OmLayer Int (Om (Record req) err a)
164+
165+-- =============================================================================
166+-- ParOmLayer — parallel counterpart
167+-- =============================================================================
168+
169+data ParOmLayer req err a = ParOmLayer (ParOm (Record req) err a)
170+
171+-- =============================================================================
172+-- Instances
173+-- =============================================================================
174+
175+instance Functor (OmLayer req err) where
176+ map f layer = OmLayer (freshId unit) (map f (buildLayer layer))
177+
178+instance Apply (OmLayer req err) where
179+ apply f a = OmLayer (freshId unit) (apply (buildLayer f) (buildLayer a))
180+
181+instance Applicative (OmLayer req err) where
182+ pure a = OmLayer (freshId unit) (pure a)
183+
184+instance Bind (OmLayer req err) where
185+ bind layer f = OmLayer (freshId unit) do
186+ a <- buildLayer layer
187+ buildLayer (f a)
188+
189+instance Monad (OmLayer req err)
190+
191+instance MonadEffect (OmLayer req err) where
192+ liftEffect eff = OmLayer (freshId unit) (Effect.liftEffect eff)
193+
194+instance MonadAff (OmLayer req err) where
195+ liftAff aff = OmLayer (freshId unit) (Aff.liftAff aff)
196+
197+instance MonadAsk (Record req) (OmLayer req err) where
198+ ask = OmLayer (freshId unit) Reader.ask
199+
200+instance MonadReader (Record req) (OmLayer req err) where
201+ local f layer = OmLayer (freshId unit) (Reader.local f (buildLayer layer))
202+
203+instance MonadThrow (Variant (Exception err)) (OmLayer req err) where
204+ throwError e = OmLayer (freshId unit) (throwError e)
205+
206+instance MonadError (Variant (Exception err)) (OmLayer req err) where
207+ catchError layer handler = OmLayer (freshId unit) (catchError (buildLayer layer) (\e -> buildLayer (handler e)))
31208
32−derive instance Newtype (OmLayer req prov err) _
209+instance MonadRec (OmLayer req err) where
210+ tailRecM f a = OmLayer (freshId unit) (tailRecM (\x -> buildLayer (f x)) a)
33211
34−-- | Helper to create a layer from an Om computation
35−makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
36−makeLayer = OmLayer
212+instance Semigroup a => Semigroup (OmLayer req err a) where
213+ append a b = OmLayer (freshId unit) (append (buildLayer a) (buildLayer b))
214+
215+instance Alt (OmLayer req err) where
216+ alt a b = OmLayer (freshId unit) (Alt.alt (buildLayer a) (buildLayer b))
217+
218+instance Plus (OmLayer req err) where
219+ empty = OmLayer (freshId unit) Plus.empty
220+
221+instance Functor (ParOmLayer req err) where
222+ map f (ParOmLayer p) = ParOmLayer (map f p)
223+
224+instance Apply (ParOmLayer req err) where
225+ apply (ParOmLayer f) (ParOmLayer a) = ParOmLayer (apply f a)
226+
227+instance Applicative (ParOmLayer req err) where
228+ pure a = ParOmLayer (pure a)
229+
230+instance Alt (ParOmLayer req err) where
231+ alt (ParOmLayer a) (ParOmLayer b) = ParOmLayer (Alt.alt a b)
232+
233+instance Plus (ParOmLayer req err) where
234+ empty = ParOmLayer Plus.empty
235+
236+instance Alternative (ParOmLayer req err)
237+
238+instance Parallel (ParOmLayer req err) (OmLayer req err) where
239+ parallel (OmLayer _ om) = ParOmLayer (Parallel.parallel om)
240+ sequential (ParOmLayer p) = OmLayer (freshId unit) (Parallel.sequential p)
241+
242+-- =============================================================================
243+-- Automatic memoization
244+-- =============================================================================
245+
246+-- | Wrap an Om computation with cache logic.
247+-- | If a scope is available in the context, checks/populates the cache by layer ID.
248+-- | If no scope is present, runs the computation directly.
249+withCache :: forall req err a. Int -> Om (Record req) err a -> Om (Record req) err a
250+withCache layerId om = do
251+ ctx <- Om.ask
252+ case toMaybe (tryGetScope ctx) of
253+ Nothing -> om
254+ Just (Scope s) -> do
255+ cached <- liftEffect $ Map.lookup layerId <$> Ref.read s.cache
256+ case cached of
257+ Just hit -> pure (fromDynamic hit)
258+ Nothing -> do
259+ prov <- om
260+ liftEffect $ Ref.modify_ (Map.insert layerId (toDynamic prov)) s.cache
261+ pure prov
262+
263+-- =============================================================================
264+-- Layer constructors
265+-- =============================================================================
266+
267+-- | Create a layer from an Om computation.
268+-- | The layer is automatically memoized within a Scope — the same layer value
269+-- | used in multiple branches of a dependency graph builds only once.
270+makeLayer :: forall req err a. Om (Record req) err a -> OmLayer req err a
271+makeLayer om = OmLayer (freshId unit) om
272+
273+-- | Extract the Om from a layer, wrapped with cache logic.
274+buildLayer :: forall req err a. OmLayer req err a -> Om (Record req) err a
275+buildLayer (OmLayer layerId om) = withCache layerId om
276+
277+-- | Create a scoped layer with acquire/release semantics.
278+-- | The release function runs when the enclosing scope closes.
279+-- | The layer requires `scope :: Scope` in its context.
280+makeScopedLayer
281+ :: forall req prov err
282+ . Om { scope :: Scope | req } err (Record prov)
283+ -> (Record prov -> Aff Unit)
284+ -> OmLayer (scope :: Scope | req) err (Record prov)
285+makeScopedLayer acquire release = makeLayer do
286+ acquireRelease acquire release
287+
288+-- | Bracket-style scoped layer: acquire a resource, register its release,
289+-- | then build provisions from it.
290+bracketLayer
291+ :: forall req prov resource err
292+ . Om { scope :: Scope | req } err resource
293+ -> (resource -> Aff Unit)
294+ -> (resource -> Om { scope :: Scope | req } err (Record prov))
295+ -> OmLayer (scope :: Scope | req) err (Record prov)
296+bracketLayer acquire release use = makeLayer do
297+ resource <- acquireRelease acquire (\r -> release r)
298+ use resource
299+
300+-- | Acquire a resource and register its release with the current scope.
301+-- | This is the core scoped resource primitive — the PureScript equivalent
302+-- | of ZIO's `ZIO.acquireRelease`.
303+acquireRelease
304+ :: forall ctx err a
305+ . Om { scope :: Scope | ctx } err a
306+ -> (a -> Aff Unit)
307+ -> Om { scope :: Scope | ctx } err a
308+acquireRelease acquire release = do
309+ a <- acquire
310+ { scope } <- Om.ask
311+ addFinalizer scope (release a) # liftEffect
312+ pure a
313+
314+-- | Create a fresh (non-memoized) copy of a layer.
315+-- | The new layer has a unique identity and will always build independently,
316+-- | even within the same scope.
317+fresh :: forall req err a. OmLayer req err a -> OmLayer req err a
318+fresh (OmLayer _ om) = OmLayer (freshId unit) om
37319
38320-- =============================================================================
39321-- Custom Type Errors for Missing Dependencies
40322-- =============================================================================
41323
42−-- | Check if all required dependencies are provided
43−-- If not, show a helpful diff of required vs available
324+-- | Check if all required dependencies are provided.
325+-- If not, show a helpful diff of required vs available.
44326class CheckAllProvided (required :: Row Type) (available :: Row Type)
45327
46−instance checkAllProvidedImpl ::
328+instance
47329 ( RowToList required reqList
48330 , RowToList available availList
49331 , CheckAllLabelsExist reqList availList required available
50332 ) =>
51333 CheckAllProvided required available
52334
53−-- | Check that all labels in required RowList exist in available RowList
54−-- Pass along the original rows for better error messages
335+-- | Check that all labels in required RowList exist in available RowList.
336+-- Pass along the original rows for better error messages.
55337class CheckAllLabelsExist (required :: RowList Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
56338
57−-- Base case: no more requirements to check
58−instance checkAllLabelsNil :: CheckAllLabelsExist Nil available requiredRow availableRow
339+instance CheckAllLabelsExist Nil available requiredRow availableRow
59340
60−-- Recursive case: check if this label exists, then check the rest
61−instance checkAllLabelsCons ::
341+instance
62342 ( CheckLabelExists label ty available requiredRow availableRow
63343 , CheckAllLabelsExist tail available requiredRow availableRow
64344 ) =>
65345 CheckAllLabelsExist (Cons label ty tail) available requiredRow availableRow
66346
67−-- | Check if a single label exists in the available RowList
68−-- This is where the magic happens - we dispatch on RowList structure in the instance head!
347+-- | Check if a single label exists in the available RowList.
69348class CheckLabelExists (label :: Symbol) (ty :: Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
70349
71−-- Found it! The label matches in the instance head
72−-- TypeEquals ensures the types at this label actually match!
73−instance checkLabelExistsFound ::
350+instance
74351 TypeEquals ty ty' =>
75352 CheckLabelExists label ty (Cons label ty' tail) requiredRow availableRow
76353
77−-- Keep looking: different label, recurse on tail
78−else instance checkLabelExistsKeepLooking ::
354+else instance
79355 CheckLabelExists label ty tail requiredRow availableRow =>
80356 CheckLabelExists label ty (Cons otherLabel otherTy tail) requiredRow availableRow
81357
82−-- Not found: reached end of list, emit custom error WITH FULL CONTEXT!
83−else instance checkLabelExistsNotFound ::
358+else instance
84359 Fail
85360 ( Above
86361 (Text "Missing required dependency!")
87− (Above
88− (Text "")
89− (Above
90− (Text "The first missing field is: ")
91− (Above
92− (Quote label)
93− (Above
94− (Text "")
95− (Above
96− (Text "The layer requires: ")
97− (Above
98− (Quote requiredRow)
99− (Above
100− (Text "")
101− (Above
102− (Text "But you provided: ")
103− (Quote availableRow)
104− )
362+ ( Above
363+ (Text "")
364+ ( Above
365+ (Text "The first missing field is: ")
366+ ( Above
367+ (Quote label)
368+ ( Above
369+ (Text "")
370+ ( Above
371+ (Text "The layer requires: ")
372+ ( Above
373+ (Quote requiredRow)
374+ ( Above
375+ (Text "")
376+ ( Above
377+ (Text "But you provided: ")
378+ (Quote availableRow)
379+ )
380+ )
381+ )
382+ )
105383 )
106− )
107384 )
108− )
109385 )
110− )
111386 )
112387 ) =>
113388 CheckLabelExists label ty Nil requiredRow availableRow
114389
115−-- | Run a layer with a given context, with custom error if requirements aren't met
390+-- =============================================================================
391+-- Running layers
392+-- =============================================================================
393+
394+-- | Run a layer with a given context, with custom error if requirements aren't met.
395+-- | `runLayer` requires a closed requirement row. If you have an open layer like
396+-- | `OmLayer (db :: Db | r) err a`, specialize it to a closed row before running
397+-- | it directly.
116398runLayer
117− :: forall req prov err available
118− . CheckAllProvided req available -- ← Custom error if dependencies missing!
399+ :: forall req err a available
400+ . CheckAllProvided req available
401+ => Keys req
119402 => Record available
120− -> OmLayer req prov err
121− -> Om (Record available) err (Record prov)
122−runLayer _ctx layer = unsafeCastLayer layer
403+ -> OmLayer req err a
404+ -> Om (Record available) err a
405+runLayer _ctx layer = widenCtx (buildLayer layer)
123406 where
124− -- After CheckAllProvided succeeds, we know req ⊆ available at compile time
125− -- So this cast is safe - we're just telling the type system what it already proved
126− unsafeCastLayer :: OmLayer req prov err -> Om (Record available) err (Record prov)
127− unsafeCastLayer = unsafeCoerce
128−
129−-- | The key function: combine two layers with automatic deduplication
130−-- Given two layers with potentially overlapping requirements,
131−-- produce a layer with deduplicated requirements
132−--
133−-- Uses expand to properly widen contexts and errors, then runs both layers!
407+ widenCtx :: Om (Record req) err a -> Om (Record available) err a
408+ widenCtx = unsafeCoerce
409+
410+-- | Build a fully-provided scoped layer, return the provisions.
411+-- | All finalizers run after the provisions are returned.
412+-- | For layers with typed errors, use `runScopedWith`.
413+runScoped
414+ :: forall prov
415+ . OmLayer (scope :: Scope) () (Record prov)
416+ -> Aff (Record prov)
417+runScoped layer = withScoped layer pure
418+
419+-- | Build a fully-provided scoped layer and pass the provisions to a callback.
420+-- | A fresh `Scope` is created and closed when the callback completes,
421+-- | running all finalizers in reverse order — whether by success, failure,
422+-- | or interruption.
423+-- | For layers with typed errors, use `withScopedWith`.
424+withScoped
425+ :: forall prov a
426+ . OmLayer (scope :: Scope) () (Record prov)
427+ -> (Record prov -> Aff a)
428+ -> Aff a
429+withScoped = withScopedWith { exception: throwError }
430+
431+-- | Like `runScoped` but accepts error handlers for layers with typed errors.
432+runScopedWith
433+ :: forall prov r rl err_ err
434+ . RowToList (exception :: Error -> Aff (Record prov) | r) rl
435+ => VariantMatchCases rl err_ (Aff (Record prov))
436+ => Union err_ () (exception :: Error | err)
437+ => { exception :: Error -> Aff (Record prov) | r }
438+ -> OmLayer (scope :: Scope) err (Record prov)
439+ -> Aff (Record prov)
440+runScopedWith handlers layer = withScopedWith handlers layer pure
441+
442+-- | Like `withScoped` but accepts error handlers for layers with typed errors.
443+withScopedWith
444+ :: forall prov a r rl err_ err
445+ . RowToList (exception :: Error -> Aff (Record prov) | r) rl
446+ => VariantMatchCases rl err_ (Aff (Record prov))
447+ => Union err_ () (exception :: Error | err)
448+ => { exception :: Error -> Aff (Record prov) | r }
449+ -> OmLayer (scope :: Scope) err (Record prov)
450+ -> (Record prov -> Aff a)
451+ -> Aff a
452+withScopedWith handlers layer callback = bracket acquire release use
453+ where
454+ acquire = do
455+ scope <- newScope # liftEffect
456+ let om = buildLayer layer
457+ prov <- Om.runOm { scope } handlers om
458+ pure { scope, prov }
459+ release { scope } = closeScope scope
460+ use { prov } = callback prov
461+
462+scoped
463+ :: forall prov a r rl err_ err
464+ . RowToList (exception :: Error -> Aff (Record prov) | r) rl
465+ => VariantMatchCases rl err_ (Aff (Record prov))
466+ => Union err_ () (exception :: Error | err)
467+ => { exception :: Error -> Aff (Record prov) | r }
468+ -> OmLayer (scope :: Scope) err (Record prov)
469+ -> (Record prov -> Aff a)
470+ -> Aff a
471+scoped = withScopedWith
472+
473+-- =============================================================================
474+-- Horizontal composition — combine two layers with automatic deduplication
475+-- =============================================================================
476+
134477combineRequirements
135− :: forall req1 req2 prov1 prov2 err1 err2 provMerged provMergedRaw reqMerged reqDeduped _req1 _req2 _err1 _err2
478+ :: forall req1 req2 prov1 prov2 err1 err2 provMerged reqMerged reqDeduped errMerged errDeduped _req1 _req2 _err1 _err2
136479 . Union req1 req2 reqMerged
137− => Nub reqMerged reqDeduped -- ← Automatic deduplication of requirements!
138− => Union req1 _req1 reqDeduped -- req1 is subset of reqDeduped
139− => Union req2 _req2 reqDeduped -- req2 is subset of reqDeduped
140− => Union err1 _err1 () -- Widen errors to empty
141− => Union err2 _err2 ()
142− => Union prov1 prov2 provMergedRaw -- Merge provisions
143− => Nub provMergedRaw provMerged -- Deduplicate provisions too!
144− => Keys req1 -- Needed for expand
145− => Keys req2 -- Needed for expand
146− => OmLayer req1 prov1 err1
147− -> OmLayer req2 prov2 err2
148− -> OmLayer reqDeduped provMerged ()
149−combineRequirements (OmLayer build1) (OmLayer build2) = makeLayer do
150− -- Widen both contexts and errors to allow them to run in the deduplicated context
151− -- expand allows an Om computation to run in a larger context with larger error row
152− rec1 <- Om.expand build1 -- Om req1 err1 -> Om reqDeduped ()
153− rec2 <- Om.expand build2 -- Om req2 err2 -> Om reqDeduped ()
154− -- Merge the results (Record.merge handles the Nub)
480+ => Nub reqMerged reqDeduped
481+ => Union req1 _req1 reqDeduped
482+ => Union req2 _req2 reqDeduped
483+ => Union err1 err2 errMerged
484+ => Nub errMerged errDeduped
485+ => Union err1 _err1 errDeduped
486+ => Union err2 _err2 errDeduped
487+ => Union prov1 prov2 provMerged
488+ => Nub provMerged provMerged
489+ => Keys req1
490+ => Keys req2
491+ => OmLayer req1 err1 (Record prov1)
492+ -> OmLayer req2 err2 (Record prov2)
493+ -> OmLayer reqDeduped errDeduped (Record provMerged)
494+combineRequirements layer1 layer2 = OmLayer (freshId unit) do
495+ rec1 <- Om.expand (buildLayer layer1)
496+ rec2 <- Om.expand (buildLayer layer2)
155497 pure (Record.merge rec1 rec2)
156498
157−-- Example proof that this works:
158−-- If we have:
159−-- layer1 :: OmLayer (config :: Config) (logger :: Logger) ()
160−-- layer2 :: OmLayer (config :: Config) (database :: Database) ()
161−--
162−-- Then: combineRequirements layer1 layer2
163−-- Has type: OmLayer (config :: Config) prov3 err3
164−--
165−-- NOT: OmLayer (config :: Config, config :: Config) prov3 err3
166−--
167−-- The duplicated requirement (config, config) is automatically
168−-- deduplicated to just (config) by the Nub constraint!
169−
170−-- =============================================================================
171−-- Vertical Composition (ZLayer-style provide)
172−-- =============================================================================
173−
174−-- | Vertical composition: feed output of one layer into input of another
175−-- | This is the >>> operator from ZIO ZLayer
176−-- |
177−-- | Example:
178−-- | postgresLive :: OmLayer (config :: Config) (postgres :: SQL) ()
179−-- | userRepoLive :: OmLayer (postgres :: SQL) (userRepo :: UserRepo) ()
180−-- |
181−-- | composed = userRepoLive `provide` postgresLive
182−-- | -- Result: OmLayer (config :: Config) (userRepo :: UserRepo) ()
499+withValue
500+ :: forall req err prov1 prov2 provMerged
501+ . Union prov1 prov2 provMerged
502+ => Nub provMerged provMerged
503+ => Record prov1
504+ -> OmLayer req err (Record prov2)
505+ -> OmLayer req err (Record provMerged)
506+withValue defaults layer = OmLayer (freshId unit) do
507+ rec <- buildLayer layer
508+ pure (Record.merge defaults rec)
509+
510+-- =============================================================================
511+-- Vertical composition — feed output of one layer into input of another
512+-- =============================================================================
513+
183514provide
184− :: forall req prov1 prov2 err1 err2 _req _prov1 _err1 _err2
185− . Union req _req req -- expand constraints
515+ :: forall req prov1 prov2 err1 err2 req2 reqOut errMerged errDeduped _req _prov1 _err1 _err2
516+ . Union req _req req
186517 => Union prov1 _prov1 prov1
187− => Union err1 _err1 ()
188− => Union err2 _err2 ()
518+ => Union err1 err2 errMerged
519+ => Nub errMerged errDeduped
520+ => Union err1 _err1 errDeduped
521+ => Union err2 _err2 errDeduped
522+ => Union prov1 req reqOut
523+ => Nub reqOut reqOut
524+ => Union req2 _prov1 reqOut
189525 => Keys req
190526 => Keys prov1
191− => OmLayer prov1 prov2 err2 -- layer that needs prov1, provides prov2
192− -> OmLayer req prov1 err1 -- layer that needs req, provides prov1
193− -> OmLayer req prov2 () -- composed: needs req, provides prov2
194−provide (OmLayer layer2) (OmLayer layer1) = makeLayer do
195− -- Run first layer to get prov1
196− prov1 <- Om.expand layer1
197−
198− -- Run second layer with prov1 as context
199− liftAff $ Om.runOm prov1
200− { exception: \_ -> pure (unsafeCoerce {}) }
201− (Om.expand layer2)
527+ => Keys req2
528+ => OmLayer req2 err2 (Record prov2)
529+ -> OmLayer req err1 (Record prov1)
530+ -> OmLayer req errDeduped (Record prov2)
531+provide l2 l1 = OmLayer (freshId unit) do
532+ prov1 <- Om.expand (buildLayer l1)
533+ Om.expand (buildLayer l2) # Om.widenCtx prov1
202534
203535infixl 9 provide as >->
204536
205−-- TODO: More sophisticated composition operators
206−-- For now, use >>> for simple chains and combineRequirements for parallel deps
537+-- =============================================================================
538+-- Retry / Repeat on layers
539+-- =============================================================================
540+
541+recovering
542+ :: forall req err rest a
543+ (handlersRL :: RowList Type)
544+ (handlers :: Row Type)
545+ (handled :: Row Type)
546+ . RowToList handlers handlersRL
547+ => VariantMatchCases handlersRL handled (Om (Record req) err Boolean)
548+ => Union handled rest (exception :: Error | err)
549+ => RetryPolicyM (Om (Record req) err)
550+ -> (RetryStatus -> Record handlers)
551+ -> OmLayer req err a
552+ -> OmLayer req err a
553+recovering policy mkChecks (OmLayer layerId om) =
554+ OmLayer layerId (go defaultRetryStatus)
555+ where
556+ go status =
557+ catchError om (handleErr status)
558+
559+ handleErr status variant = do
560+ let shouldRetry = variant # onMatch (mkChecks status) (\_ -> pure false)
561+ ifM shouldRetry
562+ (applyAndDelay policy status >>= maybe (throwError variant) go)
563+ (throwError variant)
564+
565+repeating
566+ :: forall req err a
567+ . RetryPolicyM (Om (Record req) err)
568+ -> (RetryStatus -> a -> Om (Record req) err Boolean)
569+ -> OmLayer req err a
570+ -> OmLayer req err a
571+repeating policy shouldRepeat (OmLayer layerId om) =
572+ OmLayer layerId (go defaultRetryStatus)
573+ where
574+ go status = do
575+ result <- om
576+ ifM (shouldRepeat status result)
577+ (applyAndDelay policy status >>= maybe (pure result) go)
578+ (pure result)
579+
580+
581+-- =============================================================================
582+-- Topological auto-wiring (order-independent)
583+-- =============================================================================
584+
585+-- | Check if ALL labels in `needles` exist in `haystack`, returning a Boolean.
586+class AllLabelsIn (needles :: RowList Type) (haystack :: RowList Type) (result :: Boolean)
587+ | needles haystack -> result
588+
589+instance AllLabelsIn Nil haystack True
590+
591+instance
592+ ( HasLabel label haystack found
593+ , AllLabelsIn tail haystack tailResult
594+ , And found tailResult result
595+ ) =>
596+ AllLabelsIn (Cons label ty tail) haystack result
597+
598+-- | Dispatch based on whether a layer is ready (deps satisfied).
599+class FindReadyDispatch
600+ (ready :: Boolean)
601+ (sym :: Symbol)
602+ (layer :: Type)
603+ (tail :: RowList Type)
604+ (available :: RowList Type)
605+ (allLayersRL :: RowList Type)
606+ (foundSym :: Symbol)
607+ (foundLayer :: Type)
608+ (rest :: RowList Type)
609+ | ready sym layer tail available allLayersRL -> foundSym foundLayer rest
610+
611+instance FindReadyDispatch True sym layer tail available allLayersRL sym layer tail
612+
613+else instance
614+ FindReady tail available allLayersRL foundSym foundLayer rest' =>
615+ FindReadyDispatch False sym layer tail available allLayersRL foundSym foundLayer (Cons sym layer rest')
616+
617+-- | Scan candidates for the first layer whose requirements are satisfied.
618+class FindReady
619+ (candidates :: RowList Type)
620+ (available :: RowList Type)
621+ (allLayersRL :: RowList Type)
622+ (sym :: Symbol)
623+ (layer :: Type)
624+ (rest :: RowList Type)
625+ | candidates available allLayersRL -> sym layer rest
626+
627+instance
628+ ( PrintLayersASCII allLayersRL graphDoc
629+ , Fail
630+ ( Above (Text "")
631+ ( Above (Text "Dependency not provided!")
632+ ( Above (Text "")
633+ ( Above (Text "No remaining layer has all its dependencies satisfied.")
634+ ( Above (Text "")
635+ ( Above (Text "Dependency graph:") graphDoc
636+ )
637+ )
638+ )
639+ )
640+ )
641+ )
642+ ) =>
643+ FindReady Nil available allLayersRL sym layer rest
644+
645+else instance
646+ ( RowToList req reqRL
647+ , AllLabelsIn reqRL available isReady
648+ , FindReadyDispatch isReady sym (OmLayer req err (Record prov)) tail available allLayersRL foundSym foundLayer rest
649+ ) =>
650+ FindReady (Cons sym (OmLayer req err (Record prov)) tail) available allLayersRL foundSym foundLayer rest
651+
652+-- | Topologically sort and wire layers. Each iteration finds a layer whose
653+-- | dependencies are already satisfied, processes it, and adds its provisions.
654+class TopoWire
655+ (remaining :: RowList Type)
656+ (layers :: Row Type)
657+ (accReq :: Row Type)
658+ (accErr :: Row Type)
659+ (accProv :: Row Type)
660+ (resReq :: Row Type)
661+ (resErr :: Row Type)
662+ (resProv :: Row Type)
663+ | remaining layers accReq accErr accProv -> resReq resErr resProv
664+ where
665+ topoWire
666+ :: Proxy remaining
667+ -> Record layers
668+ -> OmLayer accReq accErr (Record accProv)
669+ -> OmLayer resReq resErr (Record resProv)
670+
671+instance TopoWire Nil layers accReq accErr accProv accReq accErr accProv where
672+ topoWire _ _ acc = acc
673+
674+else instance
675+ ( RowToList layers allLayersRL
676+ , RowToList accProv accProvRL
677+ , RowToList accReq accReqRL
678+ , RowListAppend accProvRL accReqRL availableRL
679+ , FindReady remaining availableRL allLayersRL readySym (OmLayer nextReq nextErr (Record nextProv)) rest
680+ , IsSymbol readySym
681+ , Row.Cons readySym (OmLayer nextReq nextErr (Record nextProv)) _rest layers
682+ -- next's requirements must be subset of (accProv + accReq)
683+ , Union accProv accReq provReqOut
684+ , Nub provReqOut provReqOut
685+ , Union nextReq _nextReqRest provReqOut
686+ , Keys nextReq
687+ , Keys accReq
688+ , Keys accProv
689+ -- merge errors
690+ , Union accErr nextErr errMergedRaw
691+ , Nub errMergedRaw errMerged
692+ , Union accErr _e1 errMerged
693+ , Union nextErr _e2 errMerged
694+ -- merge provisions
695+ , Union accProv nextProv provMerged
696+ , Nub provMerged provMerged
697+ -- recurse
698+ , TopoWire rest layers accReq errMerged provMerged resReq resErr resProv
699+ ) =>
700+ TopoWire remaining layers accReq accErr accProv resReq resErr resProv where
701+ topoWire _ layers acc =
702+ topoWire (Proxy :: Proxy rest) layers combined
703+ where
704+ next :: OmLayer nextReq nextErr (Record nextProv)
705+ next = Record.get (Proxy :: Proxy readySym) layers
706+
707+ combined :: OmLayer accReq errMerged (Record provMerged)
708+ combined = OmLayer (freshId unit) do
709+ accProv <- Om.expandErr (buildLayer acc)
710+ nextProv <- Om.expand (buildLayer next) # Om.widenCtx accProv
711+ pure (Record.merge accProv nextProv)
712+
713+wireLayers
714+ :: forall layers rl req err prov
715+ . RowToList layers rl
716+ => TopoWire rl layers (scope :: Scope) () () req err prov
717+ => Record layers
718+ -> OmLayer req err (Record prov)
719+wireLayers layers =
720+ topoWire (Proxy :: Proxy rl) layers seed
721+ where
722+ seed = pure {} :: OmLayer (scope :: Scope) () (Record ())
723+
724+-- =============================================================================
725+-- Debug graph printing (D2 diagram)
726+-- =============================================================================
727+
728+-- | Remove `scope :: Scope` from a RowList (it's always present, just noise).
729+class FilterScope (rl :: RowList Type) (out :: RowList Type) | rl -> out
730+
731+instance FilterScope Nil Nil
732+instance FilterScope (Cons "scope" Scope tail) tail
733+else instance FilterScope tail out => FilterScope (Cons sym ty tail) (Cons sym ty out)
734+
735+-- | Marker symbol for unresolved dependencies.
736+type NotProvided :: Symbol
737+type NotProvided = "???"
738+
739+-- | Given a label, find which layer in the RowList provides it.
740+class FindProvider (label :: Symbol) (rl :: RowList Type) (provider :: Symbol) | label rl -> provider
741+
742+instance FindProvider label Nil NotProvided
743+
744+else instance
745+ ( RowToList prov provRL
746+ , HasLabel label provRL hasIt
747+ , FindProviderMatch hasIt label sym (Cons sym (OmLayer req err (Record prov)) tail) provider
748+ ) =>
749+ FindProvider label (Cons sym (OmLayer req err (Record prov)) tail) provider
750+
751+-- | Dispatch based on whether the current layer has the label.
752+class FindProviderMatch (hasIt :: Boolean) (label :: Symbol) (sym :: Symbol) (rl :: RowList Type) (provider :: Symbol) | hasIt label sym rl -> provider
753+
754+instance FindProviderMatch True label sym rl sym
755+
756+else instance
757+ FindProvider label tail provider =>
758+ FindProviderMatch False label sym (Cons sym ty tail) provider
759+
760+-- | Check if a RowList contains a given label.
761+class HasLabel (label :: Symbol) (rl :: RowList Type) (result :: Boolean) | label rl -> result
762+
763+instance HasLabel label Nil False
764+instance HasLabel label (Cons label ty tail) True
765+else instance HasLabel label tail result => HasLabel label (Cons sym ty tail) result
766+
767+-- | Emit D2 edges for one consumer: "provider -> consumer" per requirement.
768+class EmitEdges (consumer :: Symbol) (reqRL :: RowList Type) (allLayers :: RowList Type) (doc :: Doc) | consumer reqRL allLayers -> doc
769+
770+instance EmitEdges consumer Nil allLayers (Text "")
771+
772+else instance
773+ ( FindProvider label allLayers provider
774+ , Append " " provider s0
775+ , Append s0 " --> " s1
776+ , Append s1 consumer line
777+ , EmitEdges consumer tail allLayers restDoc
778+ ) =>
779+ EmitEdges consumer (Cons label ty tail) allLayers (Above (Text line) restDoc)
780+
781+-- | Walk all layers emitting D2 edges (mermaid format for wireLayersDebug).
782+class PrintLayersRL (rl :: RowList Type) (doc :: Doc) | rl -> doc
783+class PrintEdgesRL (rl :: RowList Type) (allLayers :: RowList Type) (doc :: Doc) | rl allLayers -> doc
784+
785+instance PrintLayersRL Nil (Text "")
786+else instance PrintEdgesRL rl rl doc => PrintLayersRL rl doc
787+
788+instance PrintEdgesRL Nil allLayers (Text "")
789+
790+else instance
791+ ( RowToList req reqRL
792+ , FilterScope reqRL filteredReqRL
793+ , EmitEdges sym filteredReqRL allLayers edgeDoc
794+ , PrintEdgesRL tail allLayers restDoc
795+ ) =>
796+ PrintEdgesRL (Cons sym (OmLayer req err (Record prov)) tail) allLayers (Above edgeDoc restDoc)
797+
798+-- | Map a symbol to same-length dashes (for box borders).
799+class DashesFor (sym :: Symbol) (dashes :: Symbol) | sym -> dashes
800+
801+instance DashesFor "" ""
802+
803+else instance
804+ ( Cons head tail sym
805+ , DashesFor tail rest
806+ , Append "─" rest dashes
807+ ) =>
808+ DashesFor sym dashes
809+
810+-- | Build box parts from a symbol.
811+class BoxTop (sym :: Symbol) (result :: Symbol) | sym -> result
812+
813+instance
814+ ( DashesFor sym dashes
815+ , Append "╭─" dashes s0
816+ , Append s0 "─╮" result
817+ ) =>
818+ BoxTop sym result
819+
820+class BoxMid (sym :: Symbol) (result :: Symbol) | sym -> result
821+
822+instance
823+ ( Append "│ " sym s0
824+ , Append s0 " │" result
825+ ) =>
826+ BoxMid sym result
827+
828+class BoxBot (sym :: Symbol) (result :: Symbol) | sym -> result
829+
830+instance
831+ ( DashesFor sym dashes
832+ , Append "╰─" dashes s0
833+ , Append s0 "─╯" result
834+ ) =>
835+ BoxBot sym result
836+
837+-- | Render a single dependency child node (dispatches on provided vs missing).
838+class RenderDepChildNode
839+ (isNotProvided :: Boolean) (provider :: Symbol) (label :: Symbol) (ty :: Type)
840+ (prePrefix :: Symbol) (contPrefix :: Symbol) (branchPrefix :: Symbol)
841+ (allLayers :: RowList Type) (doc :: Doc)
842+ | isNotProvided provider label ty prePrefix contPrefix branchPrefix allLayers -> doc
843+
844+-- Missing dep: boxed label, type outside
845+instance
846+ ( BoxTop label topLine, BoxMid label midLine, BoxBot label botLine
847+ , Append prePrefix topLine topFull
848+ , Append branchPrefix midLine midFull
849+ , Append contPrefix botLine botFull
850+ ) =>
851+ RenderDepChildNode True provider label ty prePrefix contPrefix branchPrefix allLayers
852+ (Above (Text topFull) (Above (Beside (Text midFull) (Beside (Text " :: ") (Beside (Quote ty) (Text " <-- ⚠️ not provided")))) (Text botFull)))
853+
854+-- Provided dep: boxed label, then recurse into provider's sub-tree
855+instance
856+ ( BoxTop label topLine, BoxMid label midLine, BoxBot label botLine
857+ , Append prePrefix topLine topFull
858+ , Append branchPrefix midLine midFull
859+ , Append contPrefix botLine botFull
860+ , Append contPrefix " " subTreePrefix
861+ , RenderProviderDeps provider subTreePrefix allLayers subDoc
862+ ) =>
863+ RenderDepChildNode False provider label ty prePrefix contPrefix branchPrefix allLayers
864+ (Above (Text topFull) (Above (Text midFull) (Above (Text botFull) subDoc)))
865+
866+-- | Is this provider the NotProvided marker?
867+class IsNotProvided (provider :: Symbol) (result :: Boolean) | provider -> result
868+instance IsNotProvided NotProvided True
869+else instance IsNotProvided other False
870+
871+-- | Does any requirement in this RowList resolve to NotProvided?
872+class HasDirectMissing (reqRL :: RowList Type) (allLayers :: RowList Type) (result :: Boolean)
873+ | reqRL allLayers -> result
874+
875+instance HasDirectMissing Nil allLayers False
876+
877+else instance
878+ ( FindProvider label allLayers provider
879+ , IsNotProvided provider isMissing
880+ , HasDirectMissingOr isMissing tail allLayers result
881+ ) =>
882+ HasDirectMissing (Cons label ty tail) allLayers result
883+
884+class HasDirectMissingOr (found :: Boolean) (tail :: RowList Type) (allLayers :: RowList Type) (result :: Boolean)
885+ | found tail allLayers -> result
886+
887+instance HasDirectMissingOr True tail allLayers True
888+
889+else instance
890+ HasDirectMissing tail allLayers result =>
891+ HasDirectMissingOr False tail allLayers result
892+
893+-- | Look up a layer's requirements by name.
894+class FindLayerReqs (sym :: Symbol) (allLayers :: RowList Type) (reqRL :: RowList Type)
895+ | sym allLayers -> reqRL
896+
897+instance
898+ RowToList req reqRL =>
899+ FindLayerReqs sym (Cons sym (OmLayer req err (Record prov)) tail) reqRL
900+
901+else instance
902+ FindLayerReqs sym tail reqRL =>
903+ FindLayerReqs sym (Cons other otherLayer tail) reqRL
904+
905+-- | Render a provider's sub-tree (stops at NotProvided or roots).
906+class RenderProviderDeps
907+ (provider :: Symbol) (prefix :: Symbol) (allLayers :: RowList Type) (doc :: Doc)
908+ | provider prefix allLayers -> doc
909+
910+instance RenderProviderDeps NotProvided prefix allLayers (Text "")
911+
912+else instance
913+ ( FindLayerReqs provider allLayers reqRL
914+ , FilterScope reqRL filteredReqRL
915+ , RenderDepTree prefix filteredReqRL allLayers doc
916+ ) =>
917+ RenderProviderDeps provider prefix allLayers doc
918+
919+-- | Render a dependency tree with tree connectors.
920+class RenderDepTree
921+ (prefix :: Symbol) (reqRL :: RowList Type) (allLayers :: RowList Type) (doc :: Doc)
922+ | prefix reqRL allLayers -> doc
923+
924+instance RenderDepTree prefix Nil allLayers (Text "")
925+
926+-- Last child: └───
927+else instance
928+ ( FindProvider label allLayers provider
929+ , IsNotProvided provider isNP
930+ , Append prefix "│ " prePrefix
931+ , Append prefix " " contPrefix
932+ , Append prefix "└───" branchPrefix
933+ , RenderDepChildNode isNP provider label ty prePrefix contPrefix branchPrefix allLayers doc
934+ ) =>
935+ RenderDepTree prefix (Cons label ty Nil) allLayers doc
936+
937+-- Non-last child: ├───
938+else instance
939+ ( FindProvider label allLayers provider
940+ , IsNotProvided provider isNP
941+ , Append prefix "│ " contPrefix
942+ , Append prefix "├───" branchPrefix
943+ , RenderDepChildNode isNP provider label ty contPrefix contPrefix branchPrefix allLayers childDoc
944+ , RenderDepTree prefix tail allLayers restDoc
945+ ) =>
946+ RenderDepTree prefix (Cons label ty tail) allLayers (Above childDoc restDoc)
947+
948+-- | Extract the first label from a RowList.
949+class FirstProvLabel (rl :: RowList Type) (label :: Symbol) | rl -> label
950+instance FirstProvLabel (Cons label ty tail) label
951+
952+-- | Dispatch: skip layers without missing deps, render tree for others.
953+class PrintLayerDispatch
954+ (show :: Boolean) (displayName :: Symbol) (filteredReqRL :: RowList Type)
955+ (tail :: RowList Type) (allLayers :: RowList Type) (doc :: Doc)
956+ | show displayName filteredReqRL tail allLayers -> doc
957+
958+instance
959+ PrintEdgesASCII tail allLayers doc =>
960+ PrintLayerDispatch False displayName filteredReqRL tail allLayers doc
961+
962+else instance
963+ ( BoxTop displayName topLine, BoxMid displayName midLine, BoxBot displayName botLine
964+ , RenderDepTree " " filteredReqRL allLayers treeDoc
965+ , PrintEdgesASCII tail allLayers restDoc
966+ ) =>
967+ PrintLayerDispatch True displayName filteredReqRL tail allLayers
968+ (Above (Text topLine) (Above (Text midLine) (Above (Text botLine) (Above treeDoc (Above (Text "") restDoc)))))
969+
970+-- | Walk all layers emitting ASCII dependency trees (for error messages).
971+class PrintLayersASCII (rl :: RowList Type) (doc :: Doc) | rl -> doc
972+class PrintEdgesASCII (rl :: RowList Type) (allLayers :: RowList Type) (doc :: Doc) | rl allLayers -> doc
973+
974+instance PrintLayersASCII Nil (Text "")
975+else instance PrintEdgesASCII rl rl doc => PrintLayersASCII rl doc
976+
977+instance PrintEdgesASCII Nil allLayers (Text "")
978+
979+else instance
980+ ( RowToList req reqRL
981+ , FilterScope reqRL filteredReqRL
982+ , RowToList prov provRL
983+ , FirstProvLabel provRL displayName
984+ , HasDirectMissing filteredReqRL allLayers hasMissing
985+ , PrintLayerDispatch hasMissing displayName filteredReqRL tail allLayers doc
986+ ) =>
987+ PrintEdgesASCII (Cons sym (OmLayer req err (Record prov)) tail) allLayers doc
988+
989+wireLayersDebug
990+ :: forall layers rl req err prov doc
991+ . RowToList layers rl
992+ => PrintLayersRL rl doc
993+ => Warn (Above (Text "") (Above (Text "%%{init: {\"flowchart\": {\"defaultRenderer\": \"elk\"}} }%%") (Above (Text "flowchart LR") doc)))
994+ => TopoWire rl layers (scope :: Scope) () () req err prov
995+ => Record layers
996+ -> OmLayer req err (Record prov)
997+wireLayersDebug = wireLayers
test/Playground.purs+6−43
@@ -6,9 +6,9 @@ import Data.Maybe (Maybe(..))
66import Effect (Effect)
77import Effect.Console as Console
88import Type.Row (type (+))
9−import Yoga.Om (Om, launchOm_)
9+import Yoga.Om (launchOm_)
1010import Yoga.Om as Om
11−import Yoga.Om.Layer (OmLayer, makeLayer, runLayer, combineRequirements)
11+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
1212
1313-- Define some example types
1414type Config = { port :: Int, host :: String }
@@ -24,25 +24,22 @@ type Cache = { get :: String -> Effect (Maybe String) }
2424type CacheL r = (cache :: Cache | r)
2525type CacheL_ = CacheL ()
2626
27−-- Example layer 1: needs config, provides logger
28−loggerLayer :: OmLayer ConfigL_ LoggerL_ ()
27+loggerLayer :: OmLayer ConfigL_ () { logger :: Logger }
2928loggerLayer = makeLayer do
3029 { config } <- Om.ask
3130 pure { logger: { log: \msg -> pure unit } }
3231
33−-- Example layer 2: needs config, provides database
34−databaseLayer :: OmLayer ConfigL_ DatabaseL_ ()
32+databaseLayer :: OmLayer ConfigL_ () { database :: Database }
3533databaseLayer = makeLayer do
3634 { config } <- Om.ask
3735 pure { database: { query: \q -> pure [] } }
3836
39−-- Example layer 3: needs logger and database, provides cache
40−cacheLayer :: OmLayer (LoggerL + DatabaseL_) CacheL_ ()
37+cacheLayer :: OmLayer (LoggerL + DatabaseL_) () { cache :: Cache }
4138cacheLayer = makeLayer do
4239 { logger, database } <- Om.ask
4340 pure { cache: { get: \key -> pure Nothing } }
4441
45−programLayer :: OmLayer (DatabaseL + CacheL_) () ()
42+programLayer :: OmLayer (DatabaseL + CacheL_) () {}
4643programLayer = makeLayer do
4744 pure {}
4845
@@ -53,37 +50,3 @@ aProgram = launchOm_ do
5350 , database: { query: \q -> pure [] }
5451 , cache: { get: \key -> pure Nothing }
5552 }
56−
57−--------------------------------------------------------------------------------
58−-- TRY THESE! Uncomment one at a time to see different error messages
59−--------------------------------------------------------------------------------
60−
61−-- ❌ ERROR 1: Missing config entirely
62−-- Uncomment this to see the error:
63−-- example1 :: Om (Record ()) () (Record (logger :: Logger))
64−-- example1 = runLayer {} loggerLayer
65−
66−-- ❌ ERROR 2: Partial context - have logger, missing database
67−-- Uncomment this to see the error:
68−-- example2 :: Om (Record (logger :: Logger)) () (Record (cache :: Cache))
69−-- example2 = runLayer { logger: { log: \_ -> pure unit } } cacheLayer
70−
71−-- ❌ ERROR 3: Multiple missing dependencies
72−-- Uncomment this to see the error:
73−-- combined :: OmLayer (config :: Config) (logger :: Logger, database :: Database) ()
74−-- combined = combineRequirements loggerLayer databaseLayer
75−-- example3 :: Om (Record ()) () (Record (logger :: Logger, database :: Database))
76−-- example3 = runLayer {} combined
77−
78−-- ✅ SUCCESS: All dependencies provided!
79−-- This one works! Uncomment to verify:
80−-- example4 :: Om (Record (config :: Config)) () (Record (logger :: Logger))
81−-- example4 = runLayer { config: { port: 5432, host: "localhost" } } loggerLayer
82−
83−--------------------------------------------------------------------------------
84−-- Instructions:
85−-- 1. Uncomment ONE of the examples above
86−-- 2. Run: npx spago build -p yoga-om-layer
87−-- 3. See the beautiful error message!
88−-- 4. Try different combinations to explore
89−--------------------------------------------------------------------------------
test/Test/Main.purs+5−113
@@ -2,122 +2,14 @@ module Test.Main where
22
33import Prelude
44
5−import Data.Newtype (un)
65import Effect (Effect)
76import Effect.Aff (launchAff_)
87import Effect.Class (liftEffect)
9−import Effect.Class.Console as Console
10−import Effect.Ref as Ref
11−import Test.Spec (describe, it)
8+import Test.Spec.Discovery (discover)
129import Test.Spec.Reporter (consoleReporter)
13−import Test.Spec.Runner (runSpec)
14−import Yoga.Om as Om
15−import Yoga.Om.Layer (OmLayer(..), makeLayer, combineRequirements)
16−
17−-- Example types
18−type Config = { port :: Int, host :: String }
19−type Logger = { log :: String -> Effect Unit }
20−type Database = { query :: String -> Effect (Array String) }
10+import Test.Spec.Runner.Node (runSpecAndExitProcess)
2111
2212main :: Effect Unit
23−main = launchAff_ $ runSpec [ consoleReporter ] do
24− describe "OmLayer - Proof of Concept: Type-Level Deduplication" do
25−
26− it "proves Row.Nub deduplicates shared requirements at type level" do
27− -- Define layers with dependencies
28− let
29− -- Layer that requires config
30− loggerLayer :: OmLayer (config :: Config) (logger :: Logger) ()
31− loggerLayer = makeLayer do
32− { config } <- Om.ask
33− pure { logger: { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg } }
34−
35− -- Layer that also requires config
36− databaseLayer :: OmLayer (config :: Config) (database :: Database) ()
37− databaseLayer = makeLayer do
38− { config } <- Om.ask
39− pure { database: { query: \q -> pure [ "Result from " <> config.host ] } }
40−
41− -- The proof: This type-checks!
42− -- combineRequirements takes two layers that each need (config :: Config)
43− -- and produces a layer that needs (config :: Config) - NOT (config, config)!
44− let
45− -- The type annotation proves deduplication works:
46− _proofOfDeduplication
47− :: OmLayer (config :: Config) _ _
48− _proofOfDeduplication = combineRequirements loggerLayer databaseLayer
49−
50− Console.log "✓ PROOF OF CONCEPT SUCCESS!"
51− Console.log " "
52− Console.log " Key insight: Row.Nub automatically deduplicates at the type level"
53− Console.log " "
54− Console.log " Given:"
55− Console.log " layer1 :: OmLayer (config :: Config) (logger :: Logger) ()"
56− Console.log " layer2 :: OmLayer (config :: Config) (database :: Database) ()"
57− Console.log " "
58− Console.log " combineRequirements layer1 layer2 has type:"
59− Console.log " OmLayer (config :: Config) _ _"
60− Console.log " "
61− Console.log " NOT OmLayer (config :: Config, config :: Config) _ _"
62− Console.log " "
63− Console.log " The (config, config) requirement is automatically deduplicated"
64− Console.log " to just (config) by the Nub constraint in combineRequirements!"
65− Console.log " "
66− Console.log " This is the foundation for ZLayer-style dependency injection"
67− Console.log " where the type system automatically handles shared dependencies."
68−
69− pure unit
70−
71− it "proves deduplication at runtime via shared Ref" do
72− -- Create a mutable reference to track accesses
73− accessLog <- liftEffect $ Ref.new ""
74−
75− let
76− -- Manually construct a combined layer that shows both "components"
77− -- accessing the same shared context (with deduplicated requirements)
78− combinedLayer :: OmLayer (config :: Config, accessLog :: Ref.Ref String) (logger :: Logger, database :: Database) ()
79− combinedLayer = makeLayer do
80− -- Get the shared context - note the type signature shows it's deduplicated!
81− { config, accessLog: log } <- Om.ask
82−
83− -- "Logger component" accesses config and logs it
84− liftEffect $ Ref.modify_ (_ <> "config-accessed-by-logger ") log
85− let logger = { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg }
86−
87− -- "Database component" also accesses config and logs it
88− -- CRITICAL: They're both accessing the SAME context record!
89− liftEffect $ Ref.modify_ (_ <> "config-accessed-by-database ") log
90− let database = { query: \q -> pure [ "Result from " <> config.host ] }
91−
92− pure { logger, database }
93−
94− -- Run with the context
95− result <- un OmLayer combinedLayer
96− # Om.runOm
97− { config: { port: 5432, host: "localhost" }
98− , accessLog: accessLog
99− }
100− { exception: \_ -> pure { logger: { log: \_ -> pure unit }, database: { query: \_ -> pure [] } } }
101−
102− -- Check the access log
103− finalLog <- liftEffect $ Ref.read accessLog
104−
105− -- Use the resulting services to prove they work
106− liftEffect $ result.logger.log "Testing logger from combined layer"
107− queryResult <- liftEffect $ result.database.query "SELECT *"
108−
109− -- Both "layers" ran and accessed config via the same context
110− Console.log ""
111− Console.log "✓ RUNTIME DEDUPLICATION PROOF:"
112− Console.log $ " Access log: '" <> finalLog <> "'"
113− Console.log $ " Query result: " <> show queryResult
114− Console.log " "
115− Console.log " Both components accessed the same shared context!"
116− Console.log " The type signature OmLayer (config, accessLog) _ _ shows"
117− Console.log " that even though two components need config, there's only"
118− Console.log " ONE config in the context (not config + config)."
119− Console.log " "
120− Console.log " At runtime, both components read from the same context record,"
121− Console.log " and both wrote to the same Ref, proving deduplication works!"
122−
123− pure unit
13+main = launchAff_ do
14+ specs <- discover "Test\\.Yoga\\.Om\\.Layer\\..*Spec"
15+ liftEffect $ runSpecAndExitProcess [ consoleReporter ] specs
test/Test/Yoga/Om/Layer/CompositionSpec.pursnew file+59
@@ -0,0 +1,59 @@
1+module Test.Yoga.Om.Layer.CompositionSpec where
2+
3+import Prelude
4+
5+import Effect (Effect)
6+import Effect.Class (liftEffect)
7+import Effect.Class.Console as Console
8+import Effect.Ref as Ref
9+import Test.Spec (Spec, describe, it)
10+import Test.Spec.Assertions (shouldEqual)
11+import Yoga.Om as Om
12+import Yoga.Om.Layer (OmLayer, makeLayer, combineRequirements, runLayer)
13+
14+type Config = { port :: Int, host :: String }
15+type Logger = { log :: String -> Effect Unit }
16+type Database = { query :: String -> Effect (Array String) }
17+
18+loggerLayer :: OmLayer (config :: Config) () { logger :: Logger }
19+loggerLayer = makeLayer do
20+ { config } <- Om.ask
21+ pure { logger: { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg } }
22+
23+databaseLayer :: OmLayer (config :: Config) () { database :: Database }
24+databaseLayer = makeLayer do
25+ { config } <- Om.ask
26+ pure { database: { query: \_ -> pure [ "Result from " <> config.host ] } }
27+
28+spec :: Spec Unit
29+spec = describe "Composition" do
30+
31+ it "proves Row.Nub deduplicates shared requirements at type level" do
32+ let
33+ _proofOfDeduplication :: OmLayer (config :: Config) _ _
34+ _proofOfDeduplication = combineRequirements loggerLayer databaseLayer
35+ Console.log "Type-level deduplication works: (config, config) -> (config)"
36+
37+ it "proves deduplication at runtime via shared Ref" do
38+ accessLog <- liftEffect $ Ref.new ""
39+ let
40+ combinedLayer :: OmLayer (config :: Config, accessLog :: Ref.Ref String) () { logger :: Logger, database :: Database }
41+ combinedLayer = makeLayer do
42+ { config, accessLog: log } <- Om.ask
43+ liftEffect $ Ref.modify_ (_ <> "config-accessed-by-logger ") log
44+ let logger = { log: \msg -> Console.log $ "[" <> config.host <> "] " <> msg }
45+ liftEffect $ Ref.modify_ (_ <> "config-accessed-by-database ") log
46+ let database = { query: \_ -> pure [ "Result from " <> config.host ] }
47+ pure { logger, database }
48+
49+ let ctx = { config: { port: 5432, host: "localhost" }, accessLog: accessLog }
50+ result <- runLayer ctx combinedLayer
51+ # Om.runOm ctx
52+ { exception: \_ -> pure { logger: { log: \_ -> pure unit }, database: { query: \_ -> pure [] } } }
53+
54+ finalLog <- liftEffect $ Ref.read accessLog
55+ finalLog `shouldEqual` "config-accessed-by-logger config-accessed-by-database "
56+
57+ liftEffect $ result.logger.log "Testing logger from combined layer"
58+ queryResult <- liftEffect $ result.database.query "SELECT *"
59+ queryResult `shouldEqual` [ "Result from localhost" ]
test/Test/Yoga/Om/Layer/ErrorChannelSpec.pursnew file+167
@@ -0,0 +1,167 @@
1+module Test.Yoga.Om.Layer.ErrorChannelSpec where
2+
3+import Prelude
4+
5+import Effect.Aff (try)
6+import Effect.Aff.Class (liftAff)
7+import Effect.Class (liftEffect)
8+import Effect.Ref as Ref
9+import Data.Either (Either(..))
10+import Data.Variant (match)
11+import Test.Spec (Spec, describe, it)
12+import Test.Spec.Assertions (shouldEqual)
13+import Yoga.Om as Om
14+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, combineRequirements, runLayer, runScopedWith, provide)
15+
16+spec :: Spec Unit
17+spec = describe "Error Channel" do
18+
19+ it "composes layers with different error types" do
20+ let
21+ dbLayer :: OmLayer () (dbError :: String) { db :: String }
22+ dbLayer = makeLayer do
23+ pure { db: "connected" }
24+
25+ cacheLayer :: OmLayer () (cacheError :: String) { cache :: String }
26+ cacheLayer = makeLayer do
27+ pure { cache: "ready" }
28+
29+ combined :: OmLayer () (dbError :: String, cacheError :: String) { db :: String, cache :: String }
30+ combined = combineRequirements dbLayer cacheLayer
31+
32+ result <- Om.runOm {}
33+ { exception: \_ -> pure { db: "", cache: "" }
34+ , dbError: \_ -> pure { db: "", cache: "" }
35+ , cacheError: \_ -> pure { db: "", cache: "" }
36+ }
37+ (runLayer {} combined)
38+ result.db `shouldEqual` "connected"
39+ result.cache `shouldEqual` "ready"
40+
41+ it "propagates errors from the first layer" do
42+ let
43+ failingLayer :: OmLayer () (dbError :: String) { db :: String }
44+ failingLayer = makeLayer do
45+ Om.throw { dbError: "connection refused" }
46+
47+ okLayer :: OmLayer () () { cache :: String }
48+ okLayer = makeLayer do
49+ pure { cache: "ready" }
50+
51+ combined = combineRequirements failingLayer okLayer
52+
53+ result <- Om.runReader {} (runLayer {} combined)
54+ # liftAff
55+ case result of
56+ Left err -> do
57+ let
58+ msg = err # match
59+ { exception: \_ -> "exception"
60+ , dbError: \e -> e
61+ }
62+ msg `shouldEqual` "connection refused"
63+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
64+
65+ it "propagates errors from the second layer" do
66+ let
67+ okLayer :: OmLayer () () { db :: String }
68+ okLayer = makeLayer do
69+ pure { db: "connected" }
70+
71+ failingLayer :: OmLayer () (cacheError :: String) { cache :: String }
72+ failingLayer = makeLayer do
73+ Om.throw { cacheError: "cache unavailable" }
74+
75+ combined = combineRequirements okLayer failingLayer
76+
77+ result <- Om.runReader {} (runLayer {} combined)
78+ # liftAff
79+ case result of
80+ Left err -> do
81+ let
82+ msg = err # match
83+ { exception: \_ -> "exception"
84+ , cacheError: \e -> e
85+ }
86+ msg `shouldEqual` "cache unavailable"
87+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
88+
89+ it "propagates errors through vertical composition" do
90+ let
91+ baseLayer :: OmLayer () (baseError :: String) { base :: String }
92+ baseLayer = makeLayer do
93+ Om.throw { baseError: "base failed" }
94+
95+ upperLayer :: OmLayer (base :: String) (upperError :: String) { upper :: String }
96+ upperLayer = makeLayer do
97+ { base } <- Om.ask
98+ pure { upper: base <> "-extended" }
99+
100+ composed = upperLayer `provide` baseLayer
101+
102+ result <- Om.runReader {} (runLayer {} composed)
103+ # liftAff
104+ case result of
105+ Left err -> do
106+ let
107+ msg = err # match
108+ { exception: \_ -> "exception"
109+ , baseError: \e -> e
110+ , upperError: \_ -> "upper"
111+ }
112+ msg `shouldEqual` "base failed"
113+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
114+
115+ it "scoped layers with errors run via runScopedWith" do
116+ log <- liftEffect $ Ref.new []
117+ let
118+ dbLayer :: OmLayer (scope :: Scope) (dbError :: String) { db :: String }
119+ dbLayer = makeScopedLayer
120+ (pure { db: "connected" })
121+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "db-closed" ]) log)
122+
123+ cacheLayer :: OmLayer (scope :: Scope) (cacheError :: String) { cache :: String }
124+ cacheLayer = makeScopedLayer
125+ (pure { cache: "ready" })
126+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "cache-closed" ]) log)
127+
128+ combined = combineRequirements dbLayer cacheLayer
129+
130+ let
131+ handlers =
132+ { exception: \_ -> pure { db: "", cache: "" }
133+ , dbError: \_ -> pure { db: "", cache: "" }
134+ , cacheError: \_ -> pure { db: "", cache: "" }
135+ }
136+ result <- liftAff $ runScopedWith handlers combined
137+ result.db `shouldEqual` "connected"
138+ result.cache `shouldEqual` "ready"
139+ finalLog <- liftEffect $ Ref.read log
140+ finalLog `shouldEqual` [ "cache-closed", "db-closed" ]
141+
142+ it "scoped layer error triggers finalizers" do
143+ log <- liftEffect $ Ref.new []
144+ let
145+ goodLayer :: OmLayer (scope :: Scope) () { good :: String }
146+ goodLayer = makeScopedLayer
147+ (pure { good: "ok" })
148+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "good-closed" ]) log)
149+
150+ failLayer :: OmLayer (scope :: Scope) (buildError :: String) { bad :: String }
151+ failLayer = makeScopedLayer
152+ (Om.throw { buildError: "build failed" })
153+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "bad-closed" ]) log)
154+
155+ combined = combineRequirements goodLayer failLayer
156+
157+ let
158+ handlers =
159+ { exception: \_ -> pure { good: "", bad: "" }
160+ , buildError: \_ -> pure { good: "", bad: "" }
161+ }
162+ result <- liftAff $ try $ runScopedWith handlers combined
163+ case result of
164+ Right r -> r.good `shouldEqual` ""
165+ Left _ -> pure unit
166+ finalLog <- liftEffect $ Ref.read log
167+ finalLog `shouldEqual` [ "good-closed" ]
test/Test/Yoga/Om/Layer/InstancesSpec.pursnew file+163
@@ -0,0 +1,163 @@
1+module Test.Yoga.Om.Layer.InstancesSpec where
2+
3+import Prelude
4+
5+import Control.Alt ((<|>))
6+import Control.Monad.Reader (ask, local)
7+import Control.Parallel (parApply)
8+import Effect.Class (liftEffect)
9+import Effect.Ref as Ref
10+import Test.Spec (Spec, describe, it)
11+import Test.Spec.Assertions (shouldEqual)
12+import Effect.Exception as Exception
13+import Yoga.Om as Om
14+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
15+
16+spec :: Spec Unit
17+spec = do
18+
19+ describe "Functor / Apply / Bind" do
20+
21+ it "maps over layer output" do
22+ let
23+ layer :: OmLayer () () { value :: Int }
24+ layer = makeLayer (pure { value: 10 })
25+
26+ mapped = layer <#> \r -> { doubled: r.value * 2 }
27+
28+ result <- Om.runOm {} { exception: \_ -> pure { doubled: 0 } } (runLayer {} mapped)
29+ result.doubled `shouldEqual` 20
30+
31+ it "applies two layers" do
32+ let
33+ fLayer :: OmLayer () () ({ a :: Int } -> { a :: Int, b :: String })
34+ fLayer = makeLayer (pure \r -> { a: r.a, b: show r.a })
35+
36+ aLayer :: OmLayer () () { a :: Int }
37+ aLayer = makeLayer (pure { a: 42 })
38+
39+ result <- Om.runOm {} { exception: \_ -> pure { a: 0, b: "" } } (runLayer {} (fLayer <*> aLayer))
40+ result.a `shouldEqual` 42
41+ result.b `shouldEqual` "42"
42+
43+ it "binds layers sequentially" do
44+ let
45+ layer = do
46+ r <- makeLayer (pure { x: 5 }) :: OmLayer () () { x :: Int }
47+ makeLayer (pure { result: r.x + 10 })
48+
49+ result <- Om.runOm {} { exception: \_ -> pure { result: 0 } } (runLayer {} layer)
50+ result.result `shouldEqual` 15
51+
52+ it "runs layers against a superset context" do
53+ let
54+ layer :: OmLayer (a :: Int) () { out :: Int }
55+ layer = makeLayer do
56+ { a } <- Om.ask
57+ pure { out: a + 1 }
58+
59+ ctx = { a: 41, extra: "ok" }
60+
61+ result <- Om.runOm ctx { exception: \_ -> pure { out: 0 } } (runLayer ctx layer)
62+ result.out `shouldEqual` 42
63+
64+ describe "MonadEffect / MonadAff" do
65+
66+ it "liftEffect works directly in layer bind" do
67+ ref <- liftEffect $ Ref.new 0
68+ let
69+ layer :: OmLayer () () Int
70+ layer = do
71+ liftEffect $ Ref.write 42 ref
72+ liftEffect $ Ref.read ref
73+
74+ result <- Om.runOm {} { exception: \_ -> pure 0 } (runLayer {} layer)
75+ result `shouldEqual` 42
76+
77+ describe "MonadAsk / MonadReader" do
78+
79+ it "ask reads the context directly" do
80+ let
81+ layer :: OmLayer (port :: Int) () Int
82+ layer = do
83+ ctx <- ask
84+ pure ctx.port
85+
86+ result <- Om.runOm { port: 8080 } { exception: \_ -> pure 0 } (runLayer { port: 8080 } layer)
87+ result `shouldEqual` 8080
88+
89+ it "local modifies context for a layer" do
90+ let
91+ inner :: OmLayer (port :: Int) () Int
92+ inner = do
93+ ctx <- ask
94+ pure ctx.port
95+
96+ layer :: OmLayer (port :: Int) () Int
97+ layer = local (\r -> r { port = r.port + 1 }) inner
98+
99+ result <- Om.runOm { port: 8080 } { exception: \_ -> pure 0 } (runLayer { port: 8080 } layer)
100+ result `shouldEqual` 8081
101+
102+ describe "MonadThrow / MonadError" do
103+
104+ it "throwError raises into the error channel" do
105+ let
106+ layer :: OmLayer () (myErr :: String) Int
107+ layer = Om.throw { myErr: "boom" }
108+
109+ result <- Om.runOm {}
110+ { exception: \_ -> pure (-1), myErr: \_ -> pure (-2) }
111+ (runLayer {} layer)
112+ result `shouldEqual` (-2)
113+
114+ describe "Alt" do
115+
116+ it "falls back to second layer on failure" do
117+ let
118+ failing :: OmLayer () () Int
119+ failing = Om.throw { exception: Exception.error "nope" }
120+
121+ fallback :: OmLayer () () Int
122+ fallback = pure 99
123+
124+ layer = failing <|> fallback
125+
126+ result <- Om.runOm {} { exception: \_ -> pure (-1) } (runLayer {} layer)
127+ result `shouldEqual` 99
128+
129+ describe "Semigroup" do
130+
131+ it "appends layer outputs" do
132+ let
133+ a :: OmLayer () () String
134+ a = pure "hello"
135+
136+ b :: OmLayer () () String
137+ b = pure " world"
138+
139+ result <- Om.runOm {} { exception: \_ -> pure "" } (runLayer {} (a <> b))
140+ result `shouldEqual` "hello world"
141+
142+ describe "Parallel" do
143+
144+ it "runs layers in parallel via parApply" do
145+ log <- liftEffect $ Ref.new []
146+ let
147+ layerA :: OmLayer () () { a :: String }
148+ layerA = makeLayer do
149+ liftEffect $ Ref.modify_ (_ <> [ "a" ]) log
150+ pure { a: "A" }
151+
152+ layerB :: OmLayer () () { b :: String }
153+ layerB = makeLayer do
154+ liftEffect $ Ref.modify_ (_ <> [ "b" ]) log
155+ pure { b: "B" }
156+
157+ combined = parApply (map (\ra rb -> { a: ra.a, b: rb.b }) layerA) layerB
158+
159+ result <- Om.runOm {} { exception: \_ -> pure { a: "", b: "" } } (runLayer {} combined)
160+ result.a `shouldEqual` "A"
161+ result.b `shouldEqual` "B"
162+ finalLog <- liftEffect $ Ref.read log
163+ finalLog `shouldEqual` [ "a", "b" ]
test/Test/Yoga/Om/Layer/MemoizationSpec.pursnew file+114
@@ -0,0 +1,114 @@
1+module Test.Yoga.Om.Layer.MemoizationSpec where
2+
3+import Prelude
4+
5+import Effect.Aff.Class (liftAff)
6+import Effect.Class (liftEffect)
7+import Effect.Ref as Ref
8+import Test.Spec (Spec, describe, it)
9+import Test.Spec.Assertions (shouldEqual)
10+import Yoga.Om as Om
11+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, combineRequirements, runScoped, provide)
12+
13+spec :: Spec Unit
14+spec = describe "Automatic Memoization" do
15+
16+ it "builds the same layer only once when used in two branches" do
17+ counter <- liftEffect $ Ref.new 0
18+ let
19+ expensiveLayer :: OmLayer (scope :: Scope) () { value :: String }
20+ expensiveLayer = makeLayer do
21+ liftEffect $ Ref.modify_ (_ + 1) counter
22+ pure { value: "built" }
23+
24+ branch1 :: OmLayer (scope :: Scope) () { out1 :: String }
25+ branch1 = consumer `provide` expensiveLayer
26+ where
27+ consumer :: OmLayer (scope :: Scope, value :: String) () { out1 :: String }
28+ consumer = makeLayer do
29+ { value } <- Om.ask
30+ pure { out1: value <> "-1" }
31+
32+ branch2 :: OmLayer (scope :: Scope) () { out2 :: String }
33+ branch2 = consumer `provide` expensiveLayer
34+ where
35+ consumer :: OmLayer (scope :: Scope, value :: String) () { out2 :: String }
36+ consumer = makeLayer do
37+ { value } <- Om.ask
38+ pure { out2: value <> "-2" }
39+
40+ app = combineRequirements branch1 branch2
41+
42+ result <- liftAff $ runScoped app
43+ result.out1 `shouldEqual` "built-1"
44+ result.out2 `shouldEqual` "built-2"
45+ count <- liftEffect $ Ref.read counter
46+ count `shouldEqual` 1
47+
48+ it "memoized scoped layer runs finalizer only once" do
49+ log <- liftEffect $ Ref.new []
50+ let
51+ dbLayer :: OmLayer (scope :: Scope) () { db :: String }
52+ dbLayer = makeScopedLayer
53+ (pure { db: "connected" })
54+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "db-closed" ]) log)
55+
56+ repoBranch :: OmLayer (scope :: Scope) () { repo :: String }
57+ repoBranch = repo `provide` dbLayer
58+ where
59+ repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
60+ repo = makeLayer do
61+ { db } <- Om.ask
62+ pure { repo: db <> "-repo" }
63+
64+ analyticsBranch :: OmLayer (scope :: Scope) () { analytics :: String }
65+ analyticsBranch = analytics `provide` dbLayer
66+ where
67+ analytics :: OmLayer (scope :: Scope, db :: String) () { analytics :: String }
68+ analytics = makeLayer do
69+ { db } <- Om.ask
70+ pure { analytics: db <> "-analytics" }
71+
72+ app = combineRequirements repoBranch analyticsBranch
73+
74+ result <- liftAff $ runScoped app
75+ result.repo `shouldEqual` "connected-repo"
76+ result.analytics `shouldEqual` "connected-analytics"
77+ finalLog <- liftEffect $ Ref.read log
78+ finalLog `shouldEqual` [ "db-closed" ]
79+
80+ it "memoized bracketLayer acquires and releases only once" do
81+ log <- liftEffect $ Ref.new []
82+ let
83+ connLayer :: OmLayer (scope :: Scope) () { conn :: Int }
84+ connLayer = bracketLayer
85+ ( do
86+ liftEffect $ Ref.modify_ (_ <> [ "acquire" ]) log
87+ pure 42
88+ )
89+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release" ]) log)
90+ (\n -> pure { conn: n })
91+
92+ branch1 :: OmLayer (scope :: Scope) () { x :: String }
93+ branch1 = upper `provide` connLayer
94+ where
95+ upper :: OmLayer (scope :: Scope, conn :: Int) () { x :: String }
96+ upper = makeLayer do
97+ { conn } <- Om.ask
98+ pure { x: show conn }
99+
100+ branch2 :: OmLayer (scope :: Scope) () { y :: String }
101+ branch2 = upper `provide` connLayer
102+ where
103+ upper :: OmLayer (scope :: Scope, conn :: Int) () { y :: String }
104+ upper = makeLayer do
105+ { conn } <- Om.ask
106+ pure { y: show (conn + 1) }
107+
108+ app = combineRequirements branch1 branch2
109+
110+ result <- liftAff $ runScoped app
111+ result.x `shouldEqual` "42"
112+ result.y `shouldEqual` "43"
113+ finalLog <- liftEffect $ Ref.read log
114+ finalLog `shouldEqual` [ "acquire", "release" ]
test/Test/Yoga/Om/Layer/PureInferenceSpec.pursnew file+22
@@ -0,0 +1,22 @@
1+module Test.Yoga.Om.Layer.PureInferenceSpec where
2+
3+import Prelude
4+
5+import Test.Spec (Spec, describe, it)
6+import Test.Spec.Assertions (shouldEqual)
7+import Yoga.Om as Om
8+import Yoga.Om.Layer (OmLayer, withValue, makeLayer, runLayer)
9+
10+spec :: Spec Unit
11+spec = describe "withValue" do
12+
13+ it "combines a plain record with a layer without needing VTA" do
14+ let
15+ layer :: OmLayer () () { value :: Int }
16+ layer = makeLayer (pure { value: 42 })
17+
18+ result = layer # withValue { extra: "hello" }
19+
20+ out <- Om.runOm {} { exception: \_ -> pure { value: -1, extra: "" } } (runLayer {} result)
21+ out.value `shouldEqual` 42
22+ out.extra `shouldEqual` "hello"
test/Test/Yoga/Om/Layer/RecoveringSpec.pursnew file+128
@@ -0,0 +1,128 @@
1+module Test.Yoga.Om.Layer.RecoveringSpec where
2+
3+import Prelude
4+
5+import Effect.Aff.Class (liftAff)
6+import Effect.Class (liftEffect)
7+import Effect.Ref as Ref
8+import Data.Either (Either(..))
9+import Data.Variant (match)
10+import Data.Time.Duration (Milliseconds(..))
11+import Effect.Aff.Retry (RetryStatus(..), limitRetries, constantDelay)
12+import Test.Spec (Spec, describe, it)
13+import Test.Spec.Assertions (shouldEqual)
14+import Yoga.Om as Om
15+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer, recovering)
16+
17+spec :: Spec Unit
18+spec = describe "Layer recovering" do
19+
20+ it "retries layer construction on matching errors" do
21+ attemptsRef <- liftEffect $ Ref.new 0
22+ let
23+ layer :: OmLayer () (dbError :: String) { db :: String }
24+ layer = makeLayer do
25+ attempts <- Ref.modify (_ + 1) attemptsRef # liftEffect
26+ when (attempts < 3) do
27+ Om.throw { dbError: "connection refused" }
28+ pure { db: "connected" }
29+
30+ retried = layer
31+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 5)
32+ \_ -> { dbError: \_ -> pure true }
33+
34+ result <- Om.runOm {}
35+ { exception: \_ -> pure { db: "" }, dbError: \_ -> pure { db: "" } }
36+ (runLayer {} retried)
37+ result.db `shouldEqual` "connected"
38+ attempts <- liftEffect $ Ref.read attemptsRef
39+ attempts `shouldEqual` 3
40+
41+ it "does not retry on non-matching errors" do
42+ attemptsRef <- liftEffect $ Ref.new 0
43+ let
44+ layer :: OmLayer () (dbError :: String, authError :: String) { db :: String }
45+ layer = makeLayer do
46+ Ref.modify_ (_ + 1) attemptsRef # liftEffect
47+ Om.throw { authError: "unauthorized" }
48+
49+ retried = layer
50+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 3)
51+ \_ -> { dbError: \_ -> pure true }
52+
53+ _ <- Om.runOm {}
54+ { exception: \_ -> pure { db: "" }
55+ , dbError: \_ -> pure { db: "" }
56+ , authError: \_ -> pure { db: "" }
57+ }
58+ (runLayer {} retried)
59+ attempts <- liftEffect $ Ref.read attemptsRef
60+ attempts `shouldEqual` 1
61+
62+ it "preserves the error when retries are exhausted" do
63+ let
64+ layer :: OmLayer () (dbError :: String) { db :: String }
65+ layer = makeLayer do
66+ Om.throw { dbError: "connection refused" }
67+
68+ retried = layer
69+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 2)
70+ \_ -> { dbError: \_ -> pure true }
71+
72+ result <- Om.runReader {} (runLayer {} retried)
73+ # liftAff
74+ case result of
75+ Left err -> do
76+ let
77+ msg = err # match
78+ { exception: \_ -> "exception"
79+ , dbError: \e -> e
80+ }
81+ msg `shouldEqual` "connection refused"
82+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
83+
84+ it "only retries the specified error in a multi-error layer" do
85+ attemptsRef <- liftEffect $ Ref.new 0
86+ let
87+ layer :: OmLayer () (dbError :: String, cacheError :: String) { db :: String }
88+ layer = makeLayer do
89+ attempts <- Ref.modify (_ + 1) attemptsRef # liftEffect
90+ if attempts <= 2 then Om.throw { dbError: "db down" }
91+ else Om.throw { cacheError: "cache miss" }
92+
93+ retried = layer
94+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 5)
95+ \_ -> { dbError: \_ -> pure true }
96+
97+ result <- Om.runReader {} (runLayer {} retried)
98+ # liftAff
99+ case result of
100+ Left err -> do
101+ let
102+ msg = err # match
103+ { exception: \_ -> "exception"
104+ , dbError: \e -> e
105+ , cacheError: \e -> e
106+ }
107+ msg `shouldEqual` "cache miss"
108+ Right _ -> "should have failed" `shouldEqual` "but succeeded"
109+ attempts <- liftEffect $ Ref.read attemptsRef
110+ attempts `shouldEqual` 3
111+
112+ it "uses RetryStatus in the check" do
113+ attemptsRef <- liftEffect $ Ref.new 0
114+ let
115+ layer :: OmLayer () (dbError :: String) { db :: String }
116+ layer = makeLayer do
117+ Ref.modify_ (_ + 1) attemptsRef # liftEffect
118+ Om.throw { dbError: "fail" }
119+
120+ retried = layer
121+ # recovering (constantDelay (Milliseconds 0.0) <> limitRetries 10)
122+ \(RetryStatus s) -> { dbError: \_ -> pure (s.iterNumber < 2) }
123+
124+ _ <- Om.runOm {}
125+ { exception: \_ -> pure { db: "" }, dbError: \_ -> pure { db: "" } }
126+ (runLayer {} retried)
127+ attempts <- liftEffect $ Ref.read attemptsRef
128+ attempts `shouldEqual` 3
test/Test/Yoga/Om/Layer/RepeatingSpec.pursnew file+49
@@ -0,0 +1,49 @@
1+module Test.Yoga.Om.Layer.RepeatingSpec where
2+
3+import Prelude
4+
5+import Effect.Class (liftEffect)
6+import Effect.Ref as Ref
7+import Data.Time.Duration (Milliseconds(..))
8+import Effect.Aff.Retry (limitRetries, constantDelay)
9+import Test.Spec (Spec, describe, it)
10+import Test.Spec.Assertions (shouldEqual)
11+import Yoga.Om as Om
12+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer, repeating)
13+
14+spec :: Spec Unit
15+spec = describe "Layer repeating" do
16+
17+ it "repeats layer construction while condition holds" do
18+ counterRef <- liftEffect $ Ref.new 0
19+ let
20+ layer :: OmLayer () () { count :: Int }
21+ layer = makeLayer do
22+ count <- Ref.modify (_ + 1) counterRef # liftEffect
23+ pure { count }
24+
25+ repeated = layer
26+ # repeating (constantDelay (Milliseconds 0.0) <> limitRetries 10)
27+ \_ r -> pure (r.count < 5)
28+
29+ result <- Om.runOm {}
30+ { exception: \_ -> pure { count: 0 } }
31+ (runLayer {} repeated)
32+ result.count `shouldEqual` 5
33+
34+ it "stops when policy exhausted" do
35+ counterRef <- liftEffect $ Ref.new 0
36+ let
37+ layer :: OmLayer () () { count :: Int }
38+ layer = makeLayer do
39+ count <- Ref.modify (_ + 1) counterRef # liftEffect
40+ pure { count }
41+
42+ repeated = layer
43+ # repeating (constantDelay (Milliseconds 0.0) <> limitRetries 3)
44+ \_ _ -> pure true
45+
46+ result <- Om.runOm {}
47+ { exception: \_ -> pure { count: 0 } }
48+ (runLayer {} repeated)
49+ result.count `shouldEqual` 4
test/Test/Yoga/Om/Layer/ScopedSpec.pursnew file+146
@@ -0,0 +1,146 @@
1+module Test.Yoga.Om.Layer.ScopedSpec where
2+
3+import Prelude
4+
5+import Effect.Aff (throwError, try)
6+import Effect.Aff.Class (liftAff)
7+import Effect.Exception (error)
8+import Effect.Class (liftEffect)
9+import Effect.Ref as Ref
10+import Test.Spec (Spec, describe, it)
11+import Test.Spec.Assertions (shouldEqual)
12+import Yoga.Om as Om
13+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, makeScopedLayer, bracketLayer, fresh, combineRequirements, runScoped, withScoped, provide)
14+
15+spec :: Spec Unit
16+spec = describe "Scoped Layers" do
17+
18+ it "runs finalizers in reverse order on success" do
19+ log <- liftEffect $ Ref.new []
20+ let
21+ layerA :: OmLayer (scope :: Scope) () { a :: String }
22+ layerA = makeScopedLayer
23+ (pure { a: "A" })
24+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-A" ]) log)
25+
26+ layerB :: OmLayer (scope :: Scope) () { b :: String }
27+ layerB = makeScopedLayer
28+ (pure { b: "B" })
29+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-B" ]) log)
30+
31+ layerC :: OmLayer (scope :: Scope) () { c :: String }
32+ layerC = makeScopedLayer
33+ (pure { c: "C" })
34+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-C" ]) log)
35+
36+ layerBC :: OmLayer (scope :: Scope) () { b :: String, c :: String }
37+ layerBC = combineRequirements layerB layerC
38+
39+ combined = combineRequirements layerA layerBC
40+
41+ _ <- liftAff $ runScoped combined
42+ finalLog <- liftEffect $ Ref.read log
43+ finalLog `shouldEqual` [ "release-C", "release-B", "release-A" ]
44+
45+ it "runs finalizers when the callback throws" do
46+ log <- liftEffect $ Ref.new []
47+ let
48+ layer :: OmLayer (scope :: Scope) () { value :: String }
49+ layer = makeScopedLayer
50+ (pure { value: "acquired" })
51+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "released" ]) log)
52+
53+ _ <- liftAff $ try $ withScoped layer \_ ->
54+ throwError (error "callback failed")
55+ finalLog <- liftEffect $ Ref.read log
56+ finalLog `shouldEqual` [ "released" ]
57+
58+ it "threads finalizers through vertical composition (provide)" do
59+ log <- liftEffect $ Ref.new []
60+ let
61+ baseLayer :: OmLayer (scope :: Scope) () { base :: String }
62+ baseLayer = makeScopedLayer
63+ (pure { base: "base-value" })
64+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-base" ]) log)
65+
66+ upperLayer :: OmLayer (scope :: Scope, base :: String) () { upper :: String }
67+ upperLayer = makeScopedLayer
68+ ( do
69+ { base } <- Om.ask
70+ pure { upper: base <> "-extended" }
71+ )
72+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-upper" ]) log)
73+
74+ composed = upperLayer `provide` baseLayer
75+
76+ result <- liftAff $ runScoped composed
77+ result.upper `shouldEqual` "base-value-extended"
78+ finalLog <- liftEffect $ Ref.read log
79+ finalLog `shouldEqual` [ "release-upper", "release-base" ]
80+
81+ it "works with bracketLayer" do
82+ log <- liftEffect $ Ref.new []
83+ let
84+ layer = bracketLayer
85+ ( do
86+ liftEffect $ Ref.modify_ (_ <> [ "acquire" ]) log
87+ pure 42
88+ )
89+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release" ]) log)
90+ (\n -> pure { value: n })
91+
92+ result <- liftAff $ runScoped layer
93+ result.value `shouldEqual` 42
94+ finalLog <- liftEffect $ Ref.read log
95+ finalLog `shouldEqual` [ "acquire", "release" ]
96+
97+ it "fresh layer builds twice even when same base layer" do
98+ counter <- liftEffect $ Ref.new 0
99+ let
100+ baseLayer :: OmLayer (scope :: Scope) () { value :: String }
101+ baseLayer = makeLayer do
102+ liftEffect $ Ref.modify_ (_ + 1) counter
103+ pure { value: "built" }
104+
105+ branch1 :: OmLayer (scope :: Scope) () { out1 :: String }
106+ branch1 = consumer `provide` fresh baseLayer
107+ where
108+ consumer :: OmLayer (scope :: Scope, value :: String) () { out1 :: String }
109+ consumer = makeLayer do
110+ { value } <- Om.ask
111+ pure { out1: value <> "-1" }
112+
113+ branch2 :: OmLayer (scope :: Scope) () { out2 :: String }
114+ branch2 = consumer `provide` fresh baseLayer
115+ where
116+ consumer :: OmLayer (scope :: Scope, value :: String) () { out2 :: String }
117+ consumer = makeLayer do
118+ { value } <- Om.ask
119+ pure { out2: value <> "-2" }
120+
121+ app = combineRequirements branch1 branch2
122+
123+ result <- liftAff $ runScoped app
124+ result.out1 `shouldEqual` "built-1"
125+ result.out2 `shouldEqual` "built-2"
126+ count <- liftEffect $ Ref.read counter
127+ count `shouldEqual` 2
128+
129+ it "combines scoped and non-scoped layers" do
130+ log <- liftEffect $ Ref.new []
131+ let
132+ scopedLayer :: OmLayer (scope :: Scope) () { scoped :: String }
133+ scopedLayer = makeScopedLayer
134+ (pure { scoped: "yes" })
135+ (\_ -> liftEffect $ Ref.modify_ (_ <> [ "release-scoped" ]) log)
136+
137+ plainLayer :: OmLayer () () { plain :: String }
138+ plainLayer = makeLayer (pure { plain: "no-finalizer" })
139+
140+ combined = combineRequirements scopedLayer plainLayer
141+
142+ result <- liftAff $ runScoped combined
143+ result.scoped `shouldEqual` "yes"
144+ result.plain `shouldEqual` "no-finalizer"
145+ finalLog <- liftEffect $ Ref.read log
146+ finalLog `shouldEqual` [ "release-scoped" ]
test/Test/Yoga/Om/Layer/WireLayersSpec.pursnew file+133
@@ -0,0 +1,133 @@
1+module Test.Yoga.Om.Layer.WireLayersSpec where
2+
3+import Prelude
4+
5+import Effect.Aff.Class (liftAff)
6+import Effect.Class (liftEffect)
7+import Effect.Ref as Ref
8+import Test.Spec (Spec, describe, it)
9+import Test.Spec.Assertions (shouldEqual)
10+import Yoga.Om as Om
11+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, runScoped, wireLayers, wireLayersDebug)
12+
13+spec :: Spec Unit
14+spec = describe "wireLayers" do
15+
16+ it "wires a simple linear chain (A -> B -> C)" do
17+ let
18+ a_base :: OmLayer (scope :: Scope) () { base :: String }
19+ a_base = makeLayer (pure { base: "foundation" })
20+
21+ b_mid :: OmLayer (scope :: Scope, base :: String) () { mid :: String }
22+ b_mid = makeLayer do
23+ { base } <- Om.ask
24+ pure { mid: base <> "-middle" }
25+
26+ c_top :: OmLayer (scope :: Scope, mid :: String) () { top :: String }
27+ c_top = makeLayer do
28+ { mid } <- Om.ask
29+ pure { top: mid <> "-top" }
30+
31+ app = wireLayers { a_base, b_mid, c_top }
32+
33+ result <- liftAff $ runScoped app
34+ result.base `shouldEqual` "foundation"
35+ result.mid `shouldEqual` "foundation-middle"
36+ result.top `shouldEqual` "foundation-middle-top"
37+
38+ it "wires independent layers with no inter-dependencies" do
39+ let
40+ a_x :: OmLayer (scope :: Scope) () { x :: Int }
41+ a_x = makeLayer (pure { x: 1 })
42+
43+ b_y :: OmLayer (scope :: Scope) () { y :: Int }
44+ b_y = makeLayer (pure { y: 2 })
45+
46+ c_z :: OmLayer (scope :: Scope) () { z :: Int }
47+ c_z = makeLayer (pure { z: 3 })
48+
49+ app = wireLayers { a_x, b_y, c_z }
50+
51+ result <- liftAff $ runScoped app
52+ result.x `shouldEqual` 1
53+ result.y `shouldEqual` 2
54+ result.z `shouldEqual` 3
55+
56+ it "wires a diamond dependency" do
57+ let
58+ a_db :: OmLayer (scope :: Scope) () { db :: String }
59+ a_db = makeLayer (pure { db: "postgres" })
60+
61+ b_cache :: OmLayer (scope :: Scope, db :: String) () { cache :: String }
62+ b_cache = makeLayer do
63+ { db } <- Om.ask
64+ pure { cache: db <> "-cache" }
65+
66+ b_repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
67+ b_repo = makeLayer do
68+ { db } <- Om.ask
69+ pure { repo: db <> "-repo" }
70+
71+ d_app :: OmLayer (scope :: Scope, cache :: String, repo :: String) () { app :: String }
72+ d_app = makeLayer do
73+ { cache, repo } <- Om.ask
74+ pure { app: cache <> "+" <> repo }
75+
76+ app = wireLayers { a_db, b_cache, b_repo, d_app }
77+
78+ result <- liftAff $ runScoped app
79+ result.db `shouldEqual` "postgres"
80+ result.cache `shouldEqual` "postgres-cache"
81+ result.repo `shouldEqual` "postgres-repo"
82+ result.app `shouldEqual` "postgres-cache+postgres-repo"
83+
84+ it "wireLayersDebug emits dependency graph as compiler warning" do
85+ let
86+ a_db :: OmLayer (scope :: Scope) () { db :: String }
87+ a_db = makeLayer (pure { db: "postgres" })
88+
89+ b_cache :: OmLayer (scope :: Scope, db :: String) () { cache :: String }
90+ b_cache = makeLayer do
91+ { db } <- Om.ask
92+ pure { cache: db <> "-cache" }
93+
94+ b_repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
95+ b_repo = makeLayer do
96+ { db } <- Om.ask
97+ pure { repo: db <> "-repo" }
98+
99+ d_app :: OmLayer (scope :: Scope, cache :: String, repo :: String) () { app :: String }
100+ d_app = makeLayer do
101+ { cache, repo } <- Om.ask
102+ pure { app: cache <> "+" <> repo }
103+
104+ app = wireLayersDebug { a_db, b_cache, b_repo, d_app }
105+
106+ result <- liftAff $ runScoped app
107+ result.app `shouldEqual` "postgres-cache+postgres-repo"
108+
109+ it "memoizes shared dependencies within wireLayers" do
110+ counter <- liftEffect $ Ref.new 0
111+ let
112+ a_db :: OmLayer (scope :: Scope) () { db :: String }
113+ a_db = makeLayer do
114+ liftEffect $ Ref.modify_ (_ + 1) counter
115+ pure { db: "connected" }
116+
117+ b_cache :: OmLayer (scope :: Scope, db :: String) () { cache :: String }
118+ b_cache = makeLayer do
119+ { db } <- Om.ask
120+ pure { cache: db <> "-cache" }
121+
122+ b_repo :: OmLayer (scope :: Scope, db :: String) () { repo :: String }
123+ b_repo = makeLayer do
124+ { db } <- Om.ask
125+ pure { repo: db <> "-repo" }
126+
127+ app = wireLayers { a_db, b_cache, b_repo }
128+
129+ result <- liftAff $ runScoped app
130+ result.cache `shouldEqual` "connected-cache"
131+ result.repo `shouldEqual` "connected-repo"
132+ count <- liftEffect $ Ref.read counter
133+ count `shouldEqual` 1