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
821 changes: 617 additions & 204 deletions app/application.ml

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions app/application.mli
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,15 @@ module Root_navigation : sig
| Capture_edited of string
| Capture_admitted of Journal_capture.t
| Completed of Journal_graph_runtime.response
| Graph_focused of { generation : int; graph_id : Logseq_db_types.Graph_types.Uuid.t }
| Graph_replaced of
{ generation : int
; graph_id : Logseq_db_types.Graph_types.Uuid.t option
}
| Account_cleared
| Local_copy_deleted

val next_graph : current:Logseq_db_types.Graph_types.Uuid.t -> Logseq_db_types.Graph_types.Uuid.t list -> Logseq_db_types.Graph_types.Uuid.t option
val diagnostic_rows : t -> (string * string) list
val create : graph_generation:int -> t
(** Compute state and ordered cleanup without executing file operations.
Expand Down
5 changes: 4 additions & 1 deletion app/journal_asset_runtime.ml
Original file line number Diff line number Diff line change
Expand Up @@ -747,7 +747,8 @@ let shutdown t =
Hashtbl.clear t.dependency_waiting;
t.favorites_page <- None;
t.favorite_members <- Uuids.empty;
Queue.clear t.queued;
retain_releases t;
pump t;
t.configuration <- None;
t.generation <- None;
publish t
Expand Down Expand Up @@ -847,3 +848,5 @@ let notice
| Asset_availability { consumer; asset; availability } ->
dispatch t (Availability { consumer; asset; availability }))
;;

let idle t = Queue.is_empty t.queued
2 changes: 2 additions & 0 deletions app/journal_asset_runtime.mli
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,5 @@ val notice

val pump : t -> unit
val reject : t -> request_id:Logseq_db_types.Graph_types.Uuid.t -> unit

val idle : t -> bool
13 changes: 13 additions & 0 deletions app/journal_header.ml
Original file line number Diff line number Diff line change
Expand Up @@ -540,3 +540,16 @@ let reactive_view
~capture_expanded
~body
;;

let graph_pages ~enabled ~page_id ~resident_page_ids ~target_page_id ~target_id ~on_prepare ~on_commit ~on_cancel ~current ~preview =
let payload=Yojson.Safe.to_string (`Assoc
["mode",`String "graph-pages";"enabled",`Bool enabled;"pageID",`String page_id;
"targetID",(match target_id with None->`Null | Some id->`String id);
"residentPageIDs",`List(List.map(fun id->`String id) resident_page_ids);
"targetPageID",(match target_page_id with None->`Null | Some id->`String id)]) in
V.of_lui (Journal_lui_native.mount ~key:"journal-graph-pages" ~payload
~on_event:(fun event->match event.Journal_lui_native.payload with
| "prepare"->on_prepare () | "commit"->on_commit () | "cancel"->on_cancel () | _->())
~children:[Lui_elements.stack ~grow:1. [Ui.mount current];
Lui_elements.stack ~grow:1. [Ui.mount preview]] Journal_lui_native.chrome_identifier)
;;
5 changes: 5 additions & 0 deletions app/journal_header.mli
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,8 @@ val graph_picker
-> controls:Journal_view.View.t
-> Journal_view.View.Body.t
-> Journal_view.View.Body.t

val graph_pages : enabled:bool -> page_id:string -> resident_page_ids:string list
-> target_page_id:string option -> target_id:string option
-> on_prepare:(unit -> unit) -> on_commit:(unit -> unit) -> on_cancel:(unit -> unit)
-> current:Journal_view.View.t -> preview:Journal_view.View.t -> Journal_view.View.t
72 changes: 48 additions & 24 deletions app/journal_manager_view.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ module Graph_service = Logseq_db_worker_lui.Logseq_db_worker_lui_service
module Presentation = Journal_page_layout

