git.iko.soy/rowtype-yoga/purescript-yoga-om-layer
on main help sign in
.psc-ide-portnew file+1
since splitsnapshots
unifiedsplit
@@ -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
240
240
241**Status**: Proof of concept complete ✓ with working runtime implementation
241**Status**: Proof of concept complete ✓ with working runtime implementation
242**Next**: Implement full instance-resolution based layer system with provideLayers API
242**Next**: Implement full instance-resolution based layer system with provideLayers API
243+
compile-fail-tests/CombineDuplicateProv.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: TypesDoNotUnify
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, combineRequirements, runScoped)
6+
7+layer1 :: OmLayer (scope :: Scope) () { value :: Int }
8+layer1 = makeLayer (pure { value: 1 })
9+
10+layer2 :: OmLayer (scope :: Scope) () { value :: Int }
11+layer2 = makeLayer (pure { value: 2 })
12+
13+bad = runScoped (combineRequirements layer1 layer2)
compile-fail-tests/CombineOverlappingProv.pursnew file+15
@@ -0,0 +1,15 @@
1+-- EXPECT: TypesDoNotUnify
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, combineRequirements, runScoped)
6+import Effect.Aff (Aff)
7+
8+layer1 :: OmLayer (scope :: Scope) () { value :: Int }
9+layer1 = makeLayer (pure { value: 1 })
10+
11+layer2 :: OmLayer (scope :: Scope) () { value :: String }
12+layer2 = makeLayer (pure { value: "two" })
13+
14+bad :: Aff { value :: Int, value :: String }
15+bad = runScoped (combineRequirements layer1 layer2)
compile-fail-tests/ProvideWrongType.pursnew file+16
@@ -0,0 +1,16 @@
1+-- EXPECT: TypesDoNotUnify
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, makeLayer, provide)
7+
8+base :: OmLayer () () { value :: Int }
9+base = makeLayer (pure { value: 42 })
10+
11+upper :: OmLayer (value :: String) () { out :: String }
12+upper = makeLayer do
13+ { value } <- Om.ask
14+ pure { out: value }
15+
16+bad = upper `provide` base
compile-fail-tests/RunLayerMissingDep.pursnew file+15
@@ -0,0 +1,15 @@
1+-- EXPECT: Missing required dependency
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
7+
8+type Database = { query :: String -> Array String }
9+
10+dbLayer :: OmLayer (config :: { host :: String }) () { db :: Database }
11+dbLayer = makeLayer do
12+ { config } <- Om.ask
13+ pure { db: { query: \_ -> [ config.host ] } }
14+
15+bad = runLayer {} dbLayer
compile-fail-tests/RunLayerOpenRowMissingDep.pursnew file+19
@@ -0,0 +1,19 @@
1+-- EXPECT: Prim.RowList.RowToList
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+
6+import Yoga.Om as Om
7+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
8+
9+type Fastify = String
10+type Connection = String
11+
12+apiLayer :: forall r. OmLayer (fastify :: Fastify, sqlite :: Connection | r) () {}
13+apiLayer = makeLayer do
14+ { fastify, sqlite } <- Om.ask
15+ let _ = fastify <> sqlite
16+ pure {}
17+
18+bad :: Om.Om { fastify :: Fastify } () {}
19+bad = runLayer { fastify: "server" } apiLayer
compile-fail-tests/RunLayerPartialDep.pursnew file+13
@@ -0,0 +1,13 @@
1+-- EXPECT: Missing required dependency
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
7+
8+layer :: OmLayer (a :: Int, b :: String) () { out :: String }
9+layer = makeLayer do
10+ { a, b } <- Om.ask
11+ pure { out: show a <> b }
12+
13+bad = runLayer { a: 42 } layer
compile-fail-tests/WireLayersMissingDep.pursnew file+16
@@ -0,0 +1,16 @@
1+-- EXPECT: Dependency not provided!
2+module Yoga.Om.Layer.CompileFailTest where
3+
4+import Prelude
5+import Yoga.Om as Om
6+import Yoga.Om.Layer (OmLayer, Scope, makeLayer, wireLayers)
7+
8+a_base :: OmLayer (scope :: Scope) () { base :: String }
9+a_base = makeLayer (pure { base: "ok" })
10+
11+b_upper :: OmLayer (scope :: Scope, port :: Int) () { upper :: String }
12+b_upper = makeLayer do
13+ { port } <- Om.ask
14+ pure { upper: show port }
15+
16+bad = wireLayers { a_base, b_upper }
compile-fail-tests/run.shnew file+38
@@ -0,0 +1,38 @@
1+#!/bin/bash
2+cd "$(dirname "$0")/.."
3+
4+# Build the library first
5+bunx spago build > /dev/null 2>&1
6+
7+PASS=0
8+FAIL=0
9+TOTAL=0
10+
11+for f in compile-fail-tests/*.purs; do
12+ EXPECT=$(head -1 "$f" | sed 's/-- EXPECT: //')
13+ NAME=$(basename "$f" .purs)
14+ TOTAL=$((TOTAL + 1))
15+
16+ cp "$f" "src/_CompileFailTest.purs"
17+ OUTPUT=$(bunx spago build 2>&1) || true
18+ rm -f "src/_CompileFailTest.purs"
19+
20+ if echo "$OUTPUT" | grep -q "Build succeeded"; then
21+ echo "FAIL $NAME — compiled successfully (should have failed)"
22+ FAIL=$((FAIL + 1))
23+ elif echo "$OUTPUT" | grep -q "$EXPECT"; then
24+ echo "PASS $NAME — failed with expected: $EXPECT"
25+ PASS=$((PASS + 1))
26+ else
27+ echo "FAIL $NAME — failed but without expected '$EXPECT'"
28+ echo " Got: $(echo "$OUTPUT" | grep "ERROR\|error" | head -3)"
29+ FAIL=$((FAIL + 1))
30+ fi
31+done
32+
33+echo ""
34+echo "$PASS passed, $FAIL failed out of $TOTAL compile-fail tests"
35+
36+if [ "$FAIL" -ne 0 ]; then
37+ exit 1
38+fi
spago.lock+458−721
@@ -6,86 +6,71 @@
6 "core": {
6 "core": {
7 "dependencies": [
7 "dependencies": [
8 {
8 {
9− "aff": ">=7.0.0 <8.0.0"
9+ "aff": ">=8.0.0 <9.0.0"
10 },
10 },
11 {
11 {
12− "newtype": ">=5.0.0 <6.0.0"
12+ "aff-retry": ">=2.0.0 <3.0.0"
13 },
13 },
14 {
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+ {
15− "prelude": ">=6.0.0 <7.0.0"
42+ "prelude": ">=6.0.2 <7.0.0"
16 },
43 },
17 {
44 {
18 "record": ">=4.0.0 <5.0.0"
45 "record": ">=4.0.0 <5.0.0"
19 },
46 },
20− "record-studio",
21 {
47 {
22− "type-equality": ">=4.0.0 <5.0.0"
48+ "record-studio": ">=1.0.4 <2.0.0"
49+ },
50+ {
51+ "refs": ">=6.0.0 <7.0.0"
52+ },
53+ {
54+ "tailrec": ">=6.1.0 <7.0.0"
23 },
55 },
24− "unsafe-coerce",
25 {
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+ {
26− "yoga-om-core": ">=1.0.0 <2.0.0"
72+ "yoga-om": ">=2.0.0 <3.0.0"
27 }
73 }
28− ],
29− "build_plan": [
30− "aff",
31− "ansi",
32− "arrays",
33− "avar",
34− "bifunctors",
35− "catenable-lists",
36− "console",
37− "const",
38− "contravariant",
39− "control",
40− "datetime",
41− "distributive",
42− "effect",
43− "either",
44− "enums",
45− "exceptions",
46− "exists",
47− "foldable-traversable",
48− "fork",
49− "free",
50− "functions",
51− "functors",
52− "gen",
53− "heterogeneous",
54− "identity",
55− "integers",
56− "invariant",
57− "lazy",
58− "lists",
59− "maybe",
60− "mmorph",
61− "newtype",
62− "nonempty",
63− "now",
64− "numbers",
65− "ordered-collections",
66− "orders",
67− "parallel",
68− "partial",
69− "pipes",
70− "prelude",
71− "profunctor",
72− "record",
73− "record-studio",
74− "refs",
75− "safe-coerce",
76− "spec",
77− "st",
78− "strings",
79− "tailrec",
80− "transformers",
81− "tuples",
82− "type-equality",
83− "typelevel-prelude",
84− "uncurried-transformers",
85− "unfoldable",
86− "unsafe-coerce",
87− "variant",
88− "yoga-om-core"
89 ]
74 ]
90 },
75 },
91 "test": {
76 "test": {
@@ -97,645 +82,19 @@
97 "refs": ">=6.0.0 <7.0.0"
82 "refs": ">=6.0.0 <7.0.0"
98 },
83 },
99 {
84 {
100− "spec": ">=7.0.0 <8.0.0"
85+ "spec": ">=7.0.0 <9.0.0"
86+ },
87+ {
88+ "spec-discovery": ">=8.0.0 <9.0.0"
89+ },
90+ {
91+ "spec-node": ">=0.0.3 <1.0.0"
101 }
92 }
102− ],
103− "build_plan": [
104− "aff",
105− "ansi",
106− "arrays",
107− "avar",
108− "bifunctors",
109− "catenable-lists",
110− "console",
111− "const",
112− "contravariant",
113− "control",
114− "datetime",
115− "distributive",
116− "effect",
117− "either",
118− "enums",
119− "exceptions",
120− "exists",
121− "foldable-traversable",
122− "fork",
123− "free",
124− "functions",
125− "functors",
126− "gen",
127− "identity",
128− "integers",
129− "invariant",
130− "lazy",
131− "lists",
132− "maybe",
133− "mmorph",
134− "newtype",
135− "nonempty",
136− "now",
137− "numbers",
138− "ordered-collections",
139− "orders",
140− "parallel",
141− "partial",
142− "pipes",
143− "prelude",
144− "profunctor",
145− "refs",
146− "safe-coerce",
147− "spec",
148− "st",
149− "strings",
150− "tailrec",
151− "transformers",
152− "tuples",
153− "type-equality",
154− "unfoldable",
155− "unsafe-coerce"
156 ]
93 ]
157 }
94 }
158 }
95 }
159 },
96 },
160− "package_set": {
97+ "extra_packages": {}
161− "address": {
162− "registry": "72.0.1"
163− },
164− "compiler": ">=0.15.15 <0.16.0",
165− "content": {
166− "abc-parser": "2.1.0",
167− "ace": "9.1.0",
168− "address-rfc2821": "0.1.1",
169− "aff": "8.0.0",
170− "aff-bus": "6.0.0",
171− "aff-coroutines": "9.0.0",
172− "aff-promise": "4.0.0",
173− "aff-retry": "2.0.0",
174− "affjax": "13.0.0",
175− "affjax-node": "1.0.0",
176− "affjax-web": "1.0.0",
177− "ansi": "7.0.0",
178− "apexcharts": "0.5.0",
179− "applicative-phases": "1.0.0",
180− "argonaut": "9.0.0",
181− "argonaut-aeson-generic": "0.4.1",
182− "argonaut-codecs": "9.1.0",
183− "argonaut-core": "7.0.0",
184− "argonaut-generic": "8.0.0",
185− "argonaut-traversals": "10.0.0",
186− "argparse-basic": "2.0.0",
187− "array-builder": "0.1.2",
188− "array-search": "0.6.0",
189− "arraybuffer": "13.2.0",
190− "arraybuffer-builder": "3.1.0",
191− "arraybuffer-types": "3.0.2",
192− "arrays": "7.3.0",
193− "arrays-extra": "0.6.1",
194− "arrays-zipper": "2.0.1",
195− "ask": "1.0.0",
196− "assert": "6.0.0",
197− "assert-multiple": "0.4.0",
198− "avar": "5.0.1",
199− "axon": "0.0.3",
200− "b64": "0.0.8",
201− "barbies": "1.0.1",
202− "barlow-lens": "0.9.0",
203− "benchlib": "0.0.4",
204− "bifunctors": "6.1.0",
205− "bigints": "7.0.1",
206− "blessed": "1.0.0",
207− "bolson": "0.3.9",
208− "bookhound": "0.1.7",
209− "bower-json": "3.0.0",
210− "call-by-name": "4.0.1",
211− "canvas": "6.0.0",
212− "canvas-action": "9.0.0",
213− "cartesian": "1.0.6",
214− "catenable-lists": "7.0.0",
215− "cbor-stream": "1.3.0",
216− "chameleon": "1.0.0",
217− "chameleon-halogen": "1.0.3",
218− "chameleon-react-basic": "1.1.0",
219− "chameleon-styled": "2.5.0",
220− "chameleon-transformers": "1.0.0",
221− "channel": "1.0.0",
222− "checked-exceptions": "3.1.1",
223− "choku": "1.0.2",
224− "classless": "0.1.1",
225− "classless-arbitrary": "0.1.1",
226− "classless-decode-json": "0.1.1",
227− "classless-encode-json": "0.1.3",
228− "classnames": "2.0.0",
229− "codec": "6.1.0",
230− "codec-argonaut": "10.0.0",
231− "codec-json": "2.0.0",
232− "colors": "7.0.1",
233− "concur-core": "0.5.0",
234− "concur-react": "0.5.0",
235− "concurrent-queues": "3.0.0",
236− "console": "6.1.0",
237− "const": "6.0.0",
238− "contravariant": "6.0.0",
239− "control": "6.0.0",
240− "convertable-options": "1.0.0",
241− "coroutines": "7.0.0",
242− "css": "6.0.0",
243− "css-class-name-extractor": "0.0.4",
244− "css-frameworks": "1.1.0",
245− "csv-stream": "2.3.0",
246− "data-mvc": "0.0.2",
247− "datetime": "6.1.0",
248− "datetime-parsing": "0.2.0",
249− "debounce": "0.1.0",
250− "debug": "6.0.2",
251− "decimals": "7.1.0",
252− "default-values": "1.0.1",
253− "deku": "0.9.23",
254− "deno": "0.0.5",
255− "dissect": "1.0.0",
256− "distributive": "6.0.0",
257− "dodo-printer": "2.2.3",
258− "dom-filereader": "7.0.0",
259− "dom-indexed": "13.0.0",
260− "dom-simple": "0.4.0",
261− "dotenv": "4.0.3",
262− "droplet": "0.6.0",
263− "dts": "1.0.0",
264− "dual-numbers": "1.0.3",
265− "dynamic-buffer": "3.0.1",
266− "echarts-simple": "0.0.1",
267− "effect": "4.0.0",
268− "either": "6.1.0",
269− "elmish": "0.13.0",
270− "elmish-enzyme": "0.1.1",
271− "elmish-hooks": "0.11.0",
272− "elmish-html": "0.10.0",
273− "elmish-testing-library": "0.3.2",
274− "elmish-time-machine": "0.4.2",
275− "email-validate": "7.0.0",
276− "encoding": "0.0.9",
277− "enums": "6.0.1",
278− "env-names": "0.4.0",
279− "environment": "2.0.0",
280− "error": "2.0.0",
281− "eta-conversion": "0.3.2",
282− "exceptions": "6.1.0",
283− "exists": "6.0.0",
284− "exitcodes": "4.0.0",
285− "expect-inferred": "3.0.0",
286− "express": "0.9.1",
287− "ezfetch": "1.1.1",
288− "fahrtwind": "2.0.0",
289− "fakerjs": "0.0.1",
290− "fallback": "0.1.0",
291− "fast-vect": "1.2.0",
292− "fetch": "4.1.0",
293− "fetch-argonaut": "1.0.1",
294− "fetch-core": "5.1.0",
295− "fetch-yoga-json": "1.1.0",
296− "ffi-simple": "0.5.1",
297− "fft": "0.3.0",
298− "fft-js": "0.1.0",
299− "filterable": "5.0.0",
300− "fix-functor": "0.1.0",
301− "fixed-points": "7.0.0",
302− "fixed-precision": "5.0.0",
303− "flame": "1.6.0",
304− "float32": "2.0.0",
305− "fmt": "0.2.1",
306− "foldable-traversable": "6.0.0",
307− "foldable-traversable-extra": "0.0.6",
308− "foreign": "7.0.0",
309− "foreign-object": "4.1.0",
310− "foreign-readwrite": "3.4.0",
311− "forgetmenot": "0.1.0",
312− "fork": "6.0.0",
313− "form-urlencoded": "7.0.0",
314− "formatters": "7.0.0",
315− "framer-motion": "2.0.1",
316− "free": "7.1.0",
317− "freeap": "7.0.0",
318− "freer-free": "0.0.1",
319− "freet": "7.0.0",
320− "functions": "6.0.0",
321− "functor1": "3.0.0",
322− "functors": "5.0.0",
323− "fuzzy": "0.4.0",
324− "gen": "4.0.0",
325− "generate-values": "1.0.1",
326− "generic-router": "0.0.1",
327− "geojson": "0.0.5",
328− "geometria": "2.2.0",
329− "gesso": "1.0.1",
330− "gojs": "0.1.1",
331− "golem-fetch": "0.1.0",
332− "grain": "3.0.0",
333− "grain-router": "3.0.0",
334− "grain-virtualized": "3.0.0",
335− "graphql-client": "10.1.1",
336− "graphs": "8.1.0",
337− "group": "4.1.1",
338− "halogen": "7.0.0",
339− "halogen-bootstrap5": "5.3.2",
340− "halogen-canvas": "1.0.0",
341− "halogen-css": "10.0.0",
342− "halogen-declarative-canvas": "0.0.8",
343− "halogen-echarts-simple": "0.0.4",
344− "halogen-formless": "4.0.3",
345− "halogen-helix": "1.1.0",
346− "halogen-hooks": "0.6.3",
347− "halogen-hooks-extra": "0.9.0",
348− "halogen-infinite-scroll": "1.1.0",
349− "halogen-store": "0.5.4",
350− "halogen-storybook": "2.0.0",
351− "halogen-subscriptions": "2.0.0",
352− "halogen-svg-elems": "8.0.0",
353− "halogen-typewriter": "1.0.4",
354− "halogen-use-trigger-hooks": "1.0.0",
355− "halogen-vdom": "8.0.0",
356− "halogen-vdom-string-renderer": "0.5.0",
357− "halogen-xterm": "2.0.0",
358− "heckin": "2.0.1",
359− "heterogeneous": "0.7.0",
360− "homogeneous": "0.4.0",
361− "http-methods": "6.0.0",
362− "httpurple": "4.0.0",
363− "huffman": "0.4.0",
364− "humdrum": "0.0.1",
365− "hyrule": "2.3.8",
366− "identity": "6.0.0",
367− "identy": "4.0.1",
368− "indexed-db": "1.0.0",
369− "indexed-monad": "3.0.0",
370− "ink": "0.0.1",
371− "int64": "3.0.0",
372− "integers": "6.0.0",
373− "interpolate": "5.0.2",
374− "intersection-observer": "1.0.1",
375− "invariant": "6.0.0",
376− "jarilo": "1.0.1",
377− "jelly": "0.10.0",
378− "jelly-router": "0.3.0",
379− "jelly-signal": "0.4.0",
380− "jest": "1.0.0",
381− "js-abort-controller": "1.0.0",
382− "js-bigints": "2.2.1",
383− "js-date": "8.0.0",
384− "js-fetch": "0.2.1",
385− "js-fileio": "3.0.0",
386− "js-intl": "1.1.4",
387− "js-iterators": "0.1.1",
388− "js-maps": "0.1.2",
389− "js-promise": "1.0.0",
390− "js-promise-aff": "1.0.0",
391− "js-timers": "6.1.0",
392− "js-uri": "3.1.0",
393− "jsdom": "1.0.0",
394− "json": "1.1.0",
395− "json-codecs": "5.0.0",
396− "justifill": "0.5.0",
397− "jwt": "0.0.9",
398− "labeled-data": "0.2.0",
399− "language-cst-parser": "0.14.1",
400− "lazy": "6.0.0",
401− "lazy-joe": "1.0.0",
402− "lcg": "4.0.0",
403− "leibniz": "5.0.0",
404− "leveldb": "1.0.1",
405− "liminal": "1.0.1",
406− "linalg": "6.0.0",
407− "lists": "7.0.0",
408− "literals": "1.0.2",
409− "logging": "3.0.0",
410− "logging-journald": "0.4.0",
411− "lumi-components": "18.0.0",
412− "machines": "7.0.0",
413− "maps-eager": "0.5.0",
414− "marionette": "1.0.0",
415− "marionette-react-basic-hooks": "0.1.1",
416− "marked": "0.1.0",
417− "matrices": "5.0.1",
418− "matryoshka": "1.0.0",
419− "maybe": "6.0.0",
420− "media-types": "6.0.0",
421− "meowclient": "1.0.0",
422− "midi": "4.0.0",
423− "milkis": "9.0.0",
424− "mimetype": "0.0.1",
425− "minibench": "4.0.1",
426− "mmorph": "7.0.0",
427− "monad-control": "5.0.0",
428− "monad-logger": "1.3.1",
429− "monad-loops": "0.5.0",
430− "monad-unlift": "1.0.1",
431− "monoid-extras": "0.0.1",
432− "monoidal": "0.16.0",
433− "morello": "0.4.0",
434− "mote": "3.0.0",
435− "motsunabe": "2.0.0",
436− "mvc": "0.0.1",
437− "mysql": "6.0.1",
438− "n3": "0.1.0",
439− "nano-id": "1.1.0",
440− "nanoid": "0.1.0",
441− "naturals": "3.0.0",
442− "nested-functor": "0.2.1",
443− "newtype": "5.0.0",
444− "nextjs": "0.1.1",
445− "nextui": "0.2.0",
446− "node-buffer": "9.0.0",
447− "node-child-process": "11.1.0",
448− "node-event-emitter": "3.0.0",
449− "node-execa": "5.0.0",
450− "node-fs": "9.2.0",
451− "node-glob-basic": "2.0.0",
452− "node-http": "9.1.0",
453− "node-http2": "1.1.1",
454− "node-human-signals": "1.0.0",
455− "node-net": "5.1.0",
456− "node-os": "5.1.0",
457− "node-path": "5.0.1",
458− "node-process": "11.2.0",
459− "node-readline": "8.1.1",
460− "node-sqlite3": "8.0.0",
461− "node-stream-pipes": "2.1.6",
462− "node-streams": "9.0.1",
463− "node-tls": "0.3.1",
464− "node-url": "7.0.1",
465− "node-workerbees": "0.3.1",
466− "node-zlib": "0.4.0",
467− "nonempty": "7.0.0",
468− "now": "6.0.0",
469− "npm-package-json": "2.0.0",
470− "nullable": "6.0.0",
471− "numberfield": "0.2.2",
472− "numbers": "9.0.1",
473− "oak": "3.1.1",
474− "oak-debug": "1.2.2",
475− "object-maps": "0.3.0",
476− "ocarina": "1.5.4",
477− "oooooooooorrrrrrrmm-lib": "0.0.1",
478− "open-colors-scales-and-schemes": "1.0.0",
479− "open-drawing": "6.0.4",
480− "open-folds": "6.4.0",
481− "open-foreign-generic": "11.0.3",
482− "open-memoize": "6.2.0",
483− "open-mkdirp-aff": "1.2.0",
484− "open-pairing": "6.2.0",
485− "open-smolder": "12.0.2",
486− "options": "7.0.0",
487− "optparse": "5.0.1",
488− "ordered-collections": "3.2.0",
489− "ordered-set": "0.4.0",
490− "orders": "6.0.0",
491− "org-doc": "0.1.0",
492− "owoify": "1.2.0",
493− "pairs": "9.0.1",
494− "parallel": "7.0.0",
495− "parsing": "11.0.0",
496− "parsing-dataview": "3.2.4",
497− "partial": "4.0.0",
498− "pathy": "9.0.0",
499− "pha": "0.13.0",
500− "phaser": "0.7.0",
501− "phylio": "1.1.2",
502− "pipes": "8.0.0",
503− "pirates-charm": "0.0.1",
504− "play": "1.0.0",
505− "pmock": "0.9.0",
506− "point-free": "1.0.0",
507− "pointed-list": "0.5.1",
508− "polymorphic-vectors": "4.0.0",
509− "posix-types": "6.0.0",
510− "postgresql": "2.0.21",
511− "precise": "6.0.0",
512− "precise-datetime": "7.0.0",
513− "prelude": "6.0.2",
514− "prettier-printer": "3.0.0",
515− "printf": "0.1.0",
516− "priority-queue": "0.1.2",
517− "profunctor": "6.0.1",
518− "profunctor-lenses": "8.0.0",
519− "protobuf": "4.4.0",
520− "psa-utils": "8.0.0",
521− "psci-support": "6.0.0",
522− "punycode": "1.0.0",
523− "pursfmt": "0.17.0",
524− "qualified-do": "2.2.0",
525− "quantities": "12.2.0",
526− "quickcheck": "8.0.1",
527− "quickcheck-combinators": "0.1.3",
528− "quickcheck-laws": "7.0.0",
529− "quickcheck-utf8": "0.0.0",
530− "random": "6.0.0",
531− "rationals": "6.0.0",
532− "rdf": "0.1.0",
533− "react": "11.0.0",
534− "react-aria": "0.2.0",
535− "react-basic": "17.0.0",
536− "react-basic-classic": "3.0.0",
537− "react-basic-dnd": "10.1.0",
538− "react-basic-dom": "7.0.0",
539− "react-basic-dom-beta": "0.1.1",
540− "react-basic-emotion": "7.1.0",
541− "react-basic-hooks": "9.0.0",
542− "react-basic-storybook": "2.0.0",
543− "react-dom": "8.0.0",
544− "react-halo": "3.0.0",
545− "react-icons": "1.1.5",
546− "react-markdown": "0.1.0",
547− "react-testing-library": "4.0.1",
548− "react-virtuoso": "1.0.0",
549− "reactix": "0.6.1",
550− "read": "1.0.1",
551− "recharts": "1.1.0",
552− "record": "4.0.0",
553− "record-extra": "5.0.1",
554− "record-extra-srghma": "0.2.8",
555− "record-ptional-fields": "0.1.2",
556− "record-studio": "1.0.4",
557− "refs": "6.0.0",
558− "remotedata": "5.0.1",
559− "repr": "0.5.0",
560− "resize-arrays": "0.0.1",
561− "resize-observer": "1.0.0",
562− "resource": "2.0.1",
563− "resourcet": "1.0.0",
564− "result": "1.0.3",
565− "return": "0.2.0",
566− "ring-modules": "5.0.1",
567− "rito": "0.3.4",
568− "roman": "0.4.0",
569− "rough-notation": "1.0.2",
570− "routing": "11.0.0",
571− "routing-duplex": "0.7.0",
572− "run": "5.0.0",
573− "safe-coerce": "2.0.0",
574− "safely": "4.0.1",
575− "school-of-music": "1.3.0",
576− "selection-foldable": "0.2.0",
577− "selective-functors": "1.0.1",
578− "semirings": "7.0.0",
579− "shuffle": "2.0.0",
580− "signal": "13.0.0",
581− "simple-emitter": "3.0.1",
582− "simple-i18n": "2.0.1",
583− "simple-json": "9.0.0",
584− "simple-json-generics": "0.2.1",
585− "simple-ulid": "3.0.0",
586− "sized-matrices": "1.0.0",
587− "sized-vectors": "5.0.2",
588− "slug": "3.2.0",
589− "small-ffi": "4.0.1",
590− "soundfonts": "4.1.0",
591− "sparse-matrices": "2.0.1",
592− "sparse-polynomials": "3.0.1",
593− "spec": "8.1.1",
594− "spec-discovery": "8.4.0",
595− "spec-mocha": "5.1.1",
596− "spec-node": "0.0.3",
597− "spec-quickcheck": "5.0.2",
598− "spec-reporter-xunit": "0.7.1",
599− "splitmix": "2.1.0",
600− "ssrs": "1.0.0",
601− "st": "6.2.0",
602− "statistics": "0.3.2",
603− "strictlypositiveint": "1.0.1",
604− "string-parsers": "8.0.0",
605− "strings": "6.0.1",
606− "strings-extra": "4.0.0",
607− "stringutils": "0.0.12",
608− "structured-logging": "1.0.0",
609− "substitute": "0.2.3",
610− "supply": "0.2.0",
611− "svg-parser": "3.0.0",
612− "systemd-journald": "0.3.0",
613− "tagged": "4.0.2",
614− "tailrec": "6.1.0",
615− "tanstack-query": "2.0.0",
616− "tecton": "0.2.1",
617− "tecton-halogen": "0.2.0",
618− "test-unit": "17.0.0",
619− "text-formatting": "0.1.0",
620− "thermite": "6.3.1",
621− "thermite-dom": "0.3.1",
622− "these": "6.0.0",
623− "threading": "0.0.3",
624− "tidy": "0.11.1",
625− "tidy-codegen": "4.0.1",
626− "tldr": "0.0.0",
627− "toestand": "0.9.0",
628− "transformation-matrix": "1.0.1",
629− "transformers": "6.1.0",
630− "transit": "1.0.0",
631− "tree-rose": "4.0.2",
632− "trivial-unfold": "0.5.0",
633− "ts-bridge": "4.0.0",
634− "tuples": "7.0.0",
635− "two-or-more": "1.0.0",
636− "type-equality": "4.0.1",
637− "typedenv": "2.0.1",
638− "typelevel": "6.0.0",
639− "typelevel-lists": "2.1.0",
640− "typelevel-peano": "1.0.1",
641− "typelevel-prelude": "7.0.0",
642− "typelevel-regex": "0.0.3",
643− "typelevel-rows": "0.1.0",
644− "typisch": "0.4.0",
645− "uint": "7.0.0",
646− "ulid": "3.0.1",
647− "uncurried-transformers": "1.1.0",
648− "undefined": "2.0.0",
649− "undefined-is-not-a-problem": "1.1.0",
650− "unfoldable": "6.0.0",
651− "unicode": "6.0.0",
652− "unique": "0.6.1",
653− "unlift": "1.0.1",
654− "unordered-collections": "3.1.0",
655− "unsafe-coerce": "6.0.0",
656− "unsafe-reference": "5.0.0",
657− "untagged-to-tagged": "0.1.4",
658− "untagged-union": "1.0.0",
659− "uri": "9.0.0",
660− "url-immutable": "1.0.0",
661− "url-regex-safe": "0.1.1",
662− "uuid": "9.0.0",
663− "uuidv4": "1.0.0",
664− "validation": "6.0.0",
665− "variant": "8.0.0",
666− "variant-encodings": "2.0.0",
667− "variant-gen": "1.0.0",
668− "vectorfield": "1.0.1",
669− "vectors": "2.1.0",
670− "versions": "7.0.0",
671− "visx": "0.0.2",
672− "vitest": "1.0.0",
673− "web-clipboard": "6.0.0",
674− "web-cssom": "2.0.0",
675− "web-cssom-view": "0.1.0",
676− "web-dom": "6.0.0",
677− "web-dom-parser": "8.0.0",
678− "web-dom-xpath": "3.0.0",
679− "web-encoding": "3.0.0",
680− "web-events": "4.0.0",
681− "web-fetch": "4.0.1",
682− "web-file": "4.0.0",
683− "web-geometry": "0.1.0",
684− "web-html": "4.1.1",
685− "web-pointerevents": "2.0.0",
686− "web-proletarian": "1.0.0",
687− "web-promise": "3.2.0",
688− "web-resize-observer": "2.1.0",
689− "web-router": "1.0.0",
690− "web-socket": "4.0.0",
691− "web-storage": "5.0.0",
692− "web-streams": "4.0.0",
693− "web-touchevents": "4.0.0",
694− "web-uievents": "5.0.0",
695− "web-url": "2.0.0",
696− "web-workers": "1.1.0",
697− "web-xhr": "5.0.1",
698− "webb-aff-list": "0.0.2",
699− "webb-array": "0.0.2",
700− "webb-channel": "0.0.3",
701− "webb-commandline": "0.0.3",
702− "webb-directory": "0.0.7",
703− "webb-file": "0.0.1",
704− "webb-map": "0.0.1",
705− "webb-monad": "0.0.2",
706− "webb-mutex": "0.0.7",
707− "webb-parsing": "0.0.5",
708− "webb-random": "0.0.1",
709− "webb-refer": "0.0.3",
710− "webb-set": "0.0.1",
711− "webb-slot": "0.0.1",
712− "webb-stateful": "0.0.3",
713− "webb-string": "0.0.1",
714− "webb-test": "0.0.1",
715− "webb-thread": "0.0.2",
716− "webb-writer": "0.0.3",
717− "webextension-polyfill": "0.1.0",
718− "webgpu": "0.0.1",
719− "which": "2.0.0",
720− "whine-core": "0.0.33",
721− "xterm": "1.0.0",
722− "yaml-next": "3.1.1",
723− "yoga-fetch": "1.0.1",
724− "yoga-json": "5.1.0",
725− "yoga-om": "0.1.0",
726− "yoga-postgres": "6.0.0",
727− "yoga-react-dom": "1.0.1",
728− "yoga-subtlecrypto": "0.1.0",
729− "yoga-tree": "1.0.0",
730− "yoga-tree-svg": "0.1.0",
731− "yoga-tree-utils": "1.0.0",
732− "z3": "0.0.2",
733− "zipperarray": "2.0.0"
734− }
735− },
736− "extra_packages": {
737− "yoga-om-core": "1.0.4"
738− }
739 },
98 },
740 "packages": {
99 "packages": {
741 "aff": {
100 "aff": {
@@ -743,24 +102,42 @@
743 "version": "8.0.0",
102 "version": "8.0.0",
744 "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
103 "integrity": "sha256-9BRIrSdRoYybfiUmBqgKJ66cPqxYHb277bVWQMn/9sg=",
745 "dependencies": [
104 "dependencies": [
746− "bifunctors",
747 "control",
105 "control",
748 "datetime",
106 "datetime",
749 "effect",
107 "effect",
750 "either",
108 "either",
751 "exceptions",
109 "exceptions",
752− "foldable-traversable",
753 "functions",
110 "functions",
754− "maybe",
755 "newtype",
111 "newtype",
756 "parallel",
112 "parallel",
113+ "partial",
757 "prelude",
114 "prelude",
758− "refs",
115+ "st",
759 "tailrec",
116 "tailrec",
760 "transformers",
117 "transformers",
761 "unsafe-coerce"
118 "unsafe-coerce"
762 ]
119 ]
763 },
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+ },
764 "ansi": {
141 "ansi": {
765 "type": "registry",
142 "type": "registry",
766 "version": "7.0.0",
143 "version": "7.0.0",
@@ -768,9 +145,56 @@
768 "dependencies": [
145 "dependencies": [
769 "foldable-traversable",
146 "foldable-traversable",
770 "lists",
147 "lists",
771− "strings"
148+ "prelude"
772 ]
149 ]
773 },
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+ },
774 "arrays": {
198 "arrays": {
775 "type": "registry",
199 "type": "registry",
776 "version": "7.3.0",
200 "version": "7.3.0",
@@ -802,7 +226,8 @@
802 "either",
226 "either",
803 "exceptions",
227 "exceptions",
804 "functions",
228 "functions",
805− "maybe"
229+ "maybe",
230+ "prelude"
806 ]
231 ]
807 },
232 },
808 "bifunctors": {
233 "bifunctors": {
@@ -961,6 +386,16 @@
961 "unsafe-coerce"
386 "unsafe-coerce"
962 ]
387 ]
963 },
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+ },
964 "foldable-traversable": {
399 "foldable-traversable": {
965 "type": "registry",
400 "type": "registry",
966 "version": "6.0.0",
401 "version": "6.0.0",
@@ -979,12 +414,50 @@
979 "tuples"
414 "tuples"
980 ]
415 ]
981 },
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+ },
982 "fork": {
453 "fork": {
983 "type": "registry",
454 "type": "registry",
984 "version": "6.0.0",
455 "version": "6.0.0",
985 "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
456 "integrity": "sha256-0keBVeZZg1eCZEy/En1CohVW829gQUqKtqBNIRa93/A=",
986 "dependencies": [
457 "dependencies": [
987− "aff"
458+ "aff",
459+ "prelude",
460+ "transformers"
988 ]
461 ]
989 },
462 },
990 "free": {
463 "free": {
@@ -1097,6 +570,21 @@
1097 "prelude"
570 "prelude"
1098 ]
571 ]
1099 },
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+ },
1100 "lazy": {
588 "lazy": {
1101 "type": "registry",
589 "type": "registry",
1102 "version": "6.0.0",
590 "version": "6.0.0",
@@ -1143,9 +631,16 @@
1143 "version": "7.0.0",
631 "version": "7.0.0",
1144 "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
632 "integrity": "sha256-Cfltaq+tfYjuSoSBN/b8cSQZbvZTqIRs6ebQewbIkGs=",
1145 "dependencies": [
633 "dependencies": [
634+ "bifunctors",
635+ "either",
1146 "free",
636 "free",
1147 "functors",
637 "functors",
638+ "identity",
639+ "maybe",
640+ "newtype",
641+ "prelude",
1148− "transformers"
642+ "transformers",
643+ "tuples"
1149 ]
644 ]
1150 },
645 },
1151 "newtype": {
646 "newtype": {
@@ -1157,6 +652,107 @@
1157 "safe-coerce"
652 "safe-coerce"
1158 ]
653 ]
1159 },
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+ },
1160 "nonempty": {
756 "nonempty": {
1161 "type": "registry",
757 "type": "registry",
1162 "version": "7.0.0",
758 "version": "7.0.0",
@@ -1176,7 +772,18 @@
1176 "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
772 "integrity": "sha256-mTyG4s8tps3XIf0jjV/lME2/QlwMzrQly2wqfSSUM3o=",
1177 "dependencies": [
773 "dependencies": [
1178 "datetime",
774 "datetime",
1179− "effect"
775+ "effect",
776+ "prelude"
777+ ]
778+ },
779+ "nullable": {
780+ "type": "registry",
781+ "version": "6.0.0",
782+ "integrity": "sha256-CDAZk+L9Sc0GCFTkE8n+qdp4Ys8avvnkU+m2hVALt7M=",
783+ "dependencies": [
784+ "functions",
785+ "maybe",
786+ "prelude"
1180 ]
787 ]
1181 },
788 },
1182 "numbers": {
789 "numbers": {
@@ -1185,7 +792,58 @@
1185 "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
792 "integrity": "sha256-ZydYckgwRAnRaFC7pu6fADhzdwX+UqHZrfJyet64zls=",
1186 "dependencies": [
793 "dependencies": [
1187 "functions",
794 "functions",
1188− "maybe"
795+ "maybe",
796+ "prelude"
797+ ]
798+ },
799+ "open-memoize": {
800+ "type": "registry",
801+ "version": "6.2.0",
802+ "integrity": "sha256-yJLuVYX8FocHIJ+6q7efH/Pj4S+e1Y57DZ5Dn7qf4ww=",
803+ "dependencies": [
804+ "either",
805+ "integers",
806+ "lazy",
807+ "lists",
808+ "maybe",
809+ "partial",
810+ "prelude",
811+ "strings",
812+ "tuples"
813+ ]
814+ },
815+ "optparse": {
816+ "type": "registry",
817+ "version": "5.0.1",
818+ "integrity": "sha256-2btDjHPRR0uujUQJuZAN9AzvVoXe7yG6IwnY5evERhQ=",
819+ "dependencies": [
820+ "arrays",
821+ "bifunctors",
822+ "control",
823+ "effect",
824+ "either",
825+ "enums",
826+ "exists",
827+ "exitcodes",
828+ "foldable-traversable",
829+ "free",
830+ "integers",
831+ "lazy",
832+ "lists",
833+ "maybe",
834+ "newtype",
835+ "node-buffer",
836+ "node-process",
837+ "node-streams",
838+ "nonempty",
839+ "numbers",
840+ "open-memoize",
841+ "partial",
842+ "prelude",
843+ "strings",
844+ "tailrec",
845+ "transformers",
846+ "tuples"
1189 ]
847 ]
1190 },
848 },
1191 "ordered-collections": {
849 "ordered-collections": {
@@ -1194,13 +852,16 @@
1194 "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
852 "integrity": "sha256-NaEXc2cz/7lJfxPWEja4XlZbGxuRqJwFKQJjsJf51kQ=",
1195 "dependencies": [
853 "dependencies": [
1196 "arrays",
854 "arrays",
855+ "control",
1197 "foldable-traversable",
856 "foldable-traversable",
857+ "functions",
1198 "gen",
858 "gen",
1199 "lists",
859 "lists",
1200 "maybe",
860 "maybe",
861+ "newtype",
1201 "partial",
862 "partial",
1202 "prelude",
863 "prelude",
1203− "st",
864+ "safe-coerce",
1204 "tailrec",
865 "tailrec",
1205 "tuples",
866 "tuples",
1206 "unfoldable"
867 "unfoldable"
@@ -1245,14 +906,31 @@
1245 "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
906 "integrity": "sha256-dtUNpG8BGVr7pu6mQZyAnUAftUHgNS7f0QH3Kw5UTpQ=",
1246 "dependencies": [
907 "dependencies": [
1247 "aff",
908 "aff",
909+ "control",
910+ "effect",
911+ "either",
912+ "foldable-traversable",
913+ "identity",
1248 "lists",
914 "lists",
915+ "maybe",
1249 "mmorph",
916 "mmorph",
917+ "newtype",
1250 "prelude",
918 "prelude",
1251 "tailrec",
919 "tailrec",
1252 "transformers",
920 "transformers",
1253 "tuples"
921 "tuples"
1254 ]
922 ]
1255 },
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+ },
1256 "prelude": {
934 "prelude": {
1257 "type": "registry",
935 "type": "registry",
1258 "version": "6.0.2",
936 "version": "6.0.2",
@@ -1274,6 +952,16 @@
1274 "tuples"
952 "tuples"
1275 ]
953 ]
1276 },
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+ },
1277 "record": {
965 "record": {
1278 "type": "registry",
966 "type": "registry",
1279 "version": "4.0.0",
967 "version": "4.0.0",
@@ -1316,8 +1004,8 @@
1316 },
1004 },
1317 "spec": {
1005 "spec": {
1318 "type": "registry",
1006 "type": "registry",
1319− "version": "8.1.1",
1007+ "version": "8.1.2",
1320− "integrity": "sha256-b6qjDb7lUJGLV+WEoywLu6BCXC4zGpfbN43mTL+vBeI=",
1008+ "integrity": "sha256-klPPJ6lBQI1CGOAdLJ+ZoGRt8XZippkTKjn53BrSkL0=",
1321 "dependencies": [
1009 "dependencies": [
1322 "aff",
1010 "aff",
1323 "ansi",
1011 "ansi",
@@ -1348,11 +1036,57 @@
1348 "tuples"
1036 "tuples"
1349 ]
1037 ]
1350 },
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+ },
1351 "st": {
1084 "st": {
1352 "type": "registry",
1085 "type": "registry",
1353 "version": "6.2.0",
1086 "version": "6.2.0",
1354 "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
1087 "integrity": "sha256-WI4MEkkwUd4pnwZQ3G8VKWgX5t1RSNJludm5e9boaRo=",
1355 "dependencies": [
1088 "dependencies": [
1089+ "effect",
1356 "partial",
1090 "partial",
1357 "prelude",
1091 "prelude",
1358 "tailrec",
1092 "tailrec",
@@ -1484,22 +1218,24 @@
1484 "version": "8.0.0",
1218 "version": "8.0.0",
1485 "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1219 "integrity": "sha256-Ydrgl8liWEwYQOWW76PdZtg3vxKpoKgsnmFyTLgZkoU=",
1486 "dependencies": [
1220 "dependencies": [
1221+ "control",
1487 "enums",
1222 "enums",
1223+ "foldable-traversable",
1488 "lists",
1224 "lists",
1489 "maybe",
1225 "maybe",
1490 "partial",
1226 "partial",
1491 "prelude",
1227 "prelude",
1492− "record",
1228+ "type-equality",
1493− "tuples",
1494 "unsafe-coerce"
1229 "unsafe-coerce"
1495 ]
1230 ]
1496 },
1231 },
1497− "yoga-om-core": {
1232+ "yoga-om": {
1498 "type": "registry",
1233 "type": "registry",
1499− "version": "1.0.4",
1234+ "version": "2.0.0",
1500− "integrity": "sha256-kgDxdG17MCIOOqHx8+ggwbkbix2qflhOi3kOx3QTxkA=",
1235+ "integrity": "sha256-eenoujD2rJzR6gYJbbIiqLhXHC1C5dagu91HZkAEt/Y=",
1501 "dependencies": [
1236 "dependencies": [
1502 "aff",
1237 "aff",
1238+ "aff-retry",
1503 "avar",
1239 "avar",
1504 "console",
1240 "console",
1505 "control",
1241 "control",
@@ -1508,13 +1244,14 @@
1508 "either",
1244 "either",
1509 "exceptions",
1245 "exceptions",
1510 "functions",
1246 "functions",
1247+ "heterogeneous",
1511 "maybe",
1248 "maybe",
1512 "newtype",
1249 "newtype",
1513 "parallel",
1250 "parallel",
1514 "prelude",
1251 "prelude",
1515 "record",
1252 "record",
1516 "record-studio",
1253 "record-studio",
1517− "spec",
1254+ "refs",
1518 "tailrec",
1255 "tailrec",
1519 "transformers",
1256 "transformers",
1520 "tuples",
1257 "tuples",
spago.yaml+24−13
@@ -1,29 +1,40 @@
1−workspace:
1+workspace: {}
2− packageSet:
3− registry: 72.0.1
4− extraPackages:
5− yoga-om-core: 1.0.4
6−
7package:
2package:
8 name: yoga-om-layer
3 name: yoga-om-layer
9 publish:
4 publish:
10− version: 1.0.4
5+ version: 2.1.0
11 license: MIT
6 license: MIT
12 location:
7 location:
13 githubOwner: rowtype-yoga
8 githubOwner: rowtype-yoga
14 githubRepo: purescript-yoga-om-layer
9 githubRepo: purescript-yoga-om-layer
15 dependencies:
10 dependencies:
16− - aff: ">=7.0.0 <8.0.0"
11+ - aff: ">=8.0.0 <9.0.0"
17− - newtype: ">=5.0.0 <6.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"
18− - prelude: ">=6.0.0 <7.0.0"
22+ - prelude: ">=6.0.2 <7.0.0"
19 - record: ">=4.0.0 <5.0.0"
23 - record: ">=4.0.0 <5.0.0"
20 - record-studio: ">=1.0.4 <2.0.0"
24 - record-studio: ">=1.0.4 <2.0.0"
25+ - refs: ">=6.0.0 <7.0.0"
26+ - tailrec: ">=6.1.0 <7.0.0"
27+ - transformers: ">=6.1.0 <7.0.0"
21− - type-equality: ">=4.0.0 <5.0.0"
28+ - type-equality: ">=4.0.1 <5.0.0"
29+ - typelevel-prelude: ">=7.0.0 <8.0.0"
22 - unsafe-coerce: ">=6.0.0 <7.0.0"
30 - unsafe-coerce: ">=6.0.0 <7.0.0"
31+ - variant: ">=8.0.0 <9.0.0"
23− - yoga-om-core: ">=1.0.0 <2.0.0"
32+ - yoga-om: ">=2.0.0 <3.0.0"
24 test:
33 test:
25 main: Test.Main
34 main: Test.Main
26 dependencies:
35 dependencies:
27− - spec: ">=7.0.0 <8.0.0"
36+ - spec: ">=7.0.0 <9.0.0"
37+ - spec-discovery: ">=8.0.0 <9.0.0"
38+ - spec-node: ">=0.0.3 <1.0.0"
28 - console: ">=6.0.0 <7.0.0"
39 - console: ">=6.0.0 <7.0.0"
29 - refs: ">=6.0.0 <7.0.0"
40 - 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
2-- @inline export runLayer arity=2
1-- @inline export runLayer arity=2
3−module Yoga.Om.Layer
2+module Yoga.Om.Layer
4− ( OmLayer(..)
3+ ( OmLayer
4+ , ParOmLayer
5+ , Scope
5 , makeLayer
6 , makeLayer
7+ , makeScopedLayer
8+ , bracketLayer
9+ , acquireRelease
10+ , fresh
6 , runLayer
11 , runLayer
12+ , runScoped
13+ , runScopedWith
14+ , withScoped
15+ , withScopedWith
16+ , scoped
7 , combineRequirements
17 , combineRequirements
18+ , withValue
8 , provide
19 , provide
9 , (>->)
20 , (>->)
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
10 , class CheckAllProvided
45 , class CheckAllProvided
11 , class CheckAllLabelsExist
46 , class CheckAllLabelsExist
12 , class CheckLabelExists
47 , class CheckLabelExists
48+ , class TopoWire, topoWire
49+ , class AllLabelsIn
50+ , class FindReady
51+ , class FindReadyDispatch
13 ) where
52 ) where
14
53
15import Prelude
54import Prelude
16
55
17−import Data.Newtype (class Newtype)
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)
18−import Effect.Aff.Class (liftAff)
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)
19import Prim.Row (class Nub, class Union)
87import Prim.Row (class Nub, class Union)
88+import Prim.Row as Row
89+import Type.Proxy (Proxy(..))
20import Prim.RowList (class RowToList, Cons, Nil, RowList)
90import Prim.RowList (class RowToList, Cons, Nil, RowList)
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)
21−import Prim.TypeError (class Fail, Above, Quote, Text)
95+import Prim.TypeError (class Fail, class Warn, Above, Beside, Doc, Quote, Text)
22import Record as Record
96import Record as Record
23import Record.Studio (class Keys)
97import Record.Studio (class Keys)
24import Type.Equality (class TypeEquals)
98import Type.Equality (class TypeEquals)
25import Unsafe.Coerce (unsafeCoerce)
99import Unsafe.Coerce (unsafeCoerce)
26−import Yoga.Om (Om)
100+import Yoga.Om (Om, ParOm)
27import Yoga.Om as Om
101import Yoga.Om as Om
102+import Yoga.Om.Error (Exception)
28
103
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+
29−-- | A layer that requires dependencies (req) and provides services (prov)
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.
30−newtype OmLayer req prov err = OmLayer (Om (Record req) err (Record prov))
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)))
31
208
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)
33
211
34−-- | Helper to create a layer from an Om computation
212+instance Semigroup a => Semigroup (OmLayer req err a) where
35−makeLayer :: forall req prov err. Om (Record req) err (Record prov) -> OmLayer req prov err
213+ append a b = OmLayer (freshId unit) (append (buildLayer a) (buildLayer b))
36−makeLayer = OmLayer
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
37
319
38-- =============================================================================
320-- =============================================================================
39-- Custom Type Errors for Missing Dependencies
321-- Custom Type Errors for Missing Dependencies
40-- =============================================================================
322-- =============================================================================
41
323
42−-- | Check if all required dependencies are provided
324+-- | Check if all required dependencies are provided.
43−-- If not, show a helpful diff of required vs available
325+-- If not, show a helpful diff of required vs available.
44class CheckAllProvided (required :: Row Type) (available :: Row Type)
326class CheckAllProvided (required :: Row Type) (available :: Row Type)
45
327
46−instance checkAllProvidedImpl ::
328+instance
47 ( RowToList required reqList
329 ( RowToList required reqList
48 , RowToList available availList
330 , RowToList available availList
49 , CheckAllLabelsExist reqList availList required available
331 , CheckAllLabelsExist reqList availList required available
50 ) =>
332 ) =>
51 CheckAllProvided required available
333 CheckAllProvided required available
52
334
53−-- | Check that all labels in required RowList exist in available RowList
335+-- | Check that all labels in required RowList exist in available RowList.
54−-- Pass along the original rows for better error messages
336+-- Pass along the original rows for better error messages.
55class CheckAllLabelsExist (required :: RowList Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
337class CheckAllLabelsExist (required :: RowList Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
56
338
57−-- Base case: no more requirements to check
58−instance checkAllLabelsNil :: CheckAllLabelsExist Nil available requiredRow availableRow
339+instance CheckAllLabelsExist Nil available requiredRow availableRow
59
340
60−-- Recursive case: check if this label exists, then check the rest
61−instance checkAllLabelsCons ::
341+instance
62 ( CheckLabelExists label ty available requiredRow availableRow
342 ( CheckLabelExists label ty available requiredRow availableRow
63 , CheckAllLabelsExist tail available requiredRow availableRow
343 , CheckAllLabelsExist tail available requiredRow availableRow
64 ) =>
344 ) =>
65 CheckAllLabelsExist (Cons label ty tail) available requiredRow availableRow
345 CheckAllLabelsExist (Cons label ty tail) available requiredRow availableRow
66
346
67−-- | Check if a single label exists in the available RowList
347+-- | 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!
69class CheckLabelExists (label :: Symbol) (ty :: Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
348class CheckLabelExists (label :: Symbol) (ty :: Type) (available :: RowList Type) (requiredRow :: Row Type) (availableRow :: Row Type)
70
349
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
74 TypeEquals ty ty' =>
351 TypeEquals ty ty' =>
75 CheckLabelExists label ty (Cons label ty' tail) requiredRow availableRow
352 CheckLabelExists label ty (Cons label ty' tail) requiredRow availableRow
76
353
77−-- Keep looking: different label, recurse on tail
78−else instance checkLabelExistsKeepLooking ::
354+else instance
79 CheckLabelExists label ty tail requiredRow availableRow =>
355 CheckLabelExists label ty tail requiredRow availableRow =>
80 CheckLabelExists label ty (Cons otherLabel otherTy tail) requiredRow availableRow
356 CheckLabelExists label ty (Cons otherLabel otherTy tail) requiredRow availableRow
81
357
82−-- Not found: reached end of list, emit custom error WITH FULL CONTEXT!
83−else instance checkLabelExistsNotFound ::
358+else instance
84 Fail
359 Fail
85 ( Above
360 ( Above
86 (Text "Missing required dependency!")
361 (Text "Missing required dependency!")
87− (Above
362+ ( Above
88− (Text "")
363+ (Text "")
89− (Above
364+ ( Above
90− (Text "The first missing field is: ")
365+ (Text "The first missing field is: ")
91− (Above
366+ ( Above
92− (Quote label)
367+ (Quote label)
93− (Above
368+ ( Above
94− (Text "")
369+ (Text "")
95− (Above
370+ ( Above
96− (Text "The layer requires: ")
371+ (Text "The layer requires: ")
97− (Above
372+ ( Above
98− (Quote requiredRow)
373+ (Quote requiredRow)
99− (Above
374+ ( Above
100− (Text "")
375+ (Text "")
101− (Above
376+ ( Above
102− (Text "But you provided: ")
377+ (Text "But you provided: ")
103− (Quote availableRow)
378+ (Quote availableRow)
379+ )
380+ )
381+ )
104− )
382+ )
105 )
383 )
106− )
107 )
384 )
108− )
109 )
385 )
110− )
111 )
386 )
112 ) =>
387 ) =>
113 CheckLabelExists label ty Nil requiredRow availableRow
388 CheckLabelExists label ty Nil requiredRow availableRow
114
389
390+-- =============================================================================
391+-- Running layers
392+-- =============================================================================
393+
115−-- | Run a layer with a given context, with custom error if requirements aren't met
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.
116runLayer
398runLayer
117− :: forall req prov err available
399+ :: forall req err a available
118− . CheckAllProvided req available -- ← Custom error if dependencies missing!
400+ . CheckAllProvided req available
401+ => Keys req
119 => Record available
402 => Record available
120− -> OmLayer req prov err
403+ -> OmLayer req err a
121− -> Om (Record available) err (Record prov)
404+ -> Om (Record available) err a
122−runLayer _ctx layer = unsafeCastLayer layer
405+runLayer _ctx layer = widenCtx (buildLayer layer)
123 where
406 where
124− -- After CheckAllProvided succeeds, we know req ⊆ available at compile time
407+ widenCtx :: Om (Record req) err a -> Om (Record available) err a
125− -- So this cast is safe - we're just telling the type system what it already proved
408+ widenCtx = unsafeCoerce
126− unsafeCastLayer :: OmLayer req prov err -> Om (Record available) err (Record prov)
409+
127− unsafeCastLayer = unsafeCoerce
410+-- | Build a fully-provided scoped layer, return the provisions.
128−
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+-- =============================================================================
129−-- | The key function: combine two layers with automatic deduplication
474+-- Horizontal composition — combine two layers with automatic deduplication
130−-- Given two layers with potentially overlapping requirements,
475+-- =============================================================================
131−-- produce a layer with deduplicated requirements
476+
132−--
133−-- Uses expand to properly widen contexts and errors, then runs both layers!
134combineRequirements
477combineRequirements
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
136 . Union req1 req2 reqMerged
479 . Union req1 req2 reqMerged
137− => Nub reqMerged reqDeduped -- ← Automatic deduplication of requirements!
480+ => Nub reqMerged reqDeduped
138− => Union req1 _req1 reqDeduped -- req1 is subset of reqDeduped
481+ => Union req1 _req1 reqDeduped
139− => Union req2 _req2 reqDeduped -- req2 is subset of reqDeduped
482+ => Union req2 _req2 reqDeduped
483+ => Union err1 err2 errMerged
484+ => Nub errMerged errDeduped
140− => Union err1 _err1 () -- Widen errors to empty
485+ => Union err1 _err1 errDeduped
141− => Union err2 _err2 ()
486+ => Union err2 _err2 errDeduped
142− => Union prov1 prov2 provMergedRaw -- Merge provisions
487+ => Union prov1 prov2 provMerged
143− => Nub provMergedRaw provMerged -- Deduplicate provisions too!
488+ => Nub provMerged provMerged
144− => Keys req1 -- Needed for expand
489+ => Keys req1
145− => Keys req2 -- Needed for expand
490+ => Keys req2
146− => OmLayer req1 prov1 err1
491+ => OmLayer req1 err1 (Record prov1)
147− -> OmLayer req2 prov2 err2
492+ -> OmLayer req2 err2 (Record prov2)
148− -> OmLayer reqDeduped provMerged ()
493+ -> OmLayer reqDeduped errDeduped (Record provMerged)
149−combineRequirements (OmLayer build1) (OmLayer build2) = makeLayer do
494+combineRequirements layer1 layer2 = OmLayer (freshId unit) do
150− -- Widen both contexts and errors to allow them to run in the deduplicated context
495+ rec1 <- Om.expand (buildLayer layer1)
151− -- expand allows an Om computation to run in a larger context with larger error row
496+ rec2 <- Om.expand (buildLayer layer2)
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)
155 pure (Record.merge rec1 rec2)
497 pure (Record.merge rec1 rec2)
156
498
157−-- Example proof that this works:
499+withValue
158−-- If we have:
500+ :: forall req err prov1 prov2 provMerged
159−-- layer1 :: OmLayer (config :: Config) (logger :: Logger) ()
501+ . Union prov1 prov2 provMerged
160−-- layer2 :: OmLayer (config :: Config) (database :: Database) ()
502+ => Nub provMerged provMerged
161−--
503+ => Record prov1
162−-- Then: combineRequirements layer1 layer2
504+ -> OmLayer req err (Record prov2)
163−-- Has type: OmLayer (config :: Config) prov3 err3
505+ -> OmLayer req err (Record provMerged)
164−--
506+withValue defaults layer = OmLayer (freshId unit) do
165−-- NOT: OmLayer (config :: Config, config :: Config) prov3 err3
507+ rec <- buildLayer layer
166−--
508+ pure (Record.merge defaults rec)
167−-- The duplicated requirement (config, config) is automatically
509+
168−-- deduplicated to just (config) by the Nub constraint!
510+-- =============================================================================
169−
170−-- =============================================================================
171−-- Vertical Composition (ZLayer-style provide)
172−-- =============================================================================
173−
174−-- | Vertical composition: feed output of one layer into input of another
511+-- Vertical composition — feed output of one layer into input of another
175−-- | This is the >>> operator from ZIO ZLayer
512+-- =============================================================================
176−-- |
513+
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) ()
183provide
514provide
184− :: forall req prov1 prov2 err1 err2 _req _prov1 _err1 _err2
515+ :: forall req prov1 prov2 err1 err2 req2 reqOut errMerged errDeduped _req _prov1 _err1 _err2
185− . Union req _req req -- expand constraints
516+ . Union req _req req
186 => Union prov1 _prov1 prov1
517 => Union prov1 _prov1 prov1
187− => Union err1 _err1 ()
518+ => Union err1 err2 errMerged
519+ => Nub errMerged errDeduped
520+ => Union err1 _err1 errDeduped
188− => Union err2 _err2 ()
521+ => Union err2 _err2 errDeduped
522+ => Union prov1 req reqOut
523+ => Nub reqOut reqOut
524+ => Union req2 _prov1 reqOut
189 => Keys req
525 => Keys req
190 => Keys prov1
526 => Keys prov1
191− => OmLayer prov1 prov2 err2 -- layer that needs prov1, provides prov2
527+ => Keys req2
528+ => OmLayer req2 err2 (Record prov2)
192− -> OmLayer req prov1 err1 -- layer that needs req, provides prov1
529+ -> OmLayer req err1 (Record prov1)
193− -> OmLayer req prov2 () -- composed: needs req, provides prov2
530+ -> OmLayer req errDeduped (Record prov2)
194−provide (OmLayer layer2) (OmLayer layer1) = makeLayer do
531+provide l2 l1 = OmLayer (freshId unit) do
195− -- Run first layer to get prov1
196− prov1 <- Om.expand layer1
532+ prov1 <- Om.expand (buildLayer l1)
197−
533+ Om.expand (buildLayer l2) # Om.widenCtx prov1
198− -- Run second layer with prov1 as context
199− liftAff $ Om.runOm prov1
200− { exception: \_ -> pure (unsafeCoerce {}) }
201− (Om.expand layer2)
202
534
203infixl 9 provide as >->
535infixl 9 provide as >->
204
536
205−-- TODO: More sophisticated composition operators
537+-- =============================================================================
206−-- For now, use >>> for simple chains and combineRequirements for parallel deps
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(..))
6import Effect (Effect)
6import Effect (Effect)
7import Effect.Console as Console
7import Effect.Console as Console
8import Type.Row (type (+))
8import Type.Row (type (+))
9−import Yoga.Om (Om, launchOm_)
9+import Yoga.Om (launchOm_)
10import Yoga.Om as Om
10import Yoga.Om as Om
11−import Yoga.Om.Layer (OmLayer, makeLayer, runLayer, combineRequirements)
11+import Yoga.Om.Layer (OmLayer, makeLayer, runLayer)
12
12
13-- Define some example types
13-- Define some example types
14type Config = { port :: Int, host :: String }
14type Config = { port :: Int, host :: String }
@@ -24,25 +24,22 @@ type Cache = { get :: String -> Effect (Maybe String) }
24type CacheL r = (cache :: Cache | r)
24type CacheL r = (cache :: Cache | r)
25type CacheL_ = CacheL ()
25type CacheL_ = CacheL ()
26
26
27−-- Example layer 1: needs config, provides logger
28−loggerLayer :: OmLayer ConfigL_ LoggerL_ ()
27+loggerLayer :: OmLayer ConfigL_ () { logger :: Logger }
29loggerLayer = makeLayer do
28loggerLayer = makeLayer do
30 { config } <- Om.ask
29 { config } <- Om.ask
31 pure { logger: { log: \msg -> pure unit } }
30 pure { logger: { log: \msg -> pure unit } }
32
31
33−-- Example layer 2: needs config, provides database
34−databaseLayer :: OmLayer ConfigL_ DatabaseL_ ()
32+databaseLayer :: OmLayer ConfigL_ () { database :: Database }
35databaseLayer = makeLayer do
33databaseLayer = makeLayer do
36 { config } <- Om.ask
34 { config } <- Om.ask
37 pure { database: { query: \q -> pure [] } }
35 pure { database: { query: \q -> pure [] } }
38
36
39−-- Example layer 3: needs logger and database, provides cache
40−cacheLayer :: OmLayer (LoggerL + DatabaseL_) CacheL_ ()
37+cacheLayer :: OmLayer (LoggerL + DatabaseL_) () { cache :: Cache }
41cacheLayer = makeLayer do
38cacheLayer = makeLayer do
42 { logger, database } <- Om.ask
39 { logger, database } <- Om.ask
43 pure { cache: { get: \key -> pure Nothing } }
40 pure { cache: { get: \key -> pure Nothing } }
44
41
45−programLayer :: OmLayer (DatabaseL + CacheL_) () ()
42+programLayer :: OmLayer (DatabaseL + CacheL_) () {}
46programLayer = makeLayer do
43programLayer = makeLayer do
47 pure {}
44 pure {}
48
45
@@ -53,37 +50,3 @@ aProgram = launchOm_ do
53 , database: { query: \q -> pure [] }
50 , database: { query: \q -> pure [] }
54 , cache: { get: \key -> pure Nothing }
51 , cache: { get: \key -> pure Nothing }
55 }
52 }
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
2
2
3import Prelude
3import Prelude
4
4
5−import Data.Newtype (un)
6import Effect (Effect)
5import Effect (Effect)
7import Effect.Aff (launchAff_)
6import Effect.Aff (launchAff_)
8import Effect.Class (liftEffect)
7import Effect.Class (liftEffect)
9−import Effect.Class.Console as Console
8+import Test.Spec.Discovery (discover)
10−import Effect.Ref as Ref
11−import Test.Spec (describe, it)
12import Test.Spec.Reporter (consoleReporter)
9import Test.Spec.Reporter (consoleReporter)
13−import Test.Spec.Runner (runSpec)
10+import Test.Spec.Runner.Node (runSpecAndExitProcess)
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) }
21
11
22main :: Effect Unit
12main :: Effect Unit
23−main = launchAff_ $ runSpec [ consoleReporter ] do
13+main = launchAff_ do
24− describe "OmLayer - Proof of Concept: Type-Level Deduplication" do
14+ specs <- discover "Test\\.Yoga\\.Om\\.Layer\\..*Spec"
25−
15+ liftEffect $ runSpecAndExitProcess [ consoleReporter ] specs
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
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