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
208 changes: 153 additions & 55 deletions app/application.ml

Large diffs are not rendered by default.

109 changes: 89 additions & 20 deletions app/journal_detail.ml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ type staged_delete =
type event =
| Observe_visible_range of int64 * int64
| Load_next
| Load_initial_next
| Load_more of string
| Loaded of int64 * Journal_graph_projection.detail
| Load_failed of int64 * bool * string
Expand Down Expand Up @@ -339,10 +340,43 @@ let snapshot (t : t) =
let rows (t : t) = (snapshot t).values
let read_pending (t : t) = (snapshot t).reading

let automatic_budget_available (t : t) =
t.automatic_reads < maximum_automatic_reads
&& t.materialized_blocks + page_limit <= maximum_materialized_blocks
;;

let next_unloaded_in t current_rows first last =
let rec find index =
if index >= last
then None
else (
let id, depth =
match current_rows.indexed.(index) with
| Block { block; depth } -> Journal_model.id block, depth
| More { parent_id; depth; _ } -> parent_id, depth - 1
in
let current = branch t id in
if depth < maximum_depth && needs_children current && current.error = None
then Some id
else find (index + 1))
in
find first
;;

let initial_staging (t : t) =
let current_rows = snapshot t in
if current_rows.reading
then `Waiting
else if not (automatic_budget_available t)
then `Ready
else
match next_unloaded_in t current_rows 0 (Array.length current_rows.indexed) with
| None -> `Ready
| Some id -> `Load id
;;

let next_automatic_load (t : t) =
if
t.automatic_reads >= maximum_automatic_reads
|| t.materialized_blocks + page_limit > maximum_materialized_blocks
if not (automatic_budget_available t)
then None
else (
match t.visible_range with
Expand All @@ -356,22 +390,7 @@ let next_automatic_load (t : t) =
Int64.to_int
(Int64.min (Int64.of_int (Array.length current_rows.indexed)) index)
in
let last = bounded last in
let rec find index =
if index >= last
then None
else (
let id, depth =
match current_rows.indexed.(index) with
| Block { block; depth } -> Journal_model.id block, depth
| More { parent_id; depth; _ } -> parent_id, depth - 1
in
let current = branch t id in
if depth < maximum_depth && needs_children current && current.error = None
then Some id
else find (index + 1))
in
find (bounded first)))
next_unloaded_in t current_rows (bounded first) (bounded last)))
;;

let load ?(automatic = false) (t : t) id =
Expand Down Expand Up @@ -423,6 +442,11 @@ let step (t : t) = function
| None -> t, []
| Some id ->
load ~automatic:true { t with automatic_reads = t.automatic_reads + 1 } id)
| Load_initial_next ->
(match initial_staging t with
| `Waiting | `Ready -> t, []
| `Load id ->
load ~automatic:true { t with automatic_reads = t.automatic_reads + 1 } id)
| Load_more id ->
let next, requests = load t id in
( (if requests = []
Expand Down Expand Up @@ -643,6 +667,43 @@ let reconcile_children (t : t) (detail : Journal_graph_projection.detail) =
})
;;

let cancel_initial_reads (t : t) =
if not (Blocks.exists (fun _ (branch : branch) -> branch.pending <> None) t.branches)
then t
else
invalidate_rows
{ t with
branches =
Blocks.map (fun (branch : branch) -> { branch with pending = None }) t.branches
}
;;

let account_initial_update ~(previous : t) (t : t) =
let added =
if previous.blocks == t.blocks
then 0
else
Blocks.fold
(fun id _ count -> if Blocks.mem id previous.blocks then count else count + 1)
t.blocks
0
in
let materialized_blocks =
max t.materialized_blocks (previous.materialized_blocks + added)
in
let automatic_reads = max t.automatic_reads previous.automatic_reads in
let t =
if materialized_blocks = t.materialized_blocks && automatic_reads = t.automatic_reads
then t
else { t with materialized_blocks; automatic_reads }
in
(* A shared update can consume a page already reserved by another branch.
Keep the spent attempt and expose explicit continuation instead. *)
if t.materialized_blocks + page_limit > maximum_materialized_blocks
then cancel_initial_reads t
else t
;;

let apply_child_created (t : t) ~child ~parent =
if matching_child t.pending ~root_id:t.root_id ~child ~parent
then (
Expand Down Expand Up @@ -682,7 +743,7 @@ let apply_child_created (t : t) ~child ~parent =
else t
;;

let stage_delete (t : t) ~block_id =
let stage_delete ?(fence_reads = false) (t : t) ~block_id =
match Blocks.find_opt block_id t.blocks with
| None -> None
| Some block ->
Expand All @@ -702,6 +763,14 @@ let stage_delete (t : t) ~block_id =
in
let removed = Blocks.filter (fun id _ -> descendant [] id) t.blocks in
let removed_branches = Blocks.filter (fun id _ -> Blocks.mem id removed) t.branches in
let removed_branches =
if fence_reads
then
Blocks.map
(fun (branch : branch) -> { branch with pending = None })
removed_branches
else removed_branches
in
let branches =
Blocks.filter_map
(fun id (branch : branch) ->
Expand Down
20 changes: 19 additions & 1 deletion app/journal_detail.mli
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type row =
type event =
| Observe_visible_range of int64 * int64
| Load_next
| Load_initial_next
| Load_more of string
| Loaded of int64 * Journal_graph_projection.detail
| Load_failed of int64 * bool * string
Expand Down Expand Up @@ -54,6 +55,12 @@ val row_key : row -> string
failed reads remain explicit Load more actions. *)
val next_automatic_load : t -> string option

(** Initial body discovery uses the same depth, complete-page reservation and
attempt budgets as visible demand. It reads only unknown first pages of
materialized branches; continuation cursors remain explicit. Publication
must preserve these counters rather than starting another automatic round. *)
val initial_staging : t -> [ `Waiting | `Ready | `Load of string ]

