1 # Fixed prepared HTTP applications and bounded affine dependency bundles. 2 import Base 3 import bend-kit-http@0.25.0.2/http.bend as Http 4 import bend-kit-router@0.2.0.0/router.bend as Router 5 import bend-kit-router@0.2.0.0/target.bend as Target 6 7 type Counts is Data: 8 Counts{capacity: Nat, in_use: Nat, completed: Nat, rejected: Nat} 9 10 type Outcome<-E: Data, -O: Data> is Data: 11 Success{value: O} 12 Expected{error: E} 13 Exhausted{} 14 Stopped{} 15 16 type CloseResult is Data: 17 Closed{} 18 Busy{counts: Counts} 19 20 type Message<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type: 21 Submit{principal: P, input: I, reply: Chan(Outcome)} 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. 28 type Context<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Data: 29 Context{messages: Chan(Message)} 30 31 def outcome.value(-E: Data, -O: Data, result: Result<&2, &2, E, O>) -> Outcome: 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. 40 def work.return(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, 41 messages: Chan(Message), reply: Chan(Outcome), 42 result: R & Result<&2, &2, E, O> 43 ) -> IO(Unit): 44 (returned, outcome) = result 45 do IO: 46 accepted : Bool <- Chan.send(Message, messages, Returned{returned}) 47 sent : Bool <- Chan.send(Outcome, reply, outcome.value(E, O, outcome)) 48 return Unit{} 49 50 def 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), reply: Chan(Outcome) 54 ) -> IO(Unit): 55 do IO: 56 result : R & Result<&2, &2, E, O> <- handler(resource, principal, input) 57 work.return(R, P, I, E, O, messages, reply, result) 58 59 def dispose(~R: Type, ~destroy: R -> IO(Unit), resources: List) -> IO(Unit): 60 match resources: 61 case Nil{}: 62 IO.pure(Unit, Unit{}) 63 case Con{resource, rest}: 64 do IO: 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. 70 def drain.message(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, 71 ~destroy: R -> IO(Unit), message: Message 72 ) -> IO(Unit): 73 match message: 74 case Submit{principal, input, reply}: 75 do IO: 76 sent : Bool <- Chan.send(Outcome, reply, Stopped{}) 77 return Unit{} 78 case Inspect{reply}: 79 Chan.close(Counts, reply) 80 case Shutdown{reply}: 81 do IO: 82 sent : Bool <- Chan.send(CloseResult, reply, Closed{}) 83 return Unit{} 84 case Returned{resource}: 85 destroy(resource) 86 87 def drain.step(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, 88 ~destroy: R -> IO(Unit), pending: Maybe<&1, Message>, 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: 96 drain.message(~R, ~P, ~I, ~E, ~O, ~destroy, message) 97 next(Unit{}) 98 99 # The closed inbox contains at most room buffered messages. 100 def drain(~R: Type, ~P: Data, ~I: Data, ~E: Data, ~O: Data, 101 ~destroy: R -> IO(Unit), fuel: Nat, +messages: Chan(Message) 102 ) -> IO(Unit): 103 match fuel: 104 case 0n: 105 IO.pure(Unit, Unit{}) 106 case 1n+p: 107 do IO: 108 pending : Maybe<&1, Message> <- Chan.recv(Message, messages) 109 drain.step(~R, ~P, ~I, ~E, ~O, ~destroy, pending, u => drain(~R, ~P, ~I, ~E, ~O, ~destroy, p, messages)) 110 111 type OwnerStep<-R: Type, -P: Data, -I: Data, -E: Data, -O: Data> is Type: 112 Waiting{resources: List, counts: Counts} 113 Received{message: Message, counts: Counts, resources: List} 114 115 # IO.join closes a one-shot channel; the owner inbox must remain reusable. 116 def 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 123 def receive(-A: Type, channel: Chan(A)) -> IO(A): 124 do IO: 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), +room: U32, step: OwnerStep 133 ) -> IO(Unit): 134 match step: 135 case Waiting{resources, counts}: 136 do IO: 137 message : Message <- receive(Message, 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: 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: 154 dispose(~R, ~destroy, resources) 155 Chan.close(Message, 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: 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: 167 sent : Bool <- Chan.send(Outcome, 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: 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 174 def build.step(~R: Type, ~E: Data, ~destroy: R -> IO(Unit), 175 result: Result<&2, &1, E, R>, resources: List, 176 next: List -> IO(Result<&2, &1, E, List>) 177 ) -> IO(Result<&2, &1, E, List>): 178 match result: 179 case Fail{error}: 180 do IO>>: 181 dispose(~R, ~destroy, resources) 182 return Fail{error} 183 case Done{resource}: 184 next(resource <> resources) 185 186 def 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 188 ) -> IO(Result<&2, &1, E, List>): 189 match remaining: 190 case 0n: 191 IO.pure(Result<&2, &1, E, List>, Done{resources}) 192 case 1n+ +p: 193 do IO>>: 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 197 def 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> 200 ) -> IO(Result<&2, &2, E, Context>): 201 match result: 202 case Fail{error}: 203 IO.pure(Result<&2, &2, E, Context>, Fail{error}) 204 case Done{resources}: 205 do IO>>: 206 channel : Chan(Message) <- Chan.new(Message, room) 207 +messages : Chan(Message) = 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. 213 def 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>): 218 do IO>>: 219 result : Result<&2, &1, E, List> <- build(~K, ~R, ~E, ~create, ~destroy, capacity, config, Nil{}) 220 start.ready(~R, ~P, ~I, ~E, ~O, ~handler, ~destroy, capacity, room, result) 221 222 def submit(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, 223 context: Context, principal: P, input: I, reply: Chan(Outcome) 224 ) -> IO(Bool): 225 Context{messages} = context 226 Chan.send(Message, messages, Submit{principal, input, reply}) 227 228 def call.accepted(-E: Data, -O: Data, accepted: Bool, +reply: Chan(Outcome)) -> IO(Outcome): 229 match accepted: 230 case False{}: 231 do IO>: 232 Chan.close(Outcome, reply) 233 return Stopped{} 234 case True{}: 235 do IO>: 236 result : Outcome <- IO.join(Outcome, reply) 237 Chan.close(Outcome, reply) 238 return result 239 240 def call(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, 241 context: Context, principal: P, input: I 242 ) -> IO(Outcome): 243 do IO>: 244 channel : Chan(Outcome) <- Chan.new(Outcome, 1) 245 +reply : Chan(Outcome) = channel 246 accepted : Bool <- submit(R, P, I, E, O, context, principal, input, reply) 247 call.accepted(E, O, accepted, reply) 248 249 def 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 256 def inspect.sent(sent: Bool, +reply: Chan(Counts)) -> IO(Maybe<&2, Counts>): 257 match sent: 258 case False{}: 259 do IO>: 260 Chan.close(Counts, reply) 261 return None{} 262 case True{}: 263 do IO>: 264 value : Maybe<&1, Counts> <- Chan.recv(Counts, reply) 265 Chan.close(Counts, reply) 266 return inspect.value(value) 267 268 def inspect(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context) -> IO(Maybe<&2, Counts>): 269 Context{messages} = context 270 do IO>: 271 channel : Chan(Counts) <- Chan.new(Counts, 1) 272 +reply : Chan(Counts) = channel 273 sent : Bool <- Chan.send(Message, messages, Inspect{reply}) 274 inspect.sent(sent, reply) 275 276 def close.sent(sent: Bool, +reply: Chan(CloseResult)) -> IO(CloseResult): 277 match sent: 278 case False{}: 279 do IO: 280 Chan.close(CloseResult, reply) 281 return Closed{} 282 case True{}: 283 do IO: 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. 290 def close(-R: Type, -P: Data, -I: Data, -E: Data, -O: Data, context: Context) -> IO(CloseResult): 291 Context{messages} = context 292 do IO: 293 channel : Chan(CloseResult) <- Chan.new(CloseResult, 1) 294 +reply : Chan(CloseResult) = channel 295 sent : Bool <- Chan.send(Message, 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. 300 type Group<-P: Data> is Data: 301 Group{id: U32, ancestors: List<&2, U32>, policy: P} 302 303 type Route<-A: Data, -P: Data> is Data: 304 Route{method: String, pattern: String, action: A, groups: List<&2, U32>, policy: P} 305 306 type Endpoint<-A: Data, -P: Data> is Data: 307 Endpoint{action: A, policies: List<&2, P>} 308 309 type Application<-A: Data, -C: Data, -P: Data> is Data: 310 Application{table: Router.Table>, context: C, root: P} 311 312 type RegistrationError is Data: 313 RouteError{error: Router.Error} 314 InvalidGroups{groups: List<&2, U32>} 315 316 type 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 319 def 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 328 def group.pick(~P: Data, same: Bool, group: Group

