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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
}))
sources = [root/'swift/JournalAuthentication.swift', root/'swift/JournalAuthenticationView.swift',
root/'swift/JournalPlatformWire.swift', root/'swift/JournalPlatformServices.swift',
root/'swift/JournalCognitoOAuth.swift',
root/'swift/JournalLocalAccountBindingStore.swift',
root/'apple-tests/authentication/JournalAuthenticationProbe.swift']
sdk = subprocess.check_output(['xcrun', '--sdk', 'iphonesimulator', '--show-sdk-path'], text=True).strip()
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/journal-build/scripts/journal_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -1243,6 +1243,7 @@ def interrupted(signum, frame):
int32_t journal_ocaml_extension_event(int64_t n, const char *name, const char *p)
{ (void)n; (void)name; (void)p; return 1; }
int32_t journal_ocaml_pump(void) { return 1; }
int32_t journal_ocaml_failure_reason(int32_t operation) { (void)operation; return 0; }
void journal_ocaml_platform_event(const char *d, int32_t l) { (void)d; (void)l; }
void journal_ocaml_platform_response(const char *d, int32_t l) { (void)d; (void)l; }
void journal_ocaml_platform_failure(const char *d, int32_t l) { (void)d; (void)l; }
Expand Down
10 changes: 10 additions & 0 deletions .agents/skills/journal-build/scripts/test_journal_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,16 @@ def setUpClass(cls):
cls.build = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cls.build)

def test_link_stub_exports_bounded_failure_getter(self):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory)/'stub.c'
source.write_text(self.build.STUB)
caller = Path(directory)/'caller.c'
caller.write_text('#include <stdint.h>\nextern int32_t journal_ocaml_failure_reason(int32_t);\nint main(void){return journal_ocaml_failure_reason(1);}\n')
executable = Path(directory)/'stub-test'
subprocess.run(['xcrun','clang',str(source),str(caller),'-o',str(executable)],check=True,capture_output=True)
subprocess.run([str(executable)],check=True)

@contextmanager
def pinned_switch(self):
"""Real offline switch; original source metadata deliberately has no url."""
Expand Down
99 changes: 70 additions & 29 deletions app/application.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1023,9 +1023,11 @@ let failure_source_message = function
let record_failure_source state = function
| Journal_graph_runtime.Worker_failure worker_failure ->
record_runtime_worker_failure state worker_failure
| Projection_failure _ ->
| Projection_failure source ->
let diagnostic = Logseq_db_types.Failure_diagnostic.of_source
~operation:"projectGraph" ~reason:"invalidContent" source in
let error = Logseq_db_worker.Error.create ~code:Unsupported_semantics
~message:"The graph view could not be prepared. Retry the operation." ~details:[]
~message:(Logseq_db_types.Failure_diagnostic.message diagnostic) ~details:[]
|> Result.get_ok in
record_worker_error state ~operation:"projectGraph" ~phase:"presentation" error
;;
Expand Down Expand Up @@ -1056,13 +1058,32 @@ let service_error ~operation message =

let report_ui_error ?(code = Logseq_db_worker.Error.Invalid_request) ?(message = "The operation could not finish. Retry the operation or reopen Journal.")
?recovery state ~operation ~phase =
if List.exists (fun (e:worker_error_occurrence) -> e.active && e.operation=operation
&& e.phase=Some phase && e.graph_generation=state.graph_state.generation) state.worker_errors
let latest = List.find_opt (fun (e:worker_error_occurrence) ->
e.active && e.operation=operation && e.phase=Some phase
&& e.graph_generation=state.graph_state.generation) state.worker_errors in
if Option.fold ~none:false ~some:(fun e ->
Logseq_db_worker.Error.code e.error = code
&& String.equal (Logseq_db_worker.Error.message e.error) message) latest
then state
else record_worker_error state ~operation ~phase ?recovery
(Logseq_db_worker.Error.create ~code ~message ~details:[] |> Result.get_ok)
;;

