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
11 changes: 7 additions & 4 deletions app/application.ml
Original file line number Diff line number Diff line change
Expand Up @@ -4737,7 +4737,7 @@ let start
let completion =
let current =
match pending with
| Some (generation, _, owner) ->
| Some (generation, operation, owner) ->
let snapshot = !state_ref in
generation = snapshot.graph_state.generation
&&
Expand All @@ -4752,11 +4752,14 @@ let start
then Some routes
else None))
in
(match result, Option.bind routes Journal_routes.detail with
| Ok receipt, Some detail ->
(match result, owner, Option.bind routes Journal_routes.detail with
| Ok receipt, None, _ ->
Logseq_db_types.Graph_types.Uuid.to_string receipt.operation = operation
&& receipt.graph_generation = generation
| Ok receipt, Some _, Some detail ->
Journal_model.id (Journal_detail.root detail)
= Logseq_db_types.Graph_types.Uuid.to_string receipt.target
| Error _, _ -> true
| Error _, _, _ -> true
| _ -> false)
| None -> false
in
Expand Down
7 changes: 6 additions & 1 deletion app/journal_media.ml
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,12 @@ let step t = function
| Some old
when old.graph_generation = graph_generation
&& old.consumer = consumer
&& old.asset.uuid = asset.uuid -> { t with selection = Some selected }, []
&& old.asset.uuid = asset.uuid
&& (match t.status with
| File _ -> true
| _ -> old.asset.source = asset.source
&& old.asset.current_checksum = asset.current_checksum) ->
{ t with selection = Some selected }, []
| _ ->
let effects =
release_file t @ release t @ if managed asset then [ demand selected ] else []
Expand Down
82 changes: 75 additions & 7 deletions app/journal_media_runtime.ml
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,14 @@ let notify t g =
{ token = c.consumer
; asset = c.asset
; file_type =
(match c.asset.source with
| Managed (Some v) -> v.file_type
| _ -> "")
(match P.presentation c.state with
| File path when Filename.extension path <> "" ->
let extension = Filename.extension path in
String.sub extension 1 (String.length extension - 1)
| _ ->
(match c.asset.source with
| Managed (Some v) -> v.file_type
| _ -> ""))
; presentation = P.presentation c.state
})
(List.filter
Expand Down Expand Up @@ -313,6 +318,32 @@ let release_import t (receipt : Logseq_db_worker.import_receipt) =
receipt.preview
;;

let handoff_imports t g =
let completed, pending =
List.partition
(fun (receipt : Logseq_db_worker.import_receipt) ->
let token = "import:" ^ G.Uuid.to_string receipt.operation in
not
(Hashtbl.to_seq_values t.previews
|> Seq.exists (fun (_, root, asset) -> root = g.root && asset = token))
(* Hidden controllers already released their File. Without a view
owner, the durable intent/cache owns the bytes, not this UI lease. *)
&& ((not g.visible)
|| List.exists
(fun c ->
c.asset.uuid = receipt.asset.uuid
&& c.asset.current_checksum = receipt.asset.current_checksum
&&
match P.presentation c.state with
| P.File _ -> true
| _ -> false)
g.controllers))
g.local
in
g.local <- pending;
List.iter (release_import t) completed
;;

let clear t g =
List.iter (release_import t) g.local;
g.local <- [];
Expand Down Expand Up @@ -473,8 +504,11 @@ let retain_owners t owners =
Owners.iter
(fun owner ->
if not (Owners.mem owner retained) then root_visible ~owner t ~root false)
g.owners)
t.groups
g.owners;
handoff_imports t g;
notify t g)
t.groups;
pump t
;;

let asset_visible ?(owner = "default") t ~root ~asset visible =
Expand Down Expand Up @@ -515,7 +549,16 @@ let preview_visible t ~owner ~slot ~root ~asset visible =
if (not visible) || current <> Some selected
then (
Hashtbl.remove t.previews preview;
Option.iter (fun (_, root, _) -> root_visible ~owner:preview t ~root false) current);
Option.iter
(fun (_, root, _) ->
root_visible ~owner:preview t ~root false;
Option.iter
(fun g ->
handoff_imports t g;
notify t g)
(Hashtbl.find_opt t.groups root))
current;
pump t);
if visible
then (
match Hashtbl.find_opt t.groups root with
Expand Down Expand Up @@ -1080,7 +1123,17 @@ let receive t ticket response =
g.controllers <- controllers;
g.cursor <- next_cursor;
if not g.dependency_failed then g.error <- None;
List.iter (fun c -> Hashtbl.replace t.consumers c.consumer (g, c)) controllers
List.iter (fun c ->
Hashtbl.replace t.consumers c.consumer (g, c);
if g.visible && not c.shown
&& List.exists (fun (receipt : Logseq_db_worker.import_receipt) ->
receipt.asset.uuid = c.asset.uuid) g.local
then match t.generation with
| None -> ()
| Some graph_generation ->
c.shown <- true;
c.owners <- g.owners;
dispatch t g c (P.Show { graph_generation; consumer = c.consumer; asset = c.asset })) controllers
| _ -> g.error <- Some "Unable to load attachments. Retry.");
notify t g
| _ -> ())
Expand All @@ -1095,6 +1148,7 @@ let receive t ticket response =
(match List.find_opt (fun c -> c.consumer = consumer) g.controllers with
| Some c ->
dispatch t g c (Acquired (ticket, result));
handoff_imports t g;
notify t g
| None -> release_stale t ticket result)
| _ -> release_stale t ticket result));
Expand All @@ -1111,6 +1165,20 @@ let notice t scope = function
notify t g;
pump t
| _ -> ())
| Upload_status { operation; target; status = Cancelled; _ } ->
Option.iter
(fun g ->
let cancelled, pending =
List.partition
(fun (receipt : Logseq_db_worker.import_receipt) ->
receipt.operation = operation && receipt.scope = scope)
g.local
in
g.local <- pending;
List.iter (release_import t) cancelled;
notify t g;
pump t)
(Hashtbl.find_opt t.groups (G.Uuid.to_string target))
| Upload_status _ -> ()
| Asset_capacity_available ->
Hashtbl.iter (fun _ (g, c) -> dispatch t g c P.Capacity_available) t.consumers;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Capture Local Assets