val step : t -> event -> t * Journal_graph_request.t list
val mode : t -> mode
val session_id : t -> ID.Text_input.session_id
Expand Down Expand Up @@ -89,8 +96,19 @@ val admit_child

val apply_block : t -> Journal_model.t -> t
val reconcile_children : t -> Journal_graph_projection.detail -> t

(** Charge newly materialized shared updates to the still-hidden initial owner.
Existing nodes are not charged twice; consumed counters never decrease. *)
val account_initial_update : previous:t -> t -> t

(** Fence cancelled initial completions without refunding budgets or reusing
their generations. Restored branches can admit fresh bounded reads. *)
val cancel_initial_reads : t -> t
val apply_child_created : t -> child:Journal_model.t -> parent:Journal_model.t -> t
val stage_delete : t -> block_id:string -> (t * staged_delete) option

(** With [fence_reads], initial deletion fences only removed branch tickets, including in
Undo state; unrelated pending reads continue without a duplicate request. *)
val stage_delete : ?fence_reads:bool -> t -> block_id:string -> (t * staged_delete) option
val undo_delete : t -> staged_delete -> t

(** Only composer ownership is retained; reopening loads a fresh outline projection. *)
Expand Down
9 changes: 9 additions & 0 deletions app/journal_header.ml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,15 @@ let date_header ~title =
()
;;

let startup ~loading ~initial ~active ?value ~message action =
Ui.Native_widget.widget chrome
~key:(Ui.Key.string "journal-startup-surface")
~props:(`Assoc (["mode",`String "startup";"loading",`Bool loading;
"initial",`Bool initial;"active",`Bool active;"message",`String message]
@ Option.to_list (Option.map (fun value -> "value",`Float value) value)))
~on_event:(fun _ -> ()) ~children:[V.column [action]] () |> V.Body.static
;;

let capture_control ~enabled ~on_capture =
V.buttons
~actions:
Expand Down
4 changes: 4 additions & 0 deletions app/journal_header.mli
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ val detail
-> copy_feedback:string option
-> Journal_view.View.Body.t
-> Journal_view.View.Body.t

(** Stable native startup surface; action remains reachable during waiting. *)
val startup : loading:bool -> initial:bool -> active:bool -> ?value:float
-> message:string -> Journal_view.View.t -> Journal_view.View.Body.t
6 changes: 3 additions & 3 deletions app/journal_media.ml
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ let status = function
| Waiting_remote -> Placeholder "Waiting for upload"
| Waiting_network -> Placeholder "Available when online"
| Waiting_unlock -> Placeholder "Unlock graph to view file"
| Ready _ -> Placeholder "Opening file"
| Ready _ -> Placeholder "Loading"
| Failed { failure; _ } ->
Failed
(match failure with
Expand Down Expand Up @@ -287,7 +287,7 @@ let step t = function
; available = Some (scope, handle)
; lease = None
; serial = ticket.id
; status = Placeholder "Opening file"
; status = Placeholder "Loading"
}
, release_file t @ [ Acquire ticket ] ))
| _, _ ->
Expand Down Expand Up @@ -329,7 +329,7 @@ let step t = function
pending = Some ticket
; lease = None
; serial = ticket.id
; status = Placeholder "Opening file"
; status = Placeholder "Loading"
}
, release_file t @ [ Acquire ticket ] )
| None ->
Expand Down
Loading
Loading