let report_decode_error state ~operation source =
let diagnostic = Logseq_db_types.Failure_diagnostic.of_source
~operation ~reason:"parseFailed" source in
report_ui_error state ~operation ~phase:"native"
~message:(Logseq_db_types.Failure_diagnostic.message diagnostic)
;;

let report_view_decode_error state source =
match Logseq_db_types.Failure_diagnostic.decode source with
| Some diagnostic -> report_decode_error state
~operation:(Logseq_db_types.Failure_diagnostic.operation diagnostic) source
| None -> report_decode_error state ~operation:"decodeExtensionEvent"
"Native event shape is invalid"
;;

let graph_lifecycle_owns_publication error =
let trace = Logseq_db_worker.Error.trace error in
not (String.equal trace.origin.operation "terminalStorageFailure")
Expand Down Expand Up @@ -3689,7 +3710,9 @@ let manager_page ?picker_signal state dispatch =
unavailable ~title:"Unable to prepare your account" ~symbol:"person.crop.circle.badge.exclamationmark"
~message:"Retry to check your account and open your journal."
~actions:(capsule ~id:"journal-startup-retry" ~command:Startup_retry "Retry" "arrow.clockwise")
|> toolbar "Journals" []
|> toolbar "Journals"
[ V.Toolbar.item ~key:(Ui.Key.string "startup-diagnostics")
~placement:Secondary_action diagnostics ]
| Interaction {phase=Signed_out;_},None ->
unavailable ~title:"Sign in to open a graph" ~symbol:"person.crop.circle"
~message:"Connect your account to access your graphs." ~actions:(V.empty ())
Expand Down Expand Up @@ -3940,6 +3963,7 @@ type app_context =
(Graph_service.request, Graph_service.response, Graph_service.push) Worker.client
; send_action : action -> unit
; apply_platform : bytes -> unit Effect.t
; fail_platform : int -> string -> (int64 * int64) option -> unit
; running : bool ref
; release_staging : unit -> unit
}
Expand Down Expand Up @@ -6837,7 +6861,9 @@ let start
k result
| None -> ());
(match result with
| Error _ when current -> report_ui_error model ~operation:"platformRequest" ~phase:"native"
| Error "native.platform.cancelled" when current -> model
| Error reason when current -> report_ui_error model ~operation:"platformRequest" ~phase:"native"
~message:(Journal_bridge.platform_failure_message reason)
| Ok _ | Error _ -> model)
| Environment_changed snapshot ->
prepare_presentation (sync_startup_presentation { model with environment = snapshot })
Expand Down Expand Up @@ -7010,8 +7036,8 @@ let start
let on_event payload =
match Journal_asset_import.decode_event payload with
| Ok event -> dispatch.send (Capture_asset event)
| Error _ -> Effect.run (set_state (fun state ->
report_ui_error state ~operation:"decodeAttachmentSelection" ~phase:"native"))
| Error source -> Effect.run (set_state (fun state ->
report_decode_error state ~operation:"decodeAttachmentSelection" source))
in
let dispatch =
match owner with
Expand Down Expand Up @@ -7606,8 +7632,26 @@ let start
Hashtbl.iter (fun _ batch -> discard_import_batches [ batch ]) capture_staged_items;
Hashtbl.clear capture_staged_items
in
let fail_platform tag reason supplied_scope =
let owner_scope = startup_scope !state_ref in
let matches_scope () = match supplied_scope with
| None -> true (* Validated legacy host envelope. *)
| Some(account,presentation) -> account=Int64.of_int owner_scope.account_generation
&& presentation=Int64.of_int owner_scope.presentation_generation in
let response_tag=response_tag tag in
let owner=Hashtbl.find_opt pending_platform response_tag in
if matches_scope () then Journal_pump.enqueue pump (fun () ->
if !running && startup_scope !state_ref=owner_scope then
if tag=0 then send_action(Update(fun state ->
report_ui_error state ~operation:"decodePlatformRequest" ~phase:"native"
~message:(Journal_bridge.platform_failure_message reason),Effect.ignore))
else match owner,Hashtbl.find_opt pending_platform response_tag with
| Some expected,Some current when expected==current ->
send_action(Platform_response(response_tag,Error reason))
| _ -> ())
in
let context =
{ app; pump; client; send_action; apply_platform; running; release_staging }
{ app; pump; client; send_action; apply_platform; fail_platform; running; release_staging }
in
current_app := Some context;
ignore (dispatch_worker Graph_service.Get_graph_state : Worker.send_result);
Expand Down Expand Up @@ -7694,8 +7738,8 @@ let create
(match !current_app with
| Some { app; send_action; _ } ->
Ui.with_event_error_handler
(fun operation -> send_action (Update (fun state ->
report_ui_error state ~operation ~phase:"native", Effect.ignore)))
(fun diagnostic -> send_action (Update (fun state ->
report_view_decode_error state diagnostic, Effect.ignore)))
(fun () ->
ignore (Lui_app.dispatch_event app event);
ignore (Lui_app.flush app))
Expand All @@ -7707,8 +7751,8 @@ let create
(match !current_app with
| Some { app; send_action; _ } ->
Ui.with_event_error_handler
(fun operation -> send_action (Update (fun state ->
report_ui_error state ~operation ~phase:"native", Effect.ignore)))
(fun diagnostic -> send_action (Update (fun state ->
report_view_decode_error state diagnostic, Effect.ignore)))
(fun () ->
(match Lui_runtime.extension_identifier (Lui_app.runtime app) node with
| Some identifier ->
Expand All @@ -7717,9 +7761,11 @@ let create
report_ui_error state ~operation ~phase:"native", Effect.ignore)) in
(match name, Lui_protocol.String_map.find_opt "id" values,
Lui_protocol.String_map.find_opt "payload" values with
| "event", Some (IntValue (-1)), Some (StringValue "diagnostic.nativePropsDecode") -> report "decodeNativeProps"
| "event", Some (IntValue (-1)), Some (StringValue "diagnostic.nativeEventEncode") -> report "encodeNativeEvent"
| "event", Some (IntValue (-1)), Some (StringValue "diagnostic.nativeEventDelivery") -> report "deliverNativeEvent"
| "event", Some (IntValue (-1)), Some (StringValue code) ->
(match Journal_bridge.native_diagnostic code with
| Some(operation,message) -> send_action (Update(fun state ->
report_ui_error state ~operation ~phase:"native" ~message,Effect.ignore))
| None -> report "decodeNativeEvent")
| "event", Some (IntValue _), Some (StringValue _) ->
ignore (Lui_app.dispatch_event app
(Lui_protocol.ExtensionEvent (node, identifier, name, values)))
Expand Down Expand Up @@ -7752,8 +7798,8 @@ let create
then (
match Journal_platform.decode_environment_event bytes with
| Ok snapshot -> send_action (Environment_changed snapshot)
| Error _ -> send_action (Update (fun state ->
report_ui_error state ~operation:"decodeEnvironment" ~phase:"native", Effect.ignore)))
| Error source -> send_action (Update (fun state ->
report_decode_error state ~operation:"decodeEnvironment" source, Effect.ignore)))
else Effect.run (apply_platform bytes))
| None -> ()
in
Expand All @@ -7773,17 +7819,12 @@ let create
in
let platform_failure payload =
match !current_app with
| Some { pump; send_action; _ } ->
let bytes = Bytes.of_string payload in
if Bytes.length bytes >= 8
then (
let tag = response_tag (Bytes.get_uint16_le bytes 6) in
Journal_pump.enqueue pump (fun () ->
send_action
(Platform_response (tag, Error "application platform request failed"))))
else Journal_pump.enqueue pump (fun () ->
send_action (Update (fun state ->
report_ui_error state ~operation:"decodePlatformFailure" ~phase:"native", Effect.ignore)))
| Some { pump; send_action; fail_platform; running; _ } ->
(match Journal_bridge.decode_platform_failure payload with
| Some(tag,reason,scope) -> fail_platform tag reason scope
| None -> Journal_pump.enqueue pump (fun () ->
if !running then send_action(Update(fun state ->
report_ui_error state ~operation:"decodePlatformFailure" ~phase:"native",Effect.ignore))))
| None -> ()
in
let dispose () =
Expand Down
10 changes: 8 additions & 2 deletions app/journal_asset_import.ml
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,14 @@ module Json = struct
end

let read_event payload =
try Logseq_db_types.Json_codec.decode Json.event_of_yojson (Yojson.Safe.from_string payload)
with Yojson.Json_error _ -> Error "Invalid attachment selection"
try
Logseq_db_types.Json_codec.decode Json.event_of_yojson (Yojson.Safe.from_string payload)
|> Result.map_error (function
| ("invalid UTF-8 field name" | "duplicate JSON field" | "invalid UTF-8 string"
| "non-finite JSON number" | "nonstandard JSON value" | "JSON exceeds byte budget"
| "invalid JSON value") as source -> source
| _ -> "Native event shape is invalid")
with Yojson.Json_error _ -> Error "Native event JSON syntax is invalid"
;;

let staged_of_pick (pick : Json.pick) =
Expand Down
80 changes: 80 additions & 0 deletions app/journal_bridge.ml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,16 @@ let platform_failure payload = (hooks ()).platform_failure payload
let dispose () = (hooks ()).dispose ()
let root_node () = (hooks ()).root_node ()

let classify_exception = function
| Failure message when message = "application.decodeStartup.failed" -> 5
| Failure message when message = "application.startWorker.failed" -> 6
| Failure message when message = "application.mount.failed" -> 7
| _ -> 2
;;

let register hooks =
current := Some hooks;
Callback.register "journal_ocaml_classify_exception" classify_exception;
Callback.register "lui_ocaml_init" initialize;
Callback.register "lui_ocaml_appear" appear;
Callback.register "lui_ocaml_press" press;
Expand Down Expand Up @@ -111,3 +119,75 @@ let register hooks =
Callback.register "journal_ocaml_platform_response" platform_response;
Callback.register "journal_ocaml_platform_failure" platform_failure
;;

let platform_reasons =
[ 1, "cancelled", "The native request was cancelled."
; 2, "requestDecode", "The native request format could not be read."
; 3, "responseEncode", "The native response could not be encoded."
; 4, "clipboardUnavailable", "The clipboard could not accept the copied text."
; 5, "serviceUnavailable", "The native service is currently unavailable. Retry the operation."
; 6, "serviceFailed", "The native service could not complete the request. Retry the operation."
; 7, "authenticationRequired", "Sign in again to complete the request."
; 8, "invalidBinding", "The saved account connection could not be read."
; 9, "network", "The native account request could not connect. Check your connection and retry."
; 10, "secureStorage", "Secure account storage is unavailable. Unlock your device and retry."
; 11, "invalidPacket", "The native service returned an invalid response."
]
;;

let platform_failure_message code =
match List.find_opt (fun (_,reason,_)->code="native.platform."^reason) platform_reasons with
| Some (_,_,message) -> code ^ " · " ^ message
| None -> "native.platform.unknown · The native request could not finish. Retry the operation."
;;

let decode_platform_failure payload =
let bytes=Bytes.of_string payload in
let size=Bytes.length bytes in
let known tag=List.mem tag [6;8;10;13;20;22;28;30;32] in
if size=24 && String.sub payload 0 4="LJF1" then (
let tag=Bytes.get_uint16_le bytes 4 and reason=Bytes.get_uint16_le bytes 6 in
let account=Bytes.get_int64_le bytes 8 and presentation=Bytes.get_int64_le bytes 16 in
match List.find_opt (fun (value,_,_)->value=reason) platform_reasons with
| Some (_,name,_) when (known tag || (tag=0 && reason=2)) && account>=0L && presentation>=0L
&& account<=Int64.of_int max_int && presentation<=Int64.of_int max_int ->
Some(tag,"native.platform."^name,Some(account,presentation))
| _ -> None)
else if size>=32 && size<=32+262144 && String.sub payload 0 4="LJP2"
&& Bytes.get_uint16_le bytes 4=2
&& String.sub payload 8 16=String.make 16 '\000'
&& String.sub payload 28 4=String.make 4 '\000'
&& Int32.equal (Bytes.get_int32_le bytes 24) (Int32.of_int(size-32)) then (
let tag=Bytes.get_uint16_le bytes 6 in
if known tag then Some(tag,"native.platform.serviceFailed",None) else None)
else None
;;

let native_diagnostic source =
let families=
["diagnostic.nativePropsDecode","decodeNativeProps";
"diagnostic.nativeScrollDecode","decodeNativeScroll";
"diagnostic.nativeEventDelivery","deliverNativeEvent";
"diagnostic.nativeEventEncode","encodeNativeEvent"] in
match List.find_opt (fun (prefix,_)->source=prefix || String.starts_with ~prefix:(prefix^".") source) families with
| None -> None
| Some(prefix,operation) ->
let suffix=if source=prefix then "unknown" else String.sub source (String.length prefix+1) (String.length source-String.length prefix-1) in
let reason,message = match suffix with
| "missingPayload" -> suffix,"The required native payload is missing."
| "typeMismatch" -> suffix,"A native field has an unexpected type."
| "keyNotFound" -> suffix,"A required native field is missing."
| "valueNotFound" -> suffix,"A required native value is missing."
| "dataCorrupted" -> suffix,"The native payload format is invalid."
| "invalidPayload" -> suffix,"The native payload is unsupported."
| "backendUnsupportedProperty" -> suffix,"The native backend rejected an unsupported property."
| "backendUnknownNode" -> suffix,"The native backend could not find the destination node."
| "backendUnknownExtension" -> suffix,"The native extension is not registered."
| "backendFingerprintMismatch" -> suffix,"The native extension schema does not match."
| "backendInvalidHierarchy" -> suffix,"The native node hierarchy is invalid."
| "backendInvalidEvent" -> suffix,"The native event schema is invalid."
| "backendRegistryInvalid" -> suffix,"The native extension registry is invalid."
| "backendInvalidBatch" -> suffix,"The native backend rejected the event batch."
| _ -> "unknown","The native operation could not finish. Retry the operation."
in Some(operation,"native."^operation^"."^reason^" · "^message)
;;
18 changes: 14 additions & 4 deletions app/journal_bridge.mli
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@ type hooks =
; (* Host -> OCaml: an LJP2 response envelope completing an earlier
platform request. Binary-safe string. *)
platform_response : string -> unit
; (* Host -> OCaml: the platform could not answer a request (decode or
service failure). Carries the original request envelope; the pending
continuation resolves with an error, matching the old bridge's
request-failure path. *)
; (* Host -> OCaml: the platform could not answer a request. Carries a finite
LJF1 reason/scope envelope; a validated original LJP2 request remains
supported for legacy hosts. Current cancellation still completes the
pending continuation but does not create a failure ledger row. *)
platform_failure : string -> unit
; (* Tears the app down; returns the final patch batch. *)
dispose : unit -> string
Expand All @@ -51,3 +51,13 @@ external wakeup : unit -> unit = "journal_ml_wakeup"
(* Forwards one LJP2 request envelope to the host platform bridge (the host
answers asynchronously through [platform_response]). *)
external platform_request : string -> unit = "journal_ml_platform_request"

(** Decode a versioned, finite native failure (LJF1) or a validated legacy LJP2
request. The result contains a known request tag and fixed reason code only;
no request body or exception text is returned. LJF1 also includes bounded
account/presentation generations. Tag zero is decode failure. *)
val decode_platform_failure : string -> (int * string * (int64 * int64) option) option
val platform_failure_message : string -> string
(** Recognized native diagnostic family and finite subcause. Unrecognized
suffixes use a fixed unknown reason; arbitrary payloads are never echoed. *)
val native_diagnostic : string -> (string * string) option
Loading
Loading