type picker_live =
{ status : string
{ residents : string list
; replacement : string option
; status : string
; authenticated : bool
; checking : bool
; deleting : Graph_service.local_copy_request option
Expand Down Expand Up @@ -49,6 +51,10 @@ type action =
| Startup_retry
| Refresh_catalog
| Begin_online_recovery
| Remove_resident of string
| Move_resident of int * int
| Replace_resident of string
| Cancel_replacement
| Select_graph of string
| Switch_graph
| Close_picker
Expand All @@ -66,9 +72,9 @@ let handler on_action action =
type unlock_chrome_props = { mode : string; title : string; pending : bool; error : string option [@default None] }
[@@deriving yojson]

let graph_picker_list ?header ?on_visible_range rows =
let graph_picker_list ?header ?(residents=[]) ?on_visible_range rows =
V.Native_list.vertical ~key:(Ui.Key.string "graph-list") ~style:Plain ?on_visible_range
[ V.Native_list.section ~key:(Ui.Key.string "graphs") ?header ~separator:Hidden rows ]
(residents @ [ V.Native_list.section ~key:(Ui.Key.string "graphs") ?header ~separator:Hidden rows ])
|> fun list -> V.column ~spacing:0. [list]
|> V.with_test_id (Ui.Test_id.string "graph-picker-list")
|> V.frame ~max_width:Fill ~max_height:Fill
Expand Down Expand Up @@ -165,34 +171,53 @@ let view ?picker_signal props ~on_action =
(if choose then choose_graph else V.empty ())
in
let graph_picker snapshot =
let reactive ?grow ~equal build =
match picker_signal with
| None -> build snapshot.live
| Some signal -> V.of_lui
(Lui_elements.stack ?grow
[Lui_elements.dyn ~equal (fun current -> Ui.mount (build current)) signal])
in
let icon_button ?(enabled = true) ?tooltip ~id ~command ~label icon =
let icon_button ?(enabled = true) ?disabled_signal ?tooltip ~id ~command ~label icon =
V.of_lui (Lui_elements.button ~key:id ~accessibility_identifier:id
~text:"" ~label ~icon ~size:`icon ~variant:`ghost ~disabled:(not enabled) ?tooltip
~text:"" ~label ~icon ~size:`icon ~variant:`ghost ~disabled:(not enabled) ?disabled_signal ?tooltip
~on_press:(fun _ -> on_action command) [])
in
let refresh = reactive
~equal:(fun left right -> left.deleting = right.deleting)
(fun owner -> icon_button ~enabled:(owner.deleting = None)
~id:"graph-picker-refresh" ~command:Refresh_catalog ~label:"Refresh graphs"
~tooltip:"Refresh the authorized graph catalog" `refresh_cw)
in
let projected context project = Option.map (fun source ->
let signal = Signal.map project source in
Signal.on_dispose context.Lui_ui.ui_scope (fun () -> Signal.dispose_signal signal);
signal) picker_signal in
let refresh = V.of_lui (fun context parent ->
Ui.mount (icon_button ~enabled:(snapshot.live.deleting = None)
?disabled_signal:(projected context (fun owner -> owner.deleting <> None))
~id:"graph-picker-refresh" ~command:Refresh_catalog ~label:"Refresh graphs"
~tooltip:"Refresh the authorized graph catalog" `refresh_cw) context parent)
in
let status_text current = current.status in
let status = V.of_lui (fun context parent ->
Lui_elements.text ~value:(status_text snapshot.live)
?value_signal:(projected context status_text)
~foreground:"error-foreground" ~accessibility_identifier:"graph-picker-local-copy-status" [] context parent)
in
let resident_name id = Rrbvec.to_seq snapshot.graphs |> Seq.find_map (fun (graph:Graph_service.graph)->
if Logseq_db_types.Graph_types.Uuid.to_string graph.graph_id=id then Some graph.name else None)
|> Option.value ~default:"Graph" in
let resident_rows=List.init 3 (fun index ->
let slot=string_of_int(index+1) in
let assigned=List.nth_opt snapshot.live.residents index in
let content=match assigned with
| None -> V.row ~spacing:12. [V.symbol ~name:"plus.circle" ();V.column ~alignment:Leading
[V.text ("Slot " ^ slot);V.text ~style:(Ui.Style.Text_style.create ~foreground:Secondary ()) "Choose a graph below"]]
| Some id -> V.row ~spacing:10.
[V.text slot;V.text(resident_name id) |> V.frame ~max_width:Fill
; (match snapshot.live.replacement with
| Some _ -> button ~id:("graph-resident-replace:" ^ id) ~command:(Replace_resident id) "Replace" "arrow.triangle.2.circlepath"
| None -> V.row ~spacing:4.
[icon_button ~enabled:(index>0) ~id:("graph-resident-up:" ^ id) ~command:(Move_resident (index,-1))
~label:("Move " ^ resident_name id ^ " earlier") `arrow_up
;icon_button ~id:("graph-resident-remove:" ^ id) ~command:(Remove_resident id)
~label:("Close " ^ resident_name id) `x]) ] in
V.Native_list.row ~key:(Ui.Key.string ("resident-slot:" ^ slot)) ~separator:Hidden
(content |> V.frame ~max_width:Fill ~min_height:44.)) in
let resident_header=V.column ~alignment:Leading ~spacing:6.
([V.text "Resident graphs · up to 3"] @ match snapshot.live.replacement with
| None -> [] | Some id -> [V.text ("Choose a slot to replace with " ^ resident_name id);
button ~id:"graph-resident-replace-cancel" ~command:Cancel_replacement "Cancel replacement" "xmark"]) in
let residents=[V.Native_list.section ~key:(Ui.Key.string "resident-graphs")
~header:resident_header ~separator:Hidden resident_rows] in
let content =
match Rrbvec.is_empty snapshot.graphs with
| true -> unavailable ~title:"No graphs available" ~symbol:"folder"
Expand All @@ -215,7 +240,7 @@ let view ?picker_signal props ~on_action =
let selection_enabled owner = owner.deleting = None in
let deletion_enabled owner =
owner.authenticated
&& not current && not owner.checking
&& not current && not (List.mem id owner.residents) && not owner.checking
&& owner.deleting = None && presence owner <> Some (Ok Graph_service.Not_downloaded) in
(* Native List cells retain their interactive children. Property signals
update status and enabled state without replacing callback owners. *)
Expand All @@ -233,9 +258,7 @@ let view ?picker_signal props ~on_action =
~foreground:"secondary" [] context parent)]
|> V.frame ~max_width:Fill
; (if current then V.symbol ~name:"checkmark.circle" ()
else reactive ~equal:(fun left right -> presence left = presence right)
(fun owner -> if presence owner = Some (Ok Graph_service.Not_downloaded)
then V.symbol ~name:"icloud.and.arrow.down" () else V.column [])) ]
else V.empty ()) ]
|> V.frame ~max_width:Fill ~min_height:44.)
() |> V.with_test_id (Ui.Test_id.string ("graph-picker:" ^ id))
|> V.frame ~max_width:Fill in
Expand All @@ -257,10 +280,11 @@ let view ?picker_signal props ~on_action =
in
V.Native_list.row ~key:(Ui.Key.string id) ~separator:Hidden row
) (Rrbvec.to_seq snapshot.graphs) |> List.of_seq in
graph_picker_list ~header:status
graph_picker_list ~header:status ~residents
~on_visible_range:(Ui.Event.Handler.create (function
| Ui.Event.Payload.Visible_range range ->
on_action (Visible_range (range.first_index, range.last_exclusive))
on_action (Visible_range (Int64.max 0L (Int64.sub range.first_index 3L),
Int64.max 0L (Int64.sub range.last_exclusive 3L)))
| _ -> ())) rows
in
Journal_header.graph_picker
Expand Down
8 changes: 7 additions & 1 deletion app/journal_manager_view.mli
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
type picker_live =
{ status : string
{ residents : string list
; replacement : string option
; status : string
; authenticated : bool
; checking : bool
; deleting : Logseq_db_worker_lui.Logseq_db_worker_lui_service.local_copy_request option
Expand Down Expand Up @@ -46,6 +48,10 @@ type action =
| Startup_retry
| Refresh_catalog
| Begin_online_recovery
| Remove_resident of string
| Move_resident of int * int
| Replace_resident of string
| Cancel_replacement
| Select_graph of string
| Switch_graph
| Close_picker
Expand Down
2 changes: 2 additions & 0 deletions app/journal_media_runtime.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1250,3 +1250,5 @@ let imported t ~current (receipt : Logseq_db_worker.import_receipt) =
notify t g);
pump t
;;

let idle t = t.queued = []
2 changes: 2 additions & 0 deletions app/journal_media_runtime.mli
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,5 @@ val reject : t -> ticket -> unit
val notice : t -> Service.asset_scope -> Service.asset_notice -> unit
val pump : t -> unit
val imported : t -> current:bool -> Logseq_db_worker.import_receipt -> unit

val idle : t -> bool
1 change: 1 addition & 0 deletions app/journal_native_collection.ml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ let view
let list ?scroll_request_signal () = V.Native_list.vertical
~key
~style:Plain
~retention_key:"timeline"
?scroll_request:(scroll_request scroll_target)
?scroll_request_signal
~on_scroll_completed
Expand Down
8 changes: 5 additions & 3 deletions app/journal_view.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1613,7 +1613,7 @@ module View = struct
type props =
{ style : string; sections : section list
; track_visible_range : bool; track_scroll_completion : bool
; sync_enabled : bool }
; sync_enabled : bool; retention_key : string option [@default None] }
[@@deriving yojson]
type row_event = { row : string; key : string } [@@deriving yojson]
type event =
Expand All @@ -1631,7 +1631,7 @@ module View = struct
;;

