A batteries included HTTP/1.1 client in OCaml

Add Immich CLI with authentication and fix daemon fiber handling

- Add immich_auth library with session management (JWT + API key)
- Add CLI commands: auth, server, albums, faces
- Support .well-known/immich endpoint for API URL discovery
- Support multiple profiles for managing multiple servers

Connection pool and H2 improvements:
- Use fork_daemon for connection pool fibers to allow clean shutdown
- Use fork_daemon for H2 reader fiber to prevent switch blocking
- Handle Cancelled exceptions properly during cleanup
- Add await_cancel() for proper fiber lifecycle

OpenAPI code generator fix:
- Check nullable flag on all schema types, not just the fallback case
- Fields with nullable: true are now correctly treated as optional

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+49 -39
+49 -39
lib/h2/h2_client.ml
··· 159 159 with 160 160 | End_of_file -> None 161 161 | Eio.Io _ -> None 162 + | Eio.Cancel.Cancelled _ -> None 162 163 163 164 let send_settings_ack flow = 164 165 let ack_frame = H2_frame.make_settings ~ack:true [] in ··· 374 375 () (* Already running *) 375 376 else begin 376 377 t.reader_running <- true; 377 - Eio.Fiber.fork ~sw (fun () -> 378 - Log.debug (fun m -> m "Frame reader fiber started"); 379 - let rec read_loop () = 380 - match read_frame flow with 381 - | None -> 382 - Log.info (fun m -> m "Frame reader: connection closed"); 383 - Eio.Mutex.use_rw ~protect:true t.connection_error_mutex (fun () -> 384 - if t.connection_error = None then 385 - t.connection_error <- Some "Connection closed" 386 - ); 387 - (* Notify all handlers about connection close *) 388 - Eio.Mutex.use_ro t.handlers_mutex (fun () -> 389 - Hashtbl.iter (fun _id handler -> 390 - Eio.Stream.add handler.events (Connection_error "Connection closed") 391 - ) t.handlers 392 - ) 378 + (* Use fork_daemon so the reader doesn't prevent switch completion. 379 + The reader will be automatically cancelled when the switch completes. *) 380 + Eio.Fiber.fork_daemon ~sw (fun () -> 381 + (try 382 + Log.debug (fun m -> m "Frame reader fiber started"); 383 + let rec read_loop () = 384 + match read_frame flow with 385 + | None -> 386 + Log.info (fun m -> m "Frame reader: connection closed"); 387 + Eio.Mutex.use_rw ~protect:true t.connection_error_mutex (fun () -> 388 + if t.connection_error = None then 389 + t.connection_error <- Some "Connection closed" 390 + ); 391 + (* Notify all handlers about connection close *) 392 + Eio.Mutex.use_ro t.handlers_mutex (fun () -> 393 + Hashtbl.iter (fun _id handler -> 394 + Eio.Stream.add handler.events (Connection_error "Connection closed") 395 + ) t.handlers 396 + ) 393 397 394 - | Some frame -> 395 - match dispatch_frame t flow frame with 396 - | `Continue -> read_loop () 397 - | `Goaway (last_stream_id, error_code, debug) -> 398 - (* Call the GOAWAY callback if provided *) 399 - on_goaway ~last_stream_id ~error_code ~debug; 400 - (* Continue reading to drain any remaining frames *) 401 - read_loop () 402 - | `Error msg -> 403 - Log.err (fun m -> m "Frame reader error: %s" msg); 404 - Eio.Mutex.use_rw ~protect:true t.connection_error_mutex (fun () -> 405 - t.connection_error <- Some msg 406 - ); 407 - (* Notify all handlers *) 408 - Eio.Mutex.use_ro t.handlers_mutex (fun () -> 409 - Hashtbl.iter (fun _id handler -> 410 - Eio.Stream.add handler.events (Connection_error msg) 411 - ) t.handlers 412 - ) 413 - in 414 - read_loop (); 415 - t.reader_running <- false; 416 - Log.debug (fun m -> m "Frame reader fiber stopped") 398 + | Some frame -> 399 + match dispatch_frame t flow frame with 400 + | `Continue -> read_loop () 401 + | `Goaway (last_stream_id, error_code, debug) -> 402 + (* Call the GOAWAY callback if provided *) 403 + on_goaway ~last_stream_id ~error_code ~debug; 404 + (* Continue reading to drain any remaining frames *) 405 + read_loop () 406 + | `Error msg -> 407 + Log.err (fun m -> m "Frame reader error: %s" msg); 408 + Eio.Mutex.use_rw ~protect:true t.connection_error_mutex (fun () -> 409 + t.connection_error <- Some msg 410 + ); 411 + (* Notify all handlers *) 412 + Eio.Mutex.use_ro t.handlers_mutex (fun () -> 413 + Hashtbl.iter (fun _id handler -> 414 + Eio.Stream.add handler.events (Connection_error msg) 415 + ) t.handlers 416 + ) 417 + in 418 + read_loop (); 419 + t.reader_running <- false; 420 + Log.debug (fun m -> m "Frame reader fiber stopped") 421 + with 422 + | Eio.Cancel.Cancelled _ -> 423 + Log.debug (fun m -> m "Frame reader fiber cancelled") 424 + | exn -> 425 + Log.err (fun m -> m "Frame reader fiber error: %s" (Printexc.to_string exn))); 426 + `Stop_daemon 417 427 ) 418 428 end 419 429