camber.bend
bend-kit-camber@0.1.1.0 · 467 lines · 0 definitions
1# Fixed prepared HTTP applications and bounded affine dependency bundles.
2import Base
3import bend-kit-http@0.25.0.2/http.bend as Http
4import bend-kit-router@0.2.0.0/router.bend as Router
5import bend-kit-router@0.2.0.0/target.bend as Target
6
7type Counts is Data:
8 Counts{capacity: Nat, in_use: Nat, completed: Nat, rejected: Nat}
9
10type Outcome<-E: Data, -O: Data> is Data:
11 Success{value: O}
12 Expected{error: E}
13 Exhausted{}
14 Stopped{}
15
16type CloseResult is Data:
17 Closed{}
18 Busy{counts: Counts}
19
20type Message<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type:
21 Submit{principal: P, input: I, reply: Chan(Outcome<E, O>)}
22 Returned{resource: R}
23 Inspect{reply: Chan(Counts)}
24 Shutdown{reply: Chan(CloseResult)}
25
26# Copy only this context, never the resources. The inbox bounds queued commands;
27# callers must count blocked submissions inside their transport admission limit.
28type Context<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Data:
29 Context{messages: Chan(Message<R, P, I, E, O>)}
30
31def outcome.value(-E: Data, -O: Data, result: Result<&2, &2, E, O>) -> Outcome<E, O>:
32 match result:
33 case Fail{error}:
34 Expected{error}
35 case Done{value}:
36 Success{value}
37
38# The resources return through the private owner protocol BEFORE a disposable
39# caller reply. A closed caller reply cannot erase a handle or strand capacity.
40def work.return(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
41 messages: Chan(Message<R, P, I, E, O>), reply: Chan(Outcome<E, O>),
42 result: R & Result<&2, &2, E, O>
43) -> IO(Unit):
44 (returned, outcome) = result
45 do IO<Unit>:
46 accepted : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Returned{returned})
47 sent : Bool <- Chan.send(Outcome<E, O>, reply, outcome.value(E, O, outcome))
48 return Unit{}
49
50def work(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
51 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>),
52 resource: R, principal: P, input: I,
53 messages: Chan(Message<R, P, I, E, O>), reply: Chan(Outcome<E, O>)
54) -> IO(Unit):
55 do IO<Unit>:
56 result : R & Result<&2, &2, E, O> <- handler(resource, principal, input)
57 work.return(R, P, I, E, O, messages, reply, result)
58
59def dispose(~R: Type, ~destroy: R -> IO(Unit), resources: List<R>) -> IO(Unit):
60 match resources:
61 case Nil{}:
62 IO.pure(Unit, Unit{})
63 case Con{resource, rest}:
64 do IO<Unit>:
65 destroy(resource)
66 dispose(~R, ~destroy, rest)
67
68# Closing Base channels retains buffered messages. Drain them and answer stopped
69# callers, rather than stranding accepted commands behind the close command.
70def drain.message(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
71 ~destroy: R -> IO(Unit), message: Message<R, P, I, E, O>
72) -> IO(Unit):
73 match message:
74 case Submit{principal, input, reply}:
75 do IO<Unit>:
76 sent : Bool <- Chan.send(Outcome<E, O>, reply, Stopped{})
77 return Unit{}
78 case Inspect{reply}:
79 Chan.close(Counts, reply)
80 case Shutdown{reply}:
81 do IO<Unit>:
82 sent : Bool <- Chan.send(CloseResult, reply, Closed{})
83 return Unit{}
84 case Returned{resource}:
85 destroy(resource)
86
87def drain.step(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
88 ~destroy: R -> IO(Unit), pending: Maybe<&1, Message<R, P, I, E, O>>,
89 next: Unit -> IO(Unit)
90) -> IO(Unit):
91 match pending:
92 case None{}:
93 IO.pure(Unit, Unit{})
94 case Some{message}:
95 do IO<Unit>:
96 drain.message(~R, ~P, ~I, ~E, ~O, ~destroy, message)
97 next(Unit{})
98
99# The closed inbox contains at most room buffered messages.
100def drain(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
101 ~destroy: R -> IO(Unit), fuel: Nat, +messages: Chan(Message<R, P, I, E, O>)
102) -> IO(Unit):
103 match fuel:
104 case 0n:
105 IO.pure(Unit, Unit{})
106 case 1n+p:
107 do IO<Unit>:
108 pending : Maybe<&1, Message<R, P, I, E, O>> <- Chan.recv(Message<R, P, I, E, O>, messages)
109 drain.step(~R, ~P, ~I, ~E, ~O, ~destroy, pending, u => drain(~R, ~P, ~I, ~E, ~O, ~destroy, p, messages))
110
111type OwnerStep<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type:
112 Waiting{resources: List<R>, counts: Counts}
113 Received{message: Message<R, P, I, E, O>, counts: Counts, resources: List<R>}
114
115# IO.join closes a one-shot channel; the owner inbox must remain reusable.
116def receive.value(-A: Type, value: Maybe<&1, A>) -> IO(A):
117 match value:
118 case None{}:
119 IO.die(A, 1, "camber internal channel closed unexpectedly")
120 case Some{message}:
121 IO.pure(A, message)
122
123def receive(-A: Type, channel: Chan(A)) -> IO(A):
124 do IO<A>:
125 value : Maybe<&1, A> <- Chan.recv(A, channel)
126 receive.value(A, value)
127
128# Only idle close ends this loop. Busy close never waits on a stalled handler.
129# @unsafe is solely the lifecycle receive loop, not a termination/proof claim.
130@unsafe def owner(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
131 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
132 +messages: Chan(Message<R, P, I, E, O>), +room: U32, step: OwnerStep<R, P, I, E, O>
133) -> IO(Unit):
134 match step:
135 case Waiting{resources, counts}:
136 do IO<Unit>:
137 message : Message<R, P, I, E, O> <- receive(Message<R, P, I, E, O>, messages)
138 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Received{message, counts, resources})
139 case Received{message, counts, resources}:
140 match message:
141 case Returned{resource}:
142 Counts{capacity, in_use, completed, rejected} = counts
143 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resource <> resources, Counts{capacity, Nat.sub(in_use, 1n), 1n+completed, rejected}})
144 case Inspect{reply}:
145 Counts{+capacity, +in_use, +completed, +rejected} = counts
146 do IO<Unit>:
147 sent : Bool <- Chan.send(Counts, reply, Counts{capacity, in_use, completed, rejected})
148 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, in_use, completed, rejected}})
149 case Shutdown{reply}:
150 Counts{+capacity, in_use, +completed, +rejected} = counts
151 match in_use:
152 case 0n:
153 do IO<Unit>:
154 dispose(~R, ~destroy, resources)
155 Chan.close(Message<R, P, I, E, O>, messages)
156 sent : Bool <- Chan.send(CloseResult, reply, Closed{})
157 drain(~R, ~P, ~I, ~E, ~O, ~destroy, U32.to_nat(room), messages)
158 case 1n+ +p:
159 do IO<Unit>:
160 sent : Bool <- Chan.send(CloseResult, reply, Busy{Counts{capacity, 1n+p, completed, rejected}})
161 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 1n+p, completed, rejected}})
162 case Submit{principal, input, reply}:
163 Counts{capacity, in_use, completed, rejected} = counts
164 match resources:
165 case Nil{}:
166 do IO<Unit>:
167 sent : Bool <- Chan.send(Outcome<E, O>, reply, Exhausted{})
168 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{Nil{}, Counts{capacity, in_use, completed, 1n+rejected}})
169 case Con{resource, rest}:
170 do IO<Unit>:
171 IO.spawn(Unit, work(~R, ~P, ~I, ~E, ~O, ~handler, resource, principal, input, messages, reply))
172 owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{rest, Counts{capacity, 1n+in_use, completed, rejected}})
173
174def build.step(~R: Type, ~E: Data, ~destroy: R -> IO(Unit),
175 result: Result<&2, &1, E, R>, resources: List<R>,
176 next: List<R> -> IO(Result<&2, &1, E, List<R>>)
177) -> IO(Result<&2, &1, E, List<R>>):
178 match result:
179 case Fail{error}:
180 do IO<Result<&2, &1, E, List<R>>>:
181 dispose(~R, ~destroy, resources)
182 return Fail{error}
183 case Done{resource}:
184 next(resource <> resources)
185
186def build(~K: Data, ~R: Type, ~E: Data, ~create: K -> Nat -> IO(Result<&2, &1, E, R>), ~destroy: R -> IO(Unit),
187 remaining: Nat, +config: K, resources: List<R>
188) -> IO(Result<&2, &1, E, List<R>>):
189 match remaining:
190 case 0n:
191 IO.pure(Result<&2, &1, E, List<R>>, Done{resources})
192 case 1n+ +p:
193 do IO<Result<&2, &1, E, List<R>>>:
194 result : Result<&2, &1, E, R> <- create(config, p)
195 build.step(~R, ~E, ~destroy, result, resources, next => build(~K, ~R, ~E, ~create, ~destroy, p, config, next))
196
197def start.ready(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
198 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
199 capacity: Nat, +room: U32, result: Result<&2, &1, E, List<R>>
200) -> IO(Result<&2, &2, E, Context<R, P, I, E, O>>):
201 match result:
202 case Fail{error}:
203 IO.pure(Result<&2, &2, E, Context<R, P, I, E, O>>, Fail{error})
204 case Done{resources}:
205 do IO<Result<&2, &2, E, Context<R, P, I, E, O>>>:
206 channel : Chan(Message<R, P, I, E, O>) <- Chan.new(Message<R, P, I, E, O>, room)
207 +messages : Chan(Message<R, P, I, E, O>) = channel
208 IO.spawn(Unit, owner(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, messages, room, Waiting{resources, Counts{capacity, 0n, 0n, 0n}}))
209 return Done{Context{messages}}
210
211# The initializer closes partial handles within a failed bundle; start closes
212# all prior whole bundles. No workers exist until the complete set opens.
213def start(~K: Data, ~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data,
214 ~create: K -> Nat -> IO(Result<&2, &1, E, R>),
215 ~handler: R -> P -> I -> IO(R & Result<&2, &2, E, O>), ~destroy: R -> IO(Unit),
216 config: K, +capacity: Nat, room: U32
217) -> IO(Result<&2, &2, E, Context<R, P, I, E, O>>):
218 do IO<Result<&2, &2, E, Context<R, P, I, E, O>>>:
219 result : Result<&2, &1, E, List<R>> <- build(~K, ~R, ~E, ~create, ~destroy, capacity, config, Nil{})
220 start.ready(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, capacity, room, result)
221
222def submit(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
223 context: Context<R, P, I, E, O>, principal: P, input: I, reply: Chan(Outcome<E, O>)
224) -> IO(Bool):
225 Context{messages} = context
226 Chan.send(Message<R, P, I, E, O>, messages, Submit{principal, input, reply})
227
228def call.accepted(-E: Data, -O: Data, accepted: Bool, +reply: Chan(Outcome<E, O>)) -> IO(Outcome<E, O>):
229 match accepted:
230 case False{}:
231 do IO<Outcome<E, O>>:
232 Chan.close(Outcome<E, O>, reply)
233 return Stopped{}
234 case True{}:
235 do IO<Outcome<E, O>>:
236 result : Outcome<E, O> <- IO.join(Outcome<E, O>, reply)
237 Chan.close(Outcome<E, O>, reply)
238 return result
239
240def call(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data,
241 context: Context<R, P, I, E, O>, principal: P, input: I
242) -> IO(Outcome<E, O>):
243 do IO<Outcome<E, O>>:
244 channel : Chan(Outcome<E, O>) <- Chan.new(Outcome<E, O>, 1)
245 +reply : Chan(Outcome<E, O>) = channel
246 accepted : Bool <- submit(R, P, I, E, O, context, principal, input, reply)
247 call.accepted(E, O, accepted, reply)
248
249def inspect.value(value: Maybe<&1, Counts>) -> Maybe<&2, Counts>:
250 match value:
251 case None{}:
252 None{}
253 case Some{counts}:
254 Some{counts}
255
256def inspect.sent(sent: Bool, +reply: Chan(Counts)) -> IO(Maybe<&2, Counts>):
257 match sent:
258 case False{}:
259 do IO<Maybe<&2, Counts>>:
260 Chan.close(Counts, reply)
261 return None{}
262 case True{}:
263 do IO<Maybe<&2, Counts>>:
264 value : Maybe<&1, Counts> <- Chan.recv(Counts, reply)
265 Chan.close(Counts, reply)
266 return inspect.value(value)
267
268def inspect(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context<R, P, I, E, O>) -> IO(Maybe<&2, Counts>):
269 Context{messages} = context
270 do IO<Maybe<&2, Counts>>:
271 channel : Chan(Counts) <- Chan.new(Counts, 1)
272 +reply : Chan(Counts) = channel
273 sent : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Inspect{reply})
274 inspect.sent(sent, reply)
275
276def close.sent(sent: Bool, +reply: Chan(CloseResult)) -> IO(CloseResult):
277 match sent:
278 case False{}:
279 do IO<CloseResult>:
280 Chan.close(CloseResult, reply)
281 return Closed{}
282 case True{}:
283 do IO<CloseResult>:
284 value : CloseResult <- IO.join(CloseResult, reply)
285 Chan.close(CloseResult, reply)
286 return value
287
288# Stop external admissions before close. Busy preserves live context/capacity;
289# retry after admitted operations finish. Stalled operations remain observable.
290def close(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context<R, P, I, E, O>) -> IO(CloseResult):
291 Context{messages} = context
292 do IO<CloseResult>:
293 channel : Chan(CloseResult) <- Chan.new(CloseResult, 1)
294 +reply : Chan(CloseResult) = channel
295 sent : Bool <- Chan.send(Message<R, P, I, E, O>, messages, Shutdown{reply})
296 close.sent(sent, reply)
297
298# Policies are prepared Data, not callbacks. Later lifecycle stages can consume
299# these root-to-route declarations without splitting patterns or finding groups.
300type Group<-P: Data> is Data:
301 Group{id: U32, ancestors: List<&2, U32>, policy: P}
302
303type Route<-A: Data, -P: Data> is Data:
304 Route{method: String, pattern: String, action: A, groups: List<&2, U32>, policy: P}
305
306type Endpoint<-A: Data, -P: Data> is Data:
307 Endpoint{action: A, policies: List<&2, P>}
308
309type Application<-A: Data, -C: Data, -P: Data> is Data:
310 Application{table: Router.Table<Endpoint<A, P>>, context: C, root: P}
311
312type RegistrationError is Data:
313 RouteError{error: Router.Error}
314 InvalidGroups{groups: List<&2, U32>}
315
316type Match<-P: Data> is Data:
317 Match{target: Target.Target, params: Map<&2, String>, groups: List<&2, U32>, method: String, pattern: String, policies: List<&2, P>}
318
319def groups.equal(xs: List<&2, U32>, ys: List<&2, U32>) -> Bool:
320 match xs ys:
321 case Nil{} Nil{}:
322 True{}
323 case x <> xt y <> yt:
324 U32.is_eq(x, y) && groups.equal(xt, yt)
325 case _ _:
326 False{}
327
328def group.pick(~P: Data, same: Bool, group: Group<P>, later: Unit -> Maybe<&2, Group<P>>) -> Maybe<&2, Group<P>>:
329 match same:
330 case True{}:
331 Some{group}
332 case False{}:
333 later(Unit{})
334
335def group.find(~P: Data, groups: List<&2, Group<P>>, +id: U32) -> Maybe<&2, Group<P>>:
336 match groups:
337 case Nil{}:
338 None{}
339 case Group{+here, ancestors, policy} <> rest:
340 group.pick(~P, U32.is_eq(here, id), Group{here, ancestors, policy}, u => group.find(~P, rest, id))
341
342def group.checked(~P: Data, same: Bool, policy: P) -> Maybe<&2, P>:
343 match same:
344 case False{}:
345 None{}
346 case True{}:
347 Some{policy}
348
349def group.policy(~P: Data, found: Maybe<&2, Group<P>>, ancestors: List<&2, U32>) -> Maybe<&2, P>:
350 match found:
351 case None{}:
352 None{}
353 case Some{Group{id, expected, policy}}:
354 group.checked(~P, groups.equal(expected, ancestors), policy)
355
356def groups.policies(~P: Data, ids: List<&2, U32>, +declared: List<&2, Group<P>>, +ancestors: List<&2, U32>) -> Maybe<&2, List<&2, P>>:
357 match ids:
358 case Nil{}:
359 Some{Nil{}}
360 case +id <> rest:
361 do Maybe<&2, List<&2, P>>:
362 policy : P <- group.policy(~P, group.find(~P, declared, id), ancestors)
363 policies : List<&2, P> <- groups.policies(~P, rest, declared, List.append(&2, U32, ancestors, [id]))
364 return policy <> policies
365
366def groups.unique(~P: Data, found: Maybe<&2, Group<P>>) -> Bool:
367 match found:
368 case None{}:
369 True{}
370 case Some{group}:
371 False{}
372
373def groups.valid(~P: Data, groups: List<&2, Group<P>>, +all: List<&2, Group<P>>) -> Bool:
374 match groups:
375 case Nil{}:
376 True{}
377 case Group{+id, +ancestors, policy} <> +rest:
378 chain = groups.policies(~P, List.append(&2, U32, ancestors, [id]), all, Nil{})
379 groups.unique(~P, group.find(~P, rest, id)) && Maybe.is_some(&2, List<&2, P>, chain) && groups.valid(~P, rest, all)
380
381def routes.policies(~P: Data, result: Maybe<&2, List<&2, P>>, ids: List<&2, U32>) -> Result<&2, &2, RegistrationError, List<&2, P>>:
382 match result:
383 case None{}:
384 Fail{InvalidGroups{ids}}
385 case Some{policies}:
386 Done{policies}
387
388def routes.prepare(~A: Data, ~P: Data, routes: List<&2, Route<A, P>>, +groups: List<&2, Group<P>>, +root: P) -> Result<&2, &2, RegistrationError, List<&2, Router.Entry<Endpoint<A, P>>>>:
389 match routes:
390 case Nil{}:
391 Done{Nil{}}
392 case Route{method, pattern, action, +ids, policy} <> rest:
393 do Result<&2, &2, RegistrationError, List<&2, Router.Entry<Endpoint<A, P>>>>:
394 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, groups, Nil{}), ids)
395 entries : List<&2, Router.Entry<Endpoint<A, P>>> <- routes.prepare(~A, ~P, rest, groups, root)
396 return Router.Entry{method, pattern, Endpoint{action, root <> List.append(&2, P, policies, [policy])}, ids} <> entries
397
398def application.table(~A: Data, ~C: Data, ~P: Data, result: Result<&2, &2, Router.Error, Router.Table<Endpoint<A, P>>>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
399 match result:
400 case Fail{error}:
401 Fail{RouteError{error}}
402 case Done{table}:
403 Done{Application{table, context, root}}
404
405def application.valid(~A: Data, ~C: Data, ~P: Data, valid: Bool, routes: List<&2, Route<A, P>>, groups: List<&2, Group<P>>, context: C, +root: P) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
406 match valid:
407 case False{}:
408 Fail{InvalidGroups{Nil{}}}
409 case True{}:
410 do Result<&2, &2, RegistrationError, Application<A, C, P>>:
411 entries : List<&2, Router.Entry<Endpoint<A, P>>> <- routes.prepare(~A, ~P, routes, groups, root)
412 application.table(~A, ~C, ~P, Router.prepare(~Endpoint<A, P>, entries), context, root)
413
414# Construction is pure and fallible; no listener or owner is started here.
415def application(~A: Data, ~C: Data, ~P: Data, routes: List<&2, Route<A, P>>, +groups: List<&2, Group<P>>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application<A, C, P>>:
416 application.valid(~A, ~C, ~P, groups.valid(~P, groups, groups), routes, groups, context, root)
417
418def describe(~A: Data, ~C: Data, ~P: Data, app: Application<A, C, P>) -> List<&2, Router.Description>:
419 Application{table, context, root} = app
420 Router.describe(~Endpoint<A, P>, table)
421
422def request.authority(authority: Maybe<&2, String>, req: Http.Req) -> Http.Req:
423 match authority:
424 case None{}:
425 req
426 case Some{host}:
427 Http.Req{method, path, headers, body} = req
428 Http.Req{method, path, Http.set(headers, "host", host), body}
429
430# A closed template can run repeatedly; an affine callback registry cannot.
431def plain(~handler: Http.Req -> IO(Http.Res), req: Http.Req) -> IO(Http.Res):
432 handler(req)
433
434def dispatch.choice(~A: Data, ~C: Data, ~P: Data,
435 ~run: A -> C -> Match<P> -> Http.Req -> IO(Http.Res),
436 choice: Router.Choice<Endpoint<A, P>>, context: C, target: Target.Target, req: Http.Req
437) -> IO(Http.Res):
438 match choice:
439 case Router.NotFound{}:
440 IO.pure(Http.Res, Http.Res{404, Http.empty(), Http.from_string("")})
441 case Router.MethodMissing{groups, allow}:
442 IO.pure(Http.Res, Http.Res{405, Http.set(Http.empty(), "allow", allow), Http.from_string("")})
443 case Router.Options{groups, allow}:
444 IO.pure(Http.Res, Http.Res{204, Http.set(Http.empty(), "allow", allow), Http.from_string("")})
445 case Router.Found{Endpoint{action, policies}, params, groups, method, pattern}:
446 run(action, context, Match{target, params, groups, method, pattern, policies}, req)
447
448def dispatch.resolved(~A: Data, ~C: Data, ~P: Data,
449 ~run: A -> C -> Match<P> -> Http.Req -> IO(Http.Res),
450 result: Result<&2, &2, Router.Error, Router.Resolved<Endpoint<A, P>>>, context: C, req: Http.Req
451) -> IO(Http.Res):
452 match result:
453 case Fail{error}:
454 IO.pure(Http.Res, Http.Res{400, Http.empty(), Http.from_string("")})
455 case Done{Router.Resolved{+target, choice}}:
456 Target.Target{segments, query, authority, star} = target
457 dispatch.choice(~A, ~C, ~P, ~run, choice, context, target, request.authority(authority, req))
458
459# Dispatch moves the original packed body once and returns application bytes.
460# In particular it never performs transport's HEAD/204/304 body suppression.
461def dispatch(~A: Data, ~C: Data, ~P: Data,
462 ~run: A -> C -> Match<P> -> Http.Req -> IO(Http.Res),
463 app: Application<A, C, P>, req: Http.Req
464) -> IO(Http.Res):
465 Application{table, context, root} = app
466 Http.Req{+method, +path, headers, body} = req
467 dispatch.resolved(~A, ~C, ~P, ~run, Router.resolve(~Endpoint<A, P>, table, method, path), context, Http.Req{method, path, headers, body})