///|
// Each pending lane owns a monotonically increasing generation. A retry only
// runs while its generation is still current, so a remount using the same id
// can supersede stale work without storing callback state on `globalThis`.
#cfg(target="js")
let ui_after_mount_pending : Map[String, Map[String, Int]] = Map([])
///|
#cfg(target="js")
let ui_after_ready_pending : Map[String, Int] = Map([])
///|
#cfg(target="js")
let ui_after_operation_generation : Ref[Int] = Ref(0)
///|
#cfg(target="js")
fn ui_next_after_operation_generation() -> Int {
ui_after_operation_generation.val = ui_after_operation_generation.val + 1
ui_after_operation_generation.val
}
///|
#cfg(target="js")
fn ui_after_request_frame(callback : () -> Unit) -> Unit {
if ui_has_window() {
ignore(@dom.window().request_animation_frame(_ => callback()))
} else {
// Non-browser JS tests may not expose a Window. Retrying synchronously
// preserves the bounded behavior without reaching outside @dom.
callback()
}
}
///|
/// Run a DOM operation once an incremental view has actually mounted its
/// element. `create_state_with_init` may enqueue an AfterRender command before
/// the owning `Val::view` has inserted that element into the document. A newer
/// request with the same element id and purpose supersedes an older pending
/// retry, while distinct purposes may wait on the same element independently.
#cfg(target="js")
fn ui_after_mount(
id : String,
mounted : () -> Unit,
purpose? : String = "default",
) -> Unit {
let lanes = if ui_after_mount_pending.get(id) is Some(lanes) {
lanes
} else {
let lanes : Map[String, Int] = Map([])
ui_after_mount_pending[id] = lanes
lanes
}
let generation = ui_next_after_operation_generation()
lanes[purpose] = generation
fn is_current() -> Bool {
ui_after_mount_pending.get(id) is Some(current_lanes) &&
current_lanes.get(purpose) == Some(generation)
}
fn finish() -> Unit {
if is_current() {
lanes.remove(purpose)
if lanes.length() == 0 {
ui_after_mount_pending.remove(id)
}
}
}
fn try_mount(attempt : Int) -> Unit {
if !is_current() {
return
}
if !ui_has_document() {
finish()
} else if @dom.document().get_element_by_id(id).to_option() is Some(_) {
finish()
mounted()
} else if attempt >= 60 {
finish()
} else {
ui_after_request_frame(() => try_mount(attempt + 1))
}
}
try_mount(0)
}
///|
/// Retry a live-DOM operation until all of its own prerequisites are ready.
/// The key makes a newer operation supersede an older pending retry for the
/// same component and purpose, which prevents stale incremental instances from
/// installing callbacks after a same-id remount.
#cfg(target="js")
fn ui_after_ready(key : String, ready : () -> Bool raise) -> Unit {
if !ui_has_document() {
return
}
let generation = ui_next_after_operation_generation()
ui_after_ready_pending[key] = generation
fn is_current() -> Bool {
ui_after_ready_pending.get(key) == Some(generation)
}
fn finish() -> Unit {
if is_current() {
ui_after_ready_pending.remove(key)
}
}
fn try_ready(attempt : Int) -> Unit {
if !is_current() {
return
}
let complete = try ready() catch {
_ => false
} noraise {
value => value
}
if complete {
finish()
} else if attempt >= 60 {
finish()
} else {
ui_after_request_frame(() => try_ready(attempt + 1))
}
}
try_ready(0)
}