(* Content indexes retain the native child ownership order. *)
let build sections ~style ~track_visible ~track_scroll ~sync_enabled =
let build sections ~style ~track_visible ~track_scroll ~sync_enabled ~retention_key =
let contents = ref Rrbvec.empty in
let next_content_index = ref 0 in
let push element =
Expand All @@ -1658,7 +1658,7 @@ module View = struct
in
let wire : Json.props =
{ style = style_name style; sections = List.map section_wire sections
; track_visible_range = track_visible; track_scroll_completion = track_scroll; sync_enabled }
; track_visible_range = track_visible; track_scroll_completion = track_scroll; sync_enabled; retention_key }
in
Json.props_to_yojson wire |> Yojson.Safe.to_string, Rrbvec.to_list !contents
;;
Expand Down Expand Up @@ -1696,6 +1696,7 @@ module View = struct
?on_scroll_completed
?on_visible_range
?on_sync
?retention_key
sections
=
let payload, contents =
Expand All @@ -1705,6 +1706,7 @@ module View = struct
~track_visible:(Option.is_some on_visible_range)
~track_scroll:(Option.is_some on_scroll_completed)
~sync_enabled:(Option.is_some on_sync)
~retention_key
in
(* Expansion state arrives as {"type":"expanded","key":..,"expanded":bool};
the owning row's handler receives Bool like the old disclosure callback. *)
Expand Down
1 change: 1 addition & 0 deletions app/journal_view.mli
Original file line number Diff line number Diff line change
Expand Up @@ -703,6 +703,7 @@ module View : sig
-> ?on_scroll_completed:Event.handler
-> ?on_visible_range:Event.handler
-> ?on_sync:Event.handler
-> ?retention_key:string
-> section list
-> t
end
Expand Down
32 changes: 24 additions & 8 deletions app/journal_worker_admission.ml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ module Worker = Logseq_db_worker_lui.Journal_worker
module Id = Logseq_db_worker_lui.Journal_worker_ids.Worker
module Protocol = Logseq_db_worker.Protocol

type graph_owner = int * Logseq_db_types.Graph_types.Uuid.t * int

let graph_owner = Option.map (fun (scope:Service.timeline_scope)->
scope.startup.account_generation,scope.graph_id,scope.graph_generation)

type lease =
{ protocol_request : Protocol.request
{ scope : Service.timeline_scope option
; protocol_request : Protocol.request
; mutable retired : bool
}

Expand All @@ -15,7 +21,7 @@ type t =
; projection_finished : Logseq_db_types.Graph_types.Uuid.t -> unit
; refused : Service.request -> Worker.send_result -> unit
; graph_requests : (Id.request_id, lease) Hashtbl.t
; protocol_ids : (Logseq_db_types.Graph_types.Uuid.t, Id.request_id) Hashtbl.t
; protocol_ids : (graph_owner option * Logseq_db_types.Graph_types.Uuid.t, Id.request_id) Hashtbl.t
}

let create ~send ~cancel ~projection_accepted ~projection_finished ~refused =
Expand All @@ -27,16 +33,18 @@ let create ~send ~cancel ~projection_accepted ~projection_finished ~refused =

let dispatch owner ?(observe_projection = false) (request : Service.request) =
let protocol = match request with
| Graph_request request -> Some request
| Graph_request request | Graph_request_at (_,request) -> Some request
| _ -> None in
let scope=match request with Service.Graph_request_at(scope,_) -> Some scope | _->None in
let protocol_key id=graph_owner scope,id in
let duplicate = Option.fold ~none:false ~some:(fun request ->
Hashtbl.mem owner.protocol_ids request.Protocol.request_id) protocol in
Hashtbl.mem owner.protocol_ids (protocol_key request.Protocol.request_id)) protocol in
if duplicate then Worker.Full
else match owner.send request with
| Accepted worker_id as accepted ->
Option.iter (fun protocol ->
Hashtbl.add owner.graph_requests worker_id {protocol_request=protocol;retired=false};
Hashtbl.add owner.protocol_ids protocol.Protocol.request_id worker_id;
Hashtbl.add owner.graph_requests worker_id {scope=(match request with Service.Graph_request_at (scope,_) -> Some scope | _ -> None);protocol_request=protocol;retired=false};
Hashtbl.add owner.protocol_ids (protocol_key protocol.Protocol.request_id) worker_id;
if observe_projection then owner.projection_accepted protocol) protocol;
accepted
| (Full | Not_ready | Stopping) as refused ->
Expand All @@ -49,8 +57,9 @@ let retire owner worker_id =
| None -> false
| Some lease ->
let protocol_id = lease.protocol_request.request_id in
if Hashtbl.find_opt owner.protocol_ids protocol_id = Some worker_id then
Hashtbl.remove owner.protocol_ids protocol_id;
let key=graph_owner lease.scope,protocol_id in
if Hashtbl.find_opt owner.protocol_ids key = Some worker_id then
Hashtbl.remove owner.protocol_ids key;
Hashtbl.remove owner.graph_requests worker_id;
lease.retired
;;
Expand All @@ -63,3 +72,10 @@ let cancel_graph_leases owner =
owner.cancel worker_id)) owner.graph_requests;
Hashtbl.clear owner.protocol_ids
;;

