git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
26 files changed+3650−298
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+1265 spago.yaml+25−12 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.locknew file+1265
@@ -0,0 +1,1265 @@
1+{
2+ "workspace": {
3+ "packages": {
4+ "yoga-om-layer": {
5+ "path": "./",
6+ "core": {
7+ "dependencies": [
8+ {
9+ "aff": ">=8.0.0 <9.0.0"
10+ },
11+ {
12+ "aff-retry": ">=2.0.0 <3.0.0"
13+ },
14+ {
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"
43+ },
44+ {
45+ "record": ">=4.0.0 <5.0.0"
46+ },
47+ {
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"
55+ },
56+ {
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"
73+ }
74+ ]
75+ },
76+ "test": {
77+ "dependencies": [
78+ {
79+ "console": ">=6.0.0 <7.0.0"
80+ },
81+ {
82+ "refs": ">=6.0.0 <7.0.0"
83+ },
84+ {
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"
92+ }
93+ ]
94+ }
95+ }
96+ },
97+ "extra_packages": {}
98+ },
99+ "packages": {
100+ "aff": {
101+ "type": "registry",
102+ "version": "8.0.0",
103+ "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
104+ "dependencies": [
105+ "control",
106+ "datetime",
107+ "effect",
108+ "either",
109+ "exceptions",
110+ "functions",
111+ "newtype",
112+ "parallel",
113+ "partial",
114+ "prelude",
115+ "st",
116+ "tailrec",
117+ "transformers",
118+ "unsafe-coerce"
119+ ]
120+ },
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+ },
141+ "ansi": {
142+ "type": "registry",
143+ "version": "7.0.0",
144+ "integrity": "sha256-tOUaXJFzPSfBVAqLI8hc+Tz2M1rg16cXqyHaK/3SmFg=",
145+ "dependencies": [
146+ "foldable-traversable",
147+ "lists",
148+ "prelude"
149+ ]
150+ },
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+ },
198+ "arrays": {
199+ "type": "registry",
200+ "version": "7.3.0",
201+ "integrity": "sha256-GD4z7LCi9wzi7FCQqbWhvs8paoUK9NO+HwgXZ+KP8Rk=",
202+ "dependencies": [
203+ "bifunctors",
204+ "control",
205+ "foldable-traversable",
206+ "functions",
207+ "maybe",
208+ "nonempty",
209+ "partial",
210+ "prelude",
211+ "safe-coerce",
212+ "st",
213+ "tailrec",
214+ "tuples",
215+ "unfoldable",
216+ "unsafe-coerce"
217+ ]
218+ },
219+ "avar": {
220+ "type": "registry",
221+ "version": "5.0.1",
222+ "integrity": "sha256-8V4SxF4TIWZ9Ik1P4lPzIRUAZeFBe8w5WA2VcCEKsIk=",
223+ "dependencies": [
224+ "aff",
225+ "effect",
226+ "either",
227+ "exceptions",
228+ "functions",
229+ "maybe",
230+ "prelude"
231+ ]
232+ },
233+ "bifunctors": {
234+ "type": "registry",
235+ "version": "6.1.0",
236+ "integrity": "sha256-CvZRb8uI75eKIqiYntR+k05Wk6tKlMS5w8sCA2AFna0=",
237+ "dependencies": [
238+ "const",
239+ "either",
240+ "newtype",
241+ "prelude",
242+ "tuples"
243+ ]
244+ },
245+ "catenable-lists": {
246+ "type": "registry",
247+ "version": "7.0.0",
248+ "integrity": "sha256-7+i/MHBLIRh604iCNT4ENWRWE/wuHIotER4kpdX9Ve0=",
249+ "dependencies": [
250+ "control",
251+ "foldable-traversable",
252+ "lists",
253+ "maybe",
254+ "prelude",
255+ "tuples",
256+ "unfoldable"
257+ ]
258+ },
259+ "console": {
260+ "type": "registry",
261+ "version": "6.1.0",
262+ "integrity": "sha256-3T5PPz1mATZmV0C4GzkCkpLlCa5xG7u8ZNgnLrrrI1Q=",
263+ "dependencies": [
264+ "effect",
265+ "prelude"
266+ ]
267+ },
268+ "const": {
269+ "type": "registry",
270+ "version": "6.0.0",
271+ "integrity": "sha256-S+m0lQsxnji0z1XRUmkn+3RFuWJ5CXWuIr1u3i8Bfoc=",
272+ "dependencies": [
273+ "invariant",
274+ "newtype",
275+ "prelude"
276+ ]
277+ },
278+ "contravariant": {
279+ "type": "registry",
280+ "version": "6.0.0",
281+ "integrity": "sha256-nuckEWqL80NaPmR/l9AUyrfycNh18XcvlZV9PAamwqc=",
282+ "dependencies": [
283+ "const",
284+ "either",
285+ "newtype",
286+ "prelude",
287+ "tuples"
288+ ]
289+ },
290+ "control": {
291+ "type": "registry",
292+ "version": "6.0.0",
293+ "integrity": "sha256-0vxyYh5FL+ilxgLo954w1LqCUC0RSIivNNlb9CaNUQg=",
294+ "dependencies": [
295+ "newtype",
296+ "prelude"
297+ ]
298+ },
299+ "datetime": {
300+ "type": "registry",
301+ "version": "6.1.0",
302+ "integrity": "sha256-tbv6eDXy6QOD7YSknxYSbsDF32OC2JrNbSBpzMs7Doc=",
303+ "dependencies": [
304+ "bifunctors",
305+ "control",
306+ "either",
307+ "enums",
308+ "foldable-traversable",
309+ "functions",
310+ "gen",
311+ "integers",
312+ "lists",
313+ "maybe",
314+ "newtype",
315+ "numbers",
316+ "ordered-collections",
317+ "partial",
318+ "prelude",
319+ "tuples"
320+ ]
321+ },
322+ "distributive": {
323+ "type": "registry",
324+ "version": "6.0.0",
325+ "integrity": "sha256-netyJK4B32PDYHD0tF2AenKgo3urzbI770ycM0pArAo=",
326+ "dependencies": [
327+ "identity",
328+ "newtype",
329+ "prelude",
330+ "tuples",
331+ "type-equality"
332+ ]
333+ },
334+ "effect": {
335+ "type": "registry",
336+ "version": "4.0.0",
337+ "integrity": "sha256-DdqU3bncRTjgMVPPRlAuGHVeajSPEcRaRhNuio7if6s=",
338+ "dependencies": [
339+ "prelude"
340+ ]
341+ },
342+ "either": {
343+ "type": "registry",
344+ "version": "6.1.0",
345+ "integrity": "sha256-tBHx3PgtH4GZOApZCDkcM7szRTYJinrUrVGWazQCUUg=",
346+ "dependencies": [
347+ "control",
348+ "invariant",
349+ "maybe",
350+ "prelude"
351+ ]
352+ },
353+ "enums": {
354+ "type": "registry",
355+ "version": "6.0.1",
356+ "integrity": "sha256-sdZOmLX5+5pASGpvVyT0vSD5BBYQjZiayjV5XiPutso=",
357+ "dependencies": [
358+ "control",
359+ "either",
360+ "gen",
361+ "maybe",
362+ "newtype",
363+ "nonempty",
364+ "partial",
365+ "prelude",
366+ "tuples",
367+ "unfoldable"
368+ ]
369+ },
370+ "exceptions": {
371+ "type": "registry",
372+ "version": "6.1.0",
373+ "integrity": "sha256-nRrr+N0wk0TUFOWLR7e1n1oxcXo/X345bQUtoY9lW6A=",
374+ "dependencies": [
375+ "effect",
376+ "either",
377+ "maybe",
378+ "prelude"
379+ ]
380+ },
381+ "exists": {
382+ "type": "registry",
383+ "version": "6.0.0",
384+ "integrity": "sha256-vtLbrWNaI+pzx/2fw2AQnCovoLtcosClIhjO26QKkh8=",
385+ "dependencies": [
386+ "unsafe-coerce"
387+ ]
388+ },
389+ "exitcodes": {
390+ "type": "registry",
391+ "version": "4.0.0",
392+ "integrity": "sha256-xBl4aFSEQ05Owp1ZvO6OxxvLK3LVzU35RSbj79SsePQ=",
393+ "dependencies": [
394+ "enums",
395+ "maybe",
396+ "prelude"
397+ ]
398+ },
399+ "foldable-traversable": {
400+ "type": "registry",
401+ "version": "6.0.0",
402+ "integrity": "sha256-NGGWyCio/xjce6fPP7y3wwg7CheqllID6aJudDxbWhA=",
403+ "dependencies": [
404+ "bifunctors",
405+ "const",
406+ "control",
407+ "either",
408+ "functors",
409+ "identity",
410+ "maybe",
411+ "newtype",
412+ "orders",
413+ "prelude",
414+ "tuples"
415+ ]
416+ },
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+ },
453+ "fork": {
454+ "type": "registry",
455+ "version": "6.0.0",
456+ "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
457+ "dependencies": [
458+ "aff",
459+ "prelude",
460+ "transformers"
461+ ]
462+ },
463+ "free": {
464+ "type": "registry",
465+ "version": "7.1.0",
466+ "integrity": "sha256-tUBInfUpRQEw4u94GWcYW7EYdmSdDHSRn88a+C0eltU=",
467+ "dependencies": [
468+ "catenable-lists",
469+ "control",
470+ "distributive",
471+ "either",
472+ "exists",
473+ "foldable-traversable",
474+ "invariant",
475+ "lazy",
476+ "maybe",
477+ "prelude",
478+ "tailrec",
479+ "transformers",
480+ "tuples",
481+ "unsafe-coerce"
482+ ]
483+ },
484+ "functions": {
485+ "type": "registry",
486+ "version": "6.0.0",
487+ "integrity": "sha256-0jgtOr+SFO4UScKc40oA/9Vdcf2rLn3h3vZlY0KfqVo=",
488+ "dependencies": [
489+ "prelude"
490+ ]
491+ },
492+ "functors": {
493+ "type": "registry",
494+ "version": "5.0.0",
495+ "integrity": "sha256-W1o/4wcp6S3chodYL8uixlXK2mfr1L+SzM1R+whqUco=",
496+ "dependencies": [
497+ "bifunctors",
498+ "const",
499+ "contravariant",
500+ "control",
501+ "distributive",
502+ "either",
503+ "invariant",
504+ "maybe",
505+ "newtype",
506+ "prelude",
507+ "profunctor",
508+ "tuples",
509+ "unsafe-coerce"
510+ ]
511+ },
512+ "gen": {
513+ "type": "registry",
514+ "version": "4.0.0",
515+ "integrity": "sha256-kyOateeOtQa07vfTMYeSuCKdelgKE1R65fwKkZ10wsY=",
516+ "dependencies": [
517+ "either",
518+ "foldable-traversable",
519+ "identity",
520+ "maybe",
521+ "newtype",
522+ "nonempty",
523+ "prelude",
524+ "tailrec",
525+ "tuples",
526+ "unfoldable"
527+ ]
528+ },
529+ "heterogeneous": {
530+ "type": "registry",
531+ "version": "0.7.0",
532+ "integrity": "sha256-pQwcFvhWL8noktxaoD7FlLs2F2lYf3QHoyO0VOWO3I0=",
533+ "dependencies": [
534+ "either",
535+ "foldable-traversable",
536+ "functors",
537+ "prelude",
538+ "record",
539+ "tuples",
540+ "variant"
541+ ]
542+ },
543+ "identity": {
544+ "type": "registry",
545+ "version": "6.0.0",
546+ "integrity": "sha256-RY/iXPpxpqvKHYfJeFVNI1J06kohB9rbtWYuha8t0LE=",
547+ "dependencies": [
548+ "control",
549+ "invariant",
550+ "newtype",
551+ "prelude"
552+ ]
553+ },
554+ "integers": {
555+ "type": "registry",
556+ "version": "6.0.0",
557+ "integrity": "sha256-Y8yozC1uHRLcruobEyv5/bMKzE1BoKJ/olHv6o+bgm8=",
558+ "dependencies": [
559+ "maybe",
560+ "numbers",
561+ "prelude"
562+ ]
563+ },
564+ "invariant": {
565+ "type": "registry",
566+ "version": "6.0.0",
567+ "integrity": "sha256-HyoT1I5qIHRq8RSZji6zZDvqnpVjLw748utrKwZtqsw=",
568+ "dependencies": [
569+ "control",
570+ "prelude"
571+ ]
572+ },
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+ },
588+ "lazy": {
589+ "type": "registry",
590+ "version": "6.0.0",
591+ "integrity": "sha256-MHRC+1Pc+AjortBp4b/e1e6KpEpsDwaJBNBB7TOL+1I=",
592+ "dependencies": [
593+ "control",
594+ "foldable-traversable",
595+ "invariant",
596+ "prelude"
597+ ]
598+ },
599+ "lists": {
600+ "type": "registry",
601+ "version": "7.0.0",
602+ "integrity": "sha256-/mGd7wLoU+wsTcqii7SSUByxsvwvjnu2PjwAD47yYb4=",
603+ "dependencies": [
604+ "bifunctors",
605+ "control",
606+ "foldable-traversable",
607+ "lazy",
608+ "maybe",
609+ "newtype",
610+ "nonempty",
611+ "partial",
612+ "prelude",
613+ "tailrec",
614+ "tuples",
615+ "unfoldable"
616+ ]
617+ },
618+ "maybe": {
619+ "type": "registry",
620+ "version": "6.0.0",
621+ "integrity": "sha256-8X6XEgtZ5lqfkEuc1hWOofnL6mwhk8gSJTxwvbR/SbY=",
622+ "dependencies": [
623+ "control",
624+ "invariant",
625+ "newtype",
626+ "prelude"
627+ ]
628+ },
629+ "mmorph": {
630+ "type": "registry",
631+ "version": "7.0.0",
632+ "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
633+ "dependencies": [
634+ "bifunctors",
635+ "either",
636+ "free",
637+ "functors",
638+ "identity",
639+ "maybe",
640+ "newtype",
641+ "prelude",
642+ "transformers",
643+ "tuples"
644+ ]
645+ },
646+ "newtype": {
647+ "type": "registry",
648+ "version": "5.0.0",
649+ "integrity": "sha256-dt6w0cty4OS+Pjt9fsh3iU6ottqSU83mdAZkaEEvo6c=",
650+ "dependencies": [
651+ "prelude",
652+ "safe-coerce"
653+ ]
654+ },
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+ },
756+ "nonempty": {
757+ "type": "registry",
758+ "version": "7.0.0",
759+ "integrity": "sha256-Ctkq8/+KiGqCE53Y/LvibA/coy7ev9HtRZU+GYS7yfQ=",
760+ "dependencies": [
761+ "control",
762+ "foldable-traversable",
763+ "maybe",
764+ "prelude",
765+ "tuples",
766+ "unfoldable"
767+ ]
768+ },
769+ "now": {
770+ "type": "registry",
771+ "version": "6.0.0",
772+ "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
773+ "dependencies": [
774+ "datetime",
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"
787+ ]
788+ },
789+ "numbers": {
790+ "type": "registry",
791+ "version": "9.0.1",
792+ "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
793+ "dependencies": [
794+ "functions",
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"
847+ ]
848+ },
849+ "ordered-collections": {
850+ "type": "registry",
851+ "version": "3.2.0",
852+ "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
853+ "dependencies": [
854+ "arrays",
855+ "control",
856+ "foldable-traversable",
857+ "functions",
858+ "gen",
859+ "lists",
860+ "maybe",
861+ "newtype",
862+ "partial",
863+ "prelude",
864+ "safe-coerce",
865+ "tailrec",
866+ "tuples",
867+ "unfoldable"
868+ ]
869+ },
870+ "orders": {
871+ "type": "registry",
872+ "version": "6.0.0",
873+ "integrity": "sha256-nSMxZK7wxyDULkjZqvMyB71mVo6b0AyLLHosDVbv1XM=",
874+ "dependencies": [
875+ "newtype",
876+ "prelude"
877+ ]
878+ },
879+ "parallel": {
880+ "type": "registry",
881+ "version": "7.0.0",
882+ "integrity": "sha256-bAGpVhrFz2IyHkA4MR7+tE/19FZ7dzYBdYPgK5svkqw=",
883+ "dependencies": [
884+ "control",
885+ "effect",
886+ "either",
887+ "foldable-traversable",
888+ "functors",
889+ "maybe",
890+ "newtype",
891+ "prelude",
892+ "profunctor",
893+ "refs",
894+ "transformers"
895+ ]
896+ },
897+ "partial": {
898+ "type": "registry",
899+ "version": "4.0.0",
900+ "integrity": "sha256-mdrlJBAgFMh79hNRW39uv7c+BwnnaOrAMmdh7+VhEhs=",
901+ "dependencies": []
902+ },
903+ "pipes": {
904+ "type": "registry",
905+ "version": "8.0.0",
906+ "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
907+ "dependencies": [
908+ "aff",
909+ "control",
910+ "effect",
911+ "either",
912+ "foldable-traversable",
913+ "identity",
914+ "lists",
915+ "maybe",
916+ "mmorph",
917+ "newtype",
918+ "prelude",
919+ "tailrec",
920+ "transformers",
921+ "tuples"
922+ ]
923+ },
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+ },
934+ "prelude": {
935+ "type": "registry",
936+ "version": "6.0.2",
937+ "integrity": "sha256-IqykrDRquGUD/LRSad7y75hZv3bUsGQ4knc8tdez8Qw=",
938+ "dependencies": []
939+ },
940+ "profunctor": {
941+ "type": "registry",
942+ "version": "6.0.1",
943+ "integrity": "sha256-Ow0mJC+PIIRq/a3mzCWY5vxuTgR/e+Ee68+mWeSRnWY=",
944+ "dependencies": [
945+ "control",
946+ "distributive",
947+ "either",
948+ "exists",
949+ "invariant",
950+ "newtype",
951+ "prelude",
952+ "tuples"
953+ ]
954+ },
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+ },
965+ "record": {
966+ "type": "registry",
967+ "version": "4.0.0",
968+ "integrity": "sha256-WQ8fqp4X1wTA70J4qUcPW4Z7DCwx6e5KJnT/EuTdWSc=",
969+ "dependencies": [
970+ "functions",
971+ "prelude",
972+ "unsafe-coerce"
973+ ]
974+ },
975+ "record-studio": {
976+ "type": "registry",
977+ "version": "1.0.4",
978+ "integrity": "sha256-CRnFvSXpDDzio+buibUxFU9zPZDd7/lzrYC1Ya3FuIw=",
979+ "dependencies": [
980+ "heterogeneous",
981+ "lists",
982+ "prelude",
983+ "record",
984+ "typelevel-prelude",
985+ "unsafe-coerce"
986+ ]
987+ },
988+ "refs": {
989+ "type": "registry",
990+ "version": "6.0.0",
991+ "integrity": "sha256-KKD5G9dR2SauuDGULUJsxjqDOEi1Jqm+2Sq5U2mY8YU=",
992+ "dependencies": [
993+ "effect",
994+ "prelude"
995+ ]
996+ },
997+ "safe-coerce": {
998+ "type": "registry",
999+ "version": "2.0.0",
1000+ "integrity": "sha256-EJrxtKt5xC7TYjBZrSBO/J7Aw3DmtilYjt4olpAXWVc=",
1001+ "dependencies": [
1002+ "unsafe-coerce"
1003+ ]
1004+ },
1005+ "spec": {
1006+ "type": "registry",
1007+ "version": "8.1.2",
1008+ "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=",
1009+ "dependencies": [
1010+ "aff",
1011+ "ansi",
1012+ "arrays",
1013+ "avar",
1014+ "bifunctors",
1015+ "control",
1016+ "datetime",
1017+ "effect",
1018+ "either",
1019+ "exceptions",
1020+ "foldable-traversable",
1021+ "fork",
1022+ "identity",
1023+ "integers",
1024+ "lists",
1025+ "maybe",
1026+ "newtype",
1027+ "now",
1028+ "ordered-collections",
1029+ "parallel",
1030+ "pipes",
1031+ "prelude",
1032+ "refs",
1033+ "strings",
1034+ "tailrec",
1035+ "transformers",
1036+ "tuples"
1037+ ]
1038+ },
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+ },
1084+ "st": {
1085+ "type": "registry",
1086+ "version": "6.2.0",
1087+ "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
1088+ "dependencies": [
1089+ "effect",
1090+ "partial",
1091+ "prelude",
1092+ "tailrec",
1093+ "unsafe-coerce"
1094+ ]
1095+ },
1096+ "strings": {
1097+ "type": "registry",
1098+ "version": "6.0.1",
1099+ "integrity": "sha256-FNto2hoNW5Da6W6crIk77YXMagBvOGHZE1kO7eZ1vLM=",
1100+ "dependencies": [
1101+ "arrays",
1102+ "control",
1103+ "either",
1104+ "enums",
1105+ "foldable-traversable",
1106+ "gen",
1107+ "integers",
1108+ "maybe",
1109+ "newtype",
1110+ "nonempty",
1111+ "partial",
1112+ "prelude",
1113+ "tailrec",
1114+ "tuples",
1115+ "unfoldable",
1116+ "unsafe-coerce"
1117+ ]
1118+ },
1119+ "tailrec": {
1120+ "type": "registry",
1121+ "version": "6.1.0",
1122+ "integrity": "sha256-xrorCfP0xV1wubs27idQCHQqdvSUuqmmL/am7Ji1wVU=",
1123+ "dependencies": [
1124+ "bifunctors",
1125+ "effect",
1126+ "either",
1127+ "identity",
1128+ "maybe",
1129+ "partial",
1130+ "prelude",
1131+ "refs"
1132+ ]
1133+ },
1134+ "transformers": {
1135+ "type": "registry",
1136+ "version": "6.1.0",
1137+ "integrity": "sha256-QJmgNT/y7ljPHSsXaW4zxF/982BFiQ90KNL1g/NtEOQ=",
1138+ "dependencies": [
1139+ "control",
1140+ "distributive",
1141+ "effect",
1142+ "either",
1143+ "exceptions",
1144+ "foldable-traversable",
1145+ "identity",
1146+ "lazy",
1147+ "maybe",
1148+ "newtype",
1149+ "prelude",
1150+ "st",
1151+ "tailrec",
1152+ "tuples",
1153+ "unfoldable"
1154+ ]
1155+ },
1156+ "tuples": {
1157+ "type": "registry",
1158+ "version": "7.0.0",
1159+ "integrity": "sha256-BKy7EfK8S1KeTCwyWqyW1wwcpSAQuh3IsczSVgS6fzY=",
1160+ "dependencies": [
1161+ "control",
1162+ "invariant",
1163+ "prelude"
1164+ ]
1165+ },
1166+ "type-equality": {
1167+ "type": "registry",
1168+ "version": "4.0.1",
1169+ "integrity": "sha256-BBqYOSnAKmayRvR5mtZoCf4SlYs2ipatD/+5nQx73aw=",
1170+ "dependencies": []
1171+ },
1172+ "typelevel-prelude": {
1173+ "type": "registry",
1174+ "version": "7.0.0",
1175+ "integrity": "sha256-+pHi14/40mTj/+lmCWqL0T7iXIXD1+NSg/KItyyoB0A=",
1176+ "dependencies": [
1177+ "prelude",
1178+ "type-equality"
1179+ ]
1180+ },
1181+ "uncurried-transformers": {
1182+ "type": "registry",
1183+ "version": "1.1.0",
1184+ "integrity": "sha256-AqqBMzVLzozVoDorE3QcVVRDkLd7O3624mRHCDnPhaI=",
1185+ "dependencies": [
1186+ "control",
1187+ "effect",
1188+ "either",
1189+ "functions",
1190+ "identity",
1191+ "prelude",
1192+ "safe-coerce",
1193+ "tailrec",
1194+ "transformers",
1195+ "tuples"
1196+ ]
1197+ },
1198+ "unfoldable": {
1199+ "type": "registry",
1200+ "version": "6.0.0",
1201+ "integrity": "sha256-BK/6By1sKp/ztk+9CF3q43SV0QDTLVdsGqFerKHSmbg=",
1202+ "dependencies": [
1203+ "foldable-traversable",
1204+ "maybe",
1205+ "partial",
1206+ "prelude",
1207+ "tuples"
1208+ ]
1209+ },
1210+ "unsafe-coerce": {
1211+ "type": "registry",
1212+ "version": "6.0.0",
1213+ "integrity": "sha256-0L1QsaY20OILjfU5TV72d3U/tSjhmL9hJ32WMp857Lk=",
1214+ "dependencies": []
1215+ },
1216+ "variant": {
1217+ "type": "registry",
1218+ "version": "8.0.0",
1219+ "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1220+ "dependencies": [
1221+ "control",
1222+ "enums",
1223+ "foldable-traversable",
1224+ "lists",
1225+ "maybe",
1226+ "partial",
1227+ "prelude",
1228+ "type-equality",
1229+ "unsafe-coerce"
1230+ ]
1231+ },
1232+ "yoga-om": {
1233+ "type": "registry",
1234+ "version": "2.0.0",
1235+ "integrity": "sha256-eenoujD2rJzR6gYJbbIiqLhXHC1C5dagu91HZkAEt/Y=",
1236+ "dependencies": [
1237+ "aff",
1238+ "aff-retry",
1239+ "avar",
1240+ "console",
1241+ "control",
1242+ "datetime",
1243+ "effect",
1244+ "either",
1245+ "exceptions",
1246+ "functions",
1247+ "heterogeneous",
1248+ "maybe",
1249+ "newtype",
1250+ "parallel",
1251+ "prelude",
1252+ "record",
1253+ "record-studio",
1254+ "refs",
1255+ "tailrec",
1256+ "transformers",
1257+ "tuples",
1258+ "typelevel-prelude",
1259+ "uncurried-transformers",
1260+ "unsafe-coerce",
1261+ "variant"
1262+ ]
1263+ }
1264+ }
1265+}
spago.yaml+25−12
@@ -1,27 +1,40 @@
1−workspace:
2− package_set:
3− registry: 51.1.0
4−
1+workspace: {}
52package:
63 name: yoga-om-layer
74 publish:
8− version: 1.0.3
5+ version: 2.1.0
96 license: MIT
107 location:
118 githubOwner: rowtype-yoga
129 githubRepo: purescript-yoga-om-layer
1310 dependencies:
14− - yoga-om-core: ">=1.0.0 <2.0.0"
15− - 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"
1623 - record: ">=4.0.0 <5.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"
1729 - typelevel-prelude: ">=7.0.0 <8.0.0"
18− - type-equality: ">=4.0.0 <5.0.0"
19− - effect: ">=4.0.0 <5.0.0"
20− - aff: ">=7.0.0 <8.0.0"
21− - newtype: ">=5.0.0 <6.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"
2233 test:
2334 main: Test.Main
2435 dependencies:
25− - 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"
2639 - console: ">=6.0.0 <7.0.0"
2740 - 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