, later: Unit -> Maybe<&2, Group

>) -> Maybe<&2, Group

>: 329 match same: 330 case True{}: 331 Some{group} 332 case False{}: 333 later(Unit{}) 334 335 def group.find(~P: Data, groups: List<&2, Group

>, +id: U32) -> Maybe<&2, Group

>: 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 342 def 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 349 def group.policy(~P: Data, found: Maybe<&2, Group

>, 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 356 def groups.policies(~P: Data, ids: List<&2, U32>, +declared: List<&2, Group

>, +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 366 def groups.unique(~P: Data, found: Maybe<&2, Group

>) -> Bool: 367 match found: 368 case None{}: 369 True{} 370 case Some{group}: 371 False{} 372 373 def groups.valid(~P: Data, groups: List<&2, Group

>, +all: List<&2, Group

>) -> 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 381 def 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 388 def routes.prepare(~A: Data, ~P: Data, routes: List<&2, Route>, +groups: List<&2, Group

>, +root: P) -> Result<&2, &2, RegistrationError, List<&2, Router.Entry>>>: 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>>>: 394 policies : List<&2, P> <- routes.policies(~P, groups.policies(~P, ids, groups, Nil{}), ids) 395 entries : List<&2, Router.Entry>> <- 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 398 def application.table(~A: Data, ~C: Data, ~P: Data, result: Result<&2, &2, Router.Error, Router.Table>>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application>: 399 match result: 400 case Fail{error}: 401 Fail{RouteError{error}} 402 case Done{table}: 403 Done{Application{table, context, root}} 404 405 def application.valid(~A: Data, ~C: Data, ~P: Data, valid: Bool, routes: List<&2, Route>, groups: List<&2, Group

>, context: C, +root: P) -> Result<&2, &2, RegistrationError, Application>: 406 match valid: 407 case False{}: 408 Fail{InvalidGroups{Nil{}}} 409 case True{}: 410 do Result<&2, &2, RegistrationError, Application>: 411 entries : List<&2, Router.Entry>> <- routes.prepare(~A, ~P, routes, groups, root) 412 application.table(~A, ~C, ~P, Router.prepare(~Endpoint, entries), context, root) 413 414 # Construction is pure and fallible; no listener or owner is started here. 415 def application(~A: Data, ~C: Data, ~P: Data, routes: List<&2, Route>, +groups: List<&2, Group

>, context: C, root: P) -> Result<&2, &2, RegistrationError, Application>: 416 application.valid(~A, ~C, ~P, groups.valid(~P, groups, groups), routes, groups, context, root) 417 418 def describe(~A: Data, ~C: Data, ~P: Data, app: Application) -> List<&2, Router.Description>: 419 Application{table, context, root} = app 420 Router.describe(~Endpoint, table) 421 422 def 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. 431 def plain(~handler: Http.Req -> IO(Http.Res), req: Http.Req) -> IO(Http.Res): 432 handler(req) 433 434 def dispatch.choice(~A: Data, ~C: Data, ~P: Data, 435 ~run: A -> C -> Match

-> Http.Req -> IO(Http.Res), 436 choice: Router.Choice>, 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 448 def dispatch.resolved(~A: Data, ~C: Data, ~P: Data, 449 ~run: A -> C -> Match

-> Http.Req -> IO(Http.Res), 450 result: Result<&2, &2, Router.Error, Router.Resolved>>, 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. 461 def dispatch(~A: Data, ~C: Data, ~P: Data, 462 ~run: A -> C -> Match

-> Http.Req -> IO(Http.Res), 463 app: Application, 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, table, method, path), context, Http.Req{method, path, headers, body})