Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/api-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ covered separately.

## Unreleased: component API and MyGo parity

### Pending-aware application model reads

`Lui_app.model app` now returns the latest reduced model even before a pending
signal update is published. This applies to regular, extension-enabled and
reloadable application constructors. Reading the model does not flush the
scheduler, run view updates or emit patches; view signals keep their existing
batched publication semantics.

An async context fence can capture `(Lui_app.model app).context_revision` before
starting a request and read it again before accepting the response. The revision
must be incremented by the reducer for every context publication, including an
A-to-B-to-A sequence in one flush. A published view signal cannot replace this
fresh read because intermediate updates may be coalesced. Existing callers that
read after `Lui_app.flush` require no changes; callers that intentionally need a
published view snapshot must read their borrowed model signal instead.

### MyGo transparent dynamic anchors

`box` nodes with `display="contents"` now render their children directly into
Expand Down
9 changes: 7 additions & 2 deletions src/lui_app.ml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ type ('model, 'action) reducer_app = {
let reduce model_state reducer action =
Signal.update model_state (fun current -> reducer current action)

let latest_model model_state =
match Signal.state_pending model_state with
| Some model -> model
| None -> Signal.get_state model_state

let restore_state_scopes state_scopes saved =
Hashtbl.iter
(fun path scope ->
Expand Down Expand Up @@ -82,7 +87,7 @@ let create_with_extensions ?virtual_rows backend registry initial_model reducer
app_scheduler = scheduler;
app_runtime = application;
app_scope = scope;
app_read_model = (fun () -> Signal.get_state model_state);
app_read_model = (fun () -> latest_model model_state);
app_send_action = send_action;
app_root_node = root;
app_lifecycle_state = lifecycle;
Expand Down Expand Up @@ -128,7 +133,7 @@ let create_reloadable_with_extensions ?virtual_rows backend registry source_hash
app_scheduler = scheduler;
app_runtime = application;
app_scope = scope;
app_read_model = (fun () -> Signal.get_state model_state);
app_read_model = (fun () -> latest_model model_state);
app_send_action = send_action;
app_root_node = root;
app_lifecycle_state = lifecycle;
Expand Down
3 changes: 3 additions & 0 deletions src/lui_app.mli
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ val start : ('a, 'b) reducer_app -> bool
val flush : ('a, 'b) reducer_app -> bool
val dispose : ('a, 'b) reducer_app -> bool
val disposed : ('a, 'b) reducer_app -> bool

(** Read the latest reduced model, including pending actions, without flushing
signal publication or rendering patches. *)
val model : ('a, 'b) reducer_app -> 'a
val root_node : ('a, 'b) reducer_app -> int
val runtime : ('a, 'b) reducer_app -> Lui_runtime.application
Expand Down
35 changes: 35 additions & 0 deletions test/test_lui.ml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ let test_backend_receives_patches () =
!batches);
ignore (Lui_app.dispose app)

let test_model_reads_pending_reductions () =
let constructors =
[ "plain", (fun backend ->
Lui_app.create backend 0 counter_reducer counter_view)
; "extensions", (fun backend ->
Lui_app.create_with_extensions backend (Lui_extension.registry ())
0 counter_reducer counter_view)
; "reloadable", (fun backend ->
Lui_app.create_reloadable backend "source" "contract"
0 counter_reducer counter_view)
; "reloadable extensions", (fun backend ->
Lui_app.create_reloadable_with_extensions backend
(Lui_extension.registry ()) "source" "contract"
0 counter_reducer counter_view)
]
in
List.iter
(fun (name, create) ->
let app = create (recording_backend ()) in
ignore (Lui_app.start app);
let batch_count = List.length !batches in
ignore (Lui_app.send app `Increment);
ignore (Lui_app.send app `Increment);
Alcotest.(check int) (name ^ ": pending reductions")
2 (Lui_app.model app);
Alcotest.(check int) (name ^ ": reading does not flush")
batch_count (List.length !batches);
flush_app app;
Alcotest.(check int) (name ^ ": published reductions")
2 (Lui_app.model app);
ignore (Lui_app.dispose app))
constructors

(* A node mounted and dropped inside one pending batch has its ops
elided; surviving insert/move indices it offset must be renumbered so
the stream stays incrementally valid for the backend. *)
Expand Down Expand Up @@ -5610,6 +5643,8 @@ let () =
Alcotest.test_case "ordered picked file batches" `Quick test_picked_files_batch;
Alcotest.test_case "gallery composer batches" `Quick test_gallery_composer_batches;
Alcotest.test_case "lifecycle" `Quick test_app_lifecycle;
Alcotest.test_case "pending model reads" `Quick
test_model_reads_pending_reductions;
Alcotest.test_case "backend patches" `Quick
test_backend_receives_patches;
Alcotest.test_case "same-batch create+drop renumbers" `Quick
Expand Down
Loading