let cancel_graph_scope owner scope =
Hashtbl.iter (fun worker_id lease ->
if graph_owner lease.scope=graph_owner scope && not lease.retired then (
lease.retired<-true;owner.projection_finished lease.protocol_request.request_id;
Hashtbl.remove owner.protocol_ids (graph_owner lease.scope,lease.protocol_request.request_id);owner.cancel worker_id)) owner.graph_requests
;;
2 changes: 2 additions & 0 deletions app/journal_worker_admission.mli
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ val retire : t -> Logseq_db_worker_lui.Journal_worker_ids.Worker.request_id -> b
(** Cancel each live lease once, forgetting projection ownership first. Protocol
IDs are reopened; worker-ID tombstones remain until their response arrives. *)
val cancel_graph_leases : t -> unit

val cancel_graph_scope : t -> Logseq_db_worker_lui.Logseq_db_worker_lui_service.timeline_scope option -> unit
14 changes: 14 additions & 0 deletions apple-tests/native-list/JournalListSnapshotTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ import Combine
}
static func settle() async { for _ in 0..<10 { await Task.yield() } }
static func main() async throws {
let offsets = JournalGraphScrollOffsets()
offsets.retain(["account1:A:g1", "account1:B:g2"])
offsets.write(page: "account1:A:g1", list: "timeline", y: 515)
offsets.write(page: "account1:B:g2", list: "timeline", y: 220)
check(offsets.read(page: "account1:A:g1", list: "timeline") == 515, "graph offsets remain independent")
offsets.retain(["account1:B:g2", "account1:A:g3"])
offsets.write(page: "account1:A:g1", list: "timeline", y: 999)
check(offsets.read(page: "account1:A:g1", list: "timeline") == nil, "retired generation cannot restore or write")
check(offsets.read(page: "account1:B:g2", list: "timeline") == 220, "unrelated resident retains native offset")
check(offsets.read(page: "account1:A:g3", list: "timeline") == nil, "reopened graph starts with a fresh native offset")
offsets.retain([])
offsets.write(page: "account1:B:g2", list: "timeline", y: 999)
check(offsets.read(page: "account1:B:g2", list: "timeline") == nil, "logout fences late geometry")
check(JournalGraphScrollOffsets().read(page: "account1:B:g2", list: "timeline") == nil, "runtime instances never share offsets")
var captured: LUIAppleExtensionViewContext?
let registry = LUIAppleExtensionRegistry()
try registry.register(LUIAppleExtension(identifier: "journal-list", fingerprint: "snapshot-test",
Expand Down
Loading
Loading