## Problem

Capture drops a valid imported-file lease because its completion has no Detail route owner. The durable original is not published in the ordinary asset cache, and pending same-UUID media does not refresh its demand when metadata changes. A saved Capture can therefore show Waiting for upload despite an available original and later download its own upload.

## Proposal

Admit current Capture completions, resolve local files through the existing scoped durable upload intent, and publish verified originals into the ordinary asset cache through Core.Asset_requested and the sealed runner. Split private lookup and fetch tickets so Fetch always requires a remote version; use the exact strong spec diff approved on 2026-10-16. Refresh pending media demand when its version identity changes while preserving an already displayed file. No new persistent state, business phase, dependencies, Dune changes, or public worker callback changes.

## Decision

Use the approved strong ticket split and scoped local publication path. Keep durable upload intents as the sole persisted mapping, preserve successful displayed files, and retire temporary previews on handoff or matching cancellation.

## Alternatives considered

### Optional remote version on the shared transfer ticket

Initially approved, then superseded by explicit approval of private lookup_ticket/fetch_ticket and lookup-source variants. A shared optional version admits a Fetch without a remote version and relies on runtime guards.

### Retain the imported preview indefinitely

This does not support cold start or group eviction and leaves lease cleanup tied to a transient UI group. Use the existing durable intent and ordinary cache instead.

## Acceptance criteria

- A saved Capture displays its original before PUT completes, with no asset GET.
- Pending local assets resolve offline, including after restart, in their exact account and graph scope.
- A missing local original never causes a GET without an explicit remote version; cancelled and stale completions cannot expose another scope's file.
- Publishing an original preserves active leases, survives cache reopen, and allows staging cleanup without redownloading the original.
- Pending same-UUID metadata changes refresh demand; an already displayed file remains stable.
- An open imported preview keeps its token, path and lease until its final preview owner closes; background cache handoff must not dismiss it.
- Closing or pruning the final preview owner releases its UI lease even after the source row hides; the durable intent/cache continues to own the bytes.
- Public owner regressions, existing lifecycle suites, and isolated synthetic Simulator checks pass. No real upload or private graph is used.

## Risks

- Local and remote checksums may differ. Local lookup must use the current checksum and remote fetch must use the explicit remote version.
- Publication and release must preserve staging pins and fenced graph/account lifecycles; publication failure must not discard the source.
- Simulator host-restamped OCaml objects provide experimental runtime evidence, not clean iOS cross-compilation or ABI acceptance.

## Consequences

- The existing upload intent remains the durable identity authority. Its existing scope reconciliation also rebuilds a private in-memory asset/checksum index; lookup does not rescan every checkpoint for every visible attachment.
- Import attempts verified cache publication before returning. A pending lookup can use the retained original when cache capacity is exhausted. Successful terminal cleanup secures ordinary residency before retiring staging; failure preserves the source for recovery.
- A visible preview hands off only after the ordinary controller acquires a File and no open preview still owns the imported token. Explicit close and owner cleanup finish the handoff; multiple previews retain the original until the final one closes. If the source row is hidden, the final preview owner releases the UI lease without requiring an already hidden controller to retain a File; the durable intent/cache continues to own the bytes. Cancelled notifications release an unpublished preview only for the matching scope and operation; failed or waiting uploads retain local display.
- Public owner regressions cover offline local readiness, missing local files, checksum changes, stale completions, scope mismatch, cold lookup, publication and lease lifetime, metadata refresh, preview handoff, and cancellation. Existing Application, upload, transfer, worker, cache, and sealed-interface checks pass.
- Isolated Simulator checks show both an old pending Capture and a newly saved Capture as final Timeline images before any asset PUT or GET, including after cold restart. The same installed binary later completed both encrypted PUTs and full metadata publication acknowledgements against a loopback synthetic service. Both intents reached Complete, staging was cleaned, and a subsequent cold start acquired the two ordinary cached originals with zero asset GETs. The service applied received transactions through public Database interfaces and computed acknowledgement checksums through public snapshot activation; no product completion events or persisted completion states were injected. This does not establish real cloud upload or full iOS cross-compilation/ABI acceptance.

## Questions

- May the two protected spec interfaces use the displayed strong ticket split and Publish_staged_asset request? Answered: explicitly approved by the user on 2026-10-16, replacing the optional-ticket proposal.
- May implementation use an isolated synthetic Simulator without touching private graphs or real network uploads? Answered: authorized; coordinate build and GUI windows with the other tasks.
Loading
Loading