virtual scroller finished

This commit is contained in:
lbfalvy committed 2026-08-18 20:00:24 +00:00
1 parent e5112c9cf9
commit 0b9a86d56c
21 files changed
+505 -278

No files matched your search

+18 -11
View File
@@ -7,11 +7,11 @@ use std::sync::Arc;
use actix_cors::Cors;
use actix_web::middleware::Logger;
use actix_web::{App, Error, HttpRequest, HttpResponse, HttpServer, Responder, get, post, rt, web};
use actix_ws::{AggregatedMessage, CloseCode, ProtocolError};
use actix_ws::{AggregatedMessage, ProtocolError};
use async_event::Event;
use futures::lock::Mutex;
use futures::stream::StreamExt as _;
use futures::{FutureExt, stream_select};
use futures::stream_select;
#[derive(Clone)]
struct Message {
@@ -43,10 +43,8 @@ async fn read(
eprintln!("Listening on {topic:?}");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
let stream = stream
.aggregate_continuations()
// aggregate continuation frames up to 1MiB
.max_continuation_size(2_usize.pow(20));
// aggregate continuation frames up to 1MiB
let stream = stream.aggregate_continuations().max_continuation_size(2_usize.pow(20));
// start task but don't wait for it
rt::spawn(async move {
@@ -89,11 +87,15 @@ async fn read(
let Some(ev) = events.next().await else { break 'sock_loop None };
match ev {
SubscrEvent::Message(Err(e)) => {
eprintln!("{e}");
eprintln!("Socket closed: {e}");
break 'sock_loop None;
},
SubscrEvent::Message(Ok(AggregatedMessage::Ping(msg))) => session.pong(&msg).await.unwrap(),
SubscrEvent::Message(Ok(AggregatedMessage::Pong(_))) => (),
SubscrEvent::Message(Ok(AggregatedMessage::Close(Some(actix_ws::CloseReason {
code: actix_ws::CloseCode::Away,
..
})))) => (),
SubscrEvent::Message(Ok(AggregatedMessage::Close(Some(reason)))) => eprintln!("{reason:?}"),
SubscrEvent::Message(Ok(AggregatedMessage::Close(_))) => (),
SubscrEvent::Message(Ok(AggregatedMessage::Text(tx))) => {
@@ -103,6 +105,9 @@ async fn read(
code: actix_ws::CloseCode::Normal,
description: Some("User requested shutdown by sending \"exit\"".to_string()),
});
} else if msg == "reset" {
*topic.backlog.lock().unwrap() = Vec::new();
topic.pushed.notify_all();
} else if let Some(new_cursor) = msg.strip_prefix("cursor=") {
let Ok(new_cursor) = new_cursor.parse::<usize>() else {
break 'sock_loop Some(proto_error("Could not parse cursor offset"));
@@ -148,10 +153,8 @@ async fn write(
eprintln!("Listening on {topic:?}");
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
let mut stream = stream
.aggregate_continuations()
// aggregate continuation frames up to 1MiB
.max_continuation_size(2_usize.pow(20));
// aggregate continuation frames up to 1MiB
let mut stream = stream.aggregate_continuations().max_continuation_size(2_usize.pow(20));
// start task but don't wait for it
rt::spawn(async move {
@@ -171,6 +174,10 @@ async fn write(
.unwrap();
break;
},
Ok(AggregatedMessage::Text(msg)) if msg.trim() == "reset" => {
*topic.backlog.lock().unwrap() = Vec::new();
topic.pushed.notify_all();
},
Ok(AggregatedMessage::Binary(data)) => publish_on_topic(&topic, Message { data }).await,
Ok(AggregatedMessage::Text(msg)) =>
publish_on_topic(&topic, Message { data: msg.into_bytes() }).await,
+8 -7
View File
@@ -17,13 +17,14 @@ scoped-log = { version = "0.1.0", path = "../scoped-log" }
unsync-pipe = { version = "0.2.0", path = "../unsync-pipe" }
wasm-bindgen-futures = "0.4.76"
web-sys = { version = "0.3.103", features = [
"Storage",
"Window",
"Event",
"ScrollToOptions",
"ScrollBehavior",
"HtmlElement",
"EventListener",
"Storage",
"Window",
"Event",
"ScrollToOptions",
"ScrollIntoViewOptions",
"ScrollBehavior",
"HtmlElement",
"EventListener",
] }
[features]
+23
View File
@@ -0,0 +1,23 @@
#![allow(unused, reason = "debugging tools go in and out of use")]
use std::cell::RefCell;
use std::collections::VecDeque;
use std::rc::Rc;
use std::time::Duration;
use dioxus::core::use_hook;
use crate::web_bindings::now;
/// For debugging purposes, timestamps every rerender and hard crashes if it
/// sees N renders within dT time
pub fn use_breaker(nth: usize, within: Duration) {
let rerender_log = use_hook(|| Rc::new(RefCell::new(VecDeque::new())));
let mut rerenders = rerender_log.borrow_mut();
rerenders.push_front(now());
if let Some(reference) = rerenders.iter().nth(nth).cloned() {
rerenders.pop_back();
if now() - reference < Duration::from_secs(1) {
panic!("Breaker tripped; {nth} rerenders within {:.3}s", within.as_secs_f32())
}
}
}
+60
View File
@@ -0,0 +1,60 @@
use std::rc::Rc;
use checklings::{TraceInfo, TracedEvent};
use dioxus::prelude::*;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum LogItem {
Event(TracedEvent),
Message(Rc<String>),
}
impl LogItem {
pub fn msg(str: impl AsRef<str>) -> Self { Self::Message(Rc::new(str.as_ref().to_string())) }
}
#[component]
pub fn LogLine(id: u64, line: LogItem) -> Element {
rsx! {
div {
span {
display: "inline-block",
width: "5ex",
"{id}",
}
{match line {
LogItem::Message(msg) => rsx! { span { margin_left: "10px", pre { display: "inline-block", margin: 0, "{msg}" } } },
LogItem::Event(event) => rsx! { EventLine { event: event.clone() } },
}}
}
}
}
fn mk_path(info: &TraceInfo) -> String {
match &info.parent {
None => info.label.clone(),
Some(parent) => format!("{}/{}", mk_path(parent), info.label),
}
}
#[component]
fn EventLine(event: TracedEvent) -> Element {
let TracedEvent { info, kind } = event;
let path = info.as_deref().map(mk_path);
rsx! {
span {
margin_left: "10px",
{ if let Some(path) = path {
rsx!{
span {
margin_right: "10px",
class: "path",
"{path}"
}
}
} else {
rsx!{}
} }
"{kind:?}"
}
}
}
+62 -184
View File
@@ -1,40 +1,23 @@
use std::pin::pin;
use std::time::Duration;
use checklings::{TraceInfo, TracedEvent};
use dioxus::prelude::*;
use dioxus::web::WebEventExt;
use futures::AsyncWriteExt;
use futures::future::join;
use futures::stream::StreamExt;
use gloo_net::websocket::Message;
use gloo_net::websocket::futures::WebSocket;
use js_sys::wasm_bindgen::JsCast;
use orchid_api_traits::Decode;
use futures::prelude::*;
mod web_bindings;
mod debug;
mod log;
mod virtual_scroll;
mod web_bindings;
mod ws_reader;
use scoped_log::{NullLogger, with_logger};
use web_bindings::*;
use crate::log::{LogItem, LogLine};
use crate::virtual_scroll::{PinEnd, VirtualScroll};
use crate::web_bindings::{Console, get_local_storage, set_local_storage};
use crate::ws_reader::{LogAction, with_ws};
const MAIN_CSS: Asset = asset!("/assets/main.css");
fn main() { dioxus::launch(App); }
pub async fn sleep(duration: Duration) {
let p = js_sys::Promise::new(&mut |resolve: js_sys::Function, _reject: js_sys::Function| {
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
&resolve,
duration.as_millis().try_into().unwrap(),
)
.unwrap();
});
p.await.unwrap();
}
#[component]
fn App() -> Element {
let mut remote = use_signal(|| {
@@ -45,55 +28,26 @@ fn App() -> Element {
let mut topic = use_signal(|| get_local_storage("topic").unwrap_or_else(|| "main".to_string()));
use_effect(move || set_local_storage("topic", &topic()));
let mut log = use_signal(Vec::<Result<TracedEvent, String>>::new);
let mut log = use_signal(Vec::<LogItem>::new);
use_resource(move || async move {
let connect = use_coroutine(move |mut cx: UnboundedReceiver<()>| async move {
loop {
writeln!(Console, "Initiating websocket");
let url = format!("{}/topic/{}/read", remote(), topic());
let Ok(mut socket) = WebSocket::open(&url) else {
log.set(vec![Err(format!("Failed to construct websocket at {url}"))]);
break;
};
log.set(Vec::new());
writeln!(Console, "Websocket ready, waiting for messages");
let (mut writer, reader) = unsync_pipe::pipe(1024);
let (retry, ()) = join(
with_logger(NullLogger, async move {
while let Some(msg) = socket.next().await {
match msg {
Err(e) => {
writeln!(Console, "Failed to connect websocket, {e}");
log.write().push(Err(format!("connection error: {e}")));
sleep(Duration::from_secs(3)).await;
return true;
},
Ok(Message::Text(txt)) if txt == "reset" => log.set(Vec::new()),
Ok(Message::Text(txt)) => log.write().push(Err(format!("text: {txt}"))),
Ok(Message::Bytes(bytes)) =>
if let Err(e) = writer.write_all(&bytes).await {
writeln!(Console, "{e}");
},
}
}
false
}),
with_logger(NullLogger, async move {
let mut source = pin!(reader);
loop {
let event = (TracedEvent::decode(source.as_mut()).await.map_err(|e| e.to_string()))
.inspect_err(|e| writeln!(Console, "{e}"));
log.write().push(event);
}
}),
futures::future::join(
pin!(async { cx.next().await }),
pin!(with_ws(remote(), topic(), move |action| match action {
LogAction::Set(vec) => log.set(vec),
LogAction::Push(item) => log.push(item),
})),
)
.await;
if !retry {
break;
}
}
});
use_effect(move || {
let _ = (remote(), topic());
connect.send(());
});
rsx! {
// Global app resources
document::Link { rel: "stylesheet", href: MAIN_CSS }
@@ -137,65 +91,23 @@ fn App() -> Element {
}
#[component]
fn Log(rows: ReadSignal<Vec<Result<TracedEvent, String>>>) -> Element {
let mut scroll_area = use_signal(|| None);
let mut follow_once = use_signal(|| true);
let mut new_bot_idx = use_signal(|| None);
let mut new_top_idx = use_signal(|| None);
use_effect(move || {
let _rerun_trigger = rows.read().len();
if let Some(scroll) = &*scroll_area.read()
&& *follow_once.read()
&& !is_at_bottom(scroll, 2)
{
scroll_to_bottom(scroll, web_sys::ScrollBehavior::Instant);
follow_once.set(false);
}
});
let rows_g = rows.read();
const INITIAL_VIEW_SIZE: usize = 1000;
const OVERSCROLL_SIZE: usize = 10;
const LOAD_BATCH_SIZE: usize = 200;
// The log never un-loads a row that has been manually loaded
fn Log(rows: ReadSignal<Vec<LogItem>>) -> Element {
let bound = move |idx: i64| 0.max(idx.min(rows.len() as i64 - 1));
// bottom index either
// - extended via overscroll
// - implied by a specific top idx
// - assumed to be the end of the log
let bot_idx =
new_bot_idx().unwrap_or(new_top_idx().map_or(rows_g.len(), |b| b + INITIAL_VIEW_SIZE));
// top index either
// - extended via overscroll
// - offset from the end of the log
let top_idx = new_top_idx().unwrap_or(bot_idx.saturating_sub(INITIAL_VIEW_SIZE));
// overscroll areas cut from the ends of the span and cannot overlap
let midpoint = (top_idx + bot_idx) / 2;
let mut first_index = use_signal(|| 0.max(rows.len() as i64 - 2));
let mut last_index = use_signal(|| 0.max(rows.len() as i64 - 1));
// overscroll region info
// whether it's needed (whether there is anything to show)
let top_oscr_visible = 0 < top_idx;
let bot_oscr_visible = bot_idx < rows_g.len();
// where it ends into the sequence
let top_oscr_end = if top_oscr_visible { midpoint.min(top_idx + OVERSCROLL_SIZE) } else { 0 };
let bot_oscr_start = if bot_oscr_visible {
midpoint.max(bot_idx.saturating_sub(OVERSCROLL_SIZE))
let mut follow = use_signal(|| true);
let rendered_rows = if rows.is_empty() {
Vec::new()
} else {
rows_g.len()
};
// to what point it should extend the loaded region
let next_top_idx = top_idx.saturating_sub(LOAD_BATCH_SIZE);
let next_bot_idx = rows_g.len().max(bot_idx + LOAD_BATCH_SIZE);
// split the sequence into actual overscroll regions
let top_oscr = &rows_g[top_idx..top_oscr_end];
let mid_section = &rows_g[top_oscr_end..bot_oscr_start];
let bot_oscr = &rows_g[bot_oscr_start..bot_idx];
// render callback not cached
let render_line = |line: &Result<TracedEvent, String>| -> Element {
match line {
Err(msg) => rsx! { TextLine { msg: msg } },
Ok(event) => rsx! { EventLine { event: event.clone() }},
if follow() && last_index() + 1 < rows.len() as i64 {
last_index.set(rows.len() as i64 - 1);
}
rows.read()[first_index() as usize..=bound(last_index()) as usize].to_vec()
};
rsx! {
div {
class: "log",
@@ -206,71 +118,37 @@ fn Log(rows: ReadSignal<Vec<Result<TracedEvent, String>>>) -> Element {
position: "absolute",
bottom: "10px",
right: "10px",
onclick: move |_| follow_once.toggle(),
background_color: if follow_once() { "var(--bg-btn-emphasis)" } else { "var(--bg-btn)" },
{if follow_once() { rsx!{ "Stop following" } } else { rsx!{ "Follow" } }}
onclick: move |_| follow.toggle(),
background_color: if follow() { "var(--bg-btn-emphasis)" } else { "var(--bg-btn)" },
{if follow() { rsx!{ "Stop following" } } else { rsx!{ "Follow" } }}
}
div {
height: "100%",
overflow_y: "scroll",
onmounted: move |ev| {
let el = ev.as_web_event();
*scroll_area.write() = Some(el.dyn_into::<web_sys::HtmlElement>().unwrap());
VirtualScroll {
pin_end: if follow() { Some(PinEnd::End) } else { None },
can_extend_start: 0 < first_index(),
set_start: move |start: i64| {
let new = bound(start);
writeln!(Console, "Set start from {} to {} bounded {}", first_index(), start, new);
if new != first_index() {
first_index.set(new)
}
},
div {
onvisible: move |_| {
writeln!(Console, "Top overscroll visible right now");
if top_oscr_visible {
new_top_idx.set(Some(next_top_idx));
follow_once.set(false);
}
},
{top_oscr.iter().map(render_line)}
}
{mid_section.iter().map(render_line)}
div {
onvisible: move |_| if bot_oscr_visible {
new_bot_idx.set(Some(next_bot_idx))
},
{bot_oscr.iter().map(render_line)}
}
div {
height: "5px",
onvisible: move |_| if !follow_once() { follow_once.set(true) },
}
can_extend_end: last_index() < rows.len() as i64 - 1,
set_end: move |end: i64| {
let new = bound(end);
writeln!(Console, "Set end from {} to {} bounded {}", last_index(), end, new);
if new != last_index() {
last_index.set(new)
}
},
current: first_index()..=last_index(),
overscroll: 3..=30,
{rendered_rows.iter().enumerate().map(|(i, l)| rsx!{
LogLine {
id: first_index() as u64 + i as u64,
line: l.clone(),
}
})}
}
}
}
}
#[component]
fn TextLine(msg: String) -> Element {
rsx! {
div {
pre { "{msg}" }
}
}
}
fn mk_path(info: &TraceInfo) -> String {
match &info.parent {
None => info.label.clone(),
Some(parent) => format!("{}/{}", mk_path(parent), info.label),
}
}
#[component]
fn EventLine(event: TracedEvent) -> Element {
let TracedEvent { info, kind } = event;
let path = info.as_deref().map(mk_path);
rsx! {
div {
{ if let Some(path) = path {
rsx!{ span { class: "path", "{path}" } }
} else {
rsx!{}
} }
"{kind:?}"
}
}
}
+184 -10
View File
@@ -1,21 +1,195 @@
use std::range::Range;
use std::ops::RangeInclusive;
use dioxus::prelude::*;
use futures::Stream;
use dioxus::web::WebEventExt;
use js_sys::wasm_bindgen::JsCast;
use crate::web_bindings::Console;
// scroll_height = scroll_top + offset_height
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum PinEnd {
Start,
End,
}
impl PinEnd {
fn scroll_to(self, el: web_sys::HtmlElement) {
match self {
PinEnd::Start => el.set_scroll_top(0),
PinEnd::End => el.set_scroll_top(el.scroll_height() - el.offset_height()),
}
}
}
/// The virtual scroller makes the assumption that rendering overhead scales
/// with
///
/// when we call a set_* callback, we place a request. The host can signal
/// completion by updating current, and block further attempts by setting
/// can_extend_$1 to false. Retries must be expressed in the host.
///
/// For ideal experience, overscroll.start must be big enough for the fetch
/// function to complete before a very fast scrolling user gets through it.
///
/// For correctness, `overscroll.end - overscroll.start` must be at least 3x
/// times as large as the largest possible element. Failure to ensure this can
/// cause the scroller to freeze up while repeatedly extending and shrinking the
/// list
#[component]
pub fn virtual_scroll(
wants_prev: Option<Callback<i64, ()>>,
wants_next: Option<Callback<i64, ()>>,
pub fn VirtualScroll(
pin_end: ReadSignal<Option<PinEnd>>,
set_start: Callback<i64, ()>,
can_extend_start: ReadSignal<bool>,
set_end: Callback<i64, ()>,
can_extend_end: ReadSignal<bool>,
current: ReadSignal<RangeInclusive<i64>>,
children: Element,
overscroll: RangeInclusive<i8>,
) -> Element {
let (oscr_low, oscr_high) = overscroll.into_inner();
// limited-height element inside which we scroll
let mut view_area = use_signal(|| None::<web_sys::HtmlElement>);
use_effect(move || {
let Some(end) = pin_end() else { return };
let Some(el) = view_area() else { return };
let _ = current();
end.scroll_to(el);
});
let est_item_h = move || {
let total_count = current().end() - current().start();
const DEFAULT_ITEM_H: u64 = 15;
let Some(total_h) = view_area().map(|el| el.scroll_height()) else {
return DEFAULT_ITEM_H;
};
if total_count <= 0 {
return DEFAULT_ITEM_H;
}
total_h as u64 / total_count as u64
};
// the msot recent start and end values requested from the application
let mut wanted_start = use_signal(|| *current().start());
let mut wanted_end = use_signal(|| *current().end());
let mut prev_h = use_signal(|| None::<i32>);
use_effect(move || {
if wanted_start() != *current().start() {
return;
}
let Some(prev_height) = prev_h() else { return };
let Some(el) = view_area() else { return };
prev_h.set(None);
match pin_end() {
None => {
let delta = el.scroll_height() - prev_height;
el.set_scroll_top(el.scroll_top() + delta)
},
Some(end) => end.scroll_to(el),
}
});
let mut update_oscr = move || {
writeln!(Console, "Updating overscroll");
let Some(view_area) = view_area() else {
writeln!(Console, "view_area unset");
return;
};
let offset_height = view_area.offset_height();
let scroll_height = view_area.scroll_height();
let scroll_top = view_area.scroll_top();
let top_oscr = scroll_top;
let view_h = offset_height;
let bot_oscr = scroll_height - (offset_height + scroll_top);
let max_px = view_h * oscr_high as i32;
let min_px = view_h * oscr_low as i32;
let avg_px = (max_px + min_px) / 2;
// upper integer bound of ratio of two numbers
fn ceil_div(a: u64, b: u64) -> u64 {
let remainder_bit = if a.is_multiple_of(b) { 0 } else { 1 };
a / b + remainder_bit
}
// calculate by how much we need to grow or shrink a given end.
// Zero indicates no action
let oscr_correct = |oscr_px: i32, can_extend: bool| -> i64 {
if max_px < oscr_px {
let excess_px = oscr_px - avg_px;
0 - ceil_div(excess_px as u64, est_item_h()) as i64
} else if oscr_px < min_px && can_extend {
let missing_px = avg_px - oscr_px;
ceil_div(missing_px as u64, est_item_h()) as i64
} else {
0
}
};
// this return ensures that nothing can race an update to the top
if prev_h().is_some() {
writeln!(Console, "prev_h is still active");
return;
}
if pin_end() != Some(PinEnd::Start) {
writeln!(Console, "Updating start top_oscr={top_oscr}");
// the start of the list is extended by decreasing the index
let correct_start = *current().start() - oscr_correct(top_oscr, can_extend_start());
if correct_start != wanted_start() {
wanted_start.set(correct_start);
if pin_end() != Some(PinEnd::End) {
// scroll restoration is disabled in pinned mode, mostly to ease the transition
// out of pinned mode
prev_h.set(Some(scroll_height));
}
set_start(correct_start);
// since we queued an update to the top, we have to return before we would
// generate a racing update to the bottom
return;
}
}
if pin_end() != Some(PinEnd::End) {
// the end of the list is extended by increasing the index
let correct_end = *current().end() + oscr_correct(bot_oscr, can_extend_end());
if correct_end != wanted_end() {
wanted_end.set(correct_end);
set_end(correct_end);
}
}
};
use_effect(update_oscr);
rsx! {
div {
div {
{children}
}
}
div {
height: "100%",
overflow_y: "scroll",
onmounted: move |ev| {
if view_area().is_none() {
view_area.set(Some(ev.as_web_event().dyn_into::<web_sys::HtmlElement>().unwrap()))
}
},
onscrollend: move |_| {
if let Some(end) = pin_end() && let Some(el) = view_area() {
end.scroll_to(el)
}
},
onscroll: move |_| {
if let Some(end) = pin_end() && let Some(el) = view_area() {
end.scroll_to(el)
}
update_oscr()
},
div {
{children}
}
}
}
}
+19 -10
View File
@@ -1,20 +1,29 @@
use std::time::Duration;
pub fn window() -> web_sys::Window { web_sys::window().unwrap() }
pub fn local_storage() -> web_sys::Storage { window().local_storage().unwrap().unwrap() }
pub fn get_local_storage(key: &str) -> Option<String> { local_storage().get_item(key).unwrap() }
pub fn set_local_storage(key: &str, value: &str) { local_storage().set_item(key, value).unwrap() }
fn max_scroll(el: &web_sys::HtmlElement) -> i32 { el.scroll_height() - el.offset_height() }
pub fn is_at_bottom(el: &web_sys::HtmlElement, px_tolerance: u32) -> bool {
el.scroll_top().abs_diff(max_scroll(el)) < px_tolerance
}
pub fn scroll_to_bottom(el: &web_sys::HtmlElement, behavior: web_sys::ScrollBehavior) {
let opt = web_sys::ScrollToOptions::default();
opt.set_behavior(behavior);
opt.set_top(max_scroll(el) as f64);
el.scroll_to_with_scroll_to_options(&opt);
}
pub struct Console;
impl Console {
pub fn write_fmt(&self, fmt: std::fmt::Arguments) {
web_sys::console::log_1(&fmt.to_string().into());
}
}
pub async fn sleep(duration: Duration) {
let p = js_sys::Promise::new(&mut |resolve: js_sys::Function, _reject: js_sys::Function| {
web_sys::window()
.unwrap()
.set_timeout_with_callback_and_timeout_and_arguments_0(
&resolve,
duration.as_millis().try_into().unwrap(),
)
.unwrap();
});
p.await.unwrap();
}
/// Instant is not available so this is an offset from the Unix epoch
pub fn now() -> Duration { Duration::from_millis(js_sys::Date::now().trunc() as u64) }
+78
View File
@@ -0,0 +1,78 @@
use std::cell::RefCell;
use std::pin::pin;
use std::time::Duration;
use checklings::TracedEvent;
use futures::prelude::*;
use gloo_net::websocket::Message;
use gloo_net::websocket::futures::WebSocket;
use orchid_api_traits::Decode as _;
use scoped_log::{NullLogger, with_logger};
use crate::log::LogItem;
use crate::web_bindings::{Console, sleep};
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum LogAction {
Push(LogItem),
Set(Vec<LogItem>),
}
pub async fn with_ws(url: String, topic: String, mut log_action: impl FnMut(LogAction)) {
loop {
sleep(Duration::from_millis(100)).await;
let url = format!("{}/topic/{}/read", url, topic);
let Ok(mut socket) = WebSocket::open(&url) else {
log_action(LogAction::Set(vec![LogItem::msg(format!(
"Failed to construct websocket at {url}"
))]));
break;
};
log_action(LogAction::Set(vec![LogItem::msg(format!("Connected to {url}"))]));
let log_action = &RefCell::new(&mut log_action);
let (mut writer, reader) = unsync_pipe::pipe(1024);
let (retry, ()) = futures::future::join(
with_logger(NullLogger, async move {
while let Some(msg) = socket.next().await {
match msg {
Err(e) => {
writeln!(Console, "Failed to connect websocket, {e}");
log_action.borrow_mut()(LogAction::Push(LogItem::msg(format!(
"connection error: {e}"
))));
sleep(Duration::from_secs(3)).await;
return true;
},
Ok(Message::Text(txt)) if txt == "reset" =>
log_action.borrow_mut()(LogAction::Set(Vec::new())),
Ok(Message::Text(txt)) =>
log_action.borrow_mut()(LogAction::Push(LogItem::msg(format!("text: {txt}")))),
Ok(Message::Bytes(bytes)) =>
if let Err(e) = writer.write_all(&bytes).await {
writeln!(Console, "{e}");
break;
},
}
}
false
}),
with_logger(NullLogger, async move {
let mut source = pin!(reader);
loop {
match TracedEvent::decode(source.as_mut()).await {
Ok(ev) => log_action.borrow_mut()(LogAction::Push(LogItem::Event(ev))),
Err(e) => {
writeln!(Console, "{e}");
log_action.borrow_mut()(LogAction::Push(LogItem::msg(e.to_string())));
break;
},
}
}
}),
)
.await;
if !retry {
break;
}
}
}
+6 -4
View File
@@ -214,10 +214,12 @@ impl ToExpr for GenArg<'_> {
let state = unsafe { self.0.as_ref().unwrap() };
match (*state.borrow(), CLOSURE_DEPTH.get()) {
(ArgState::Serializing { depth }, total) => inherit(GExprKind::Arg(total - depth)),
(ArgState::Building, _) =>
panic!("Argument serialized before lambda. Likely an over-eager ToExpr impl"),
(ArgState::Ready, _) =>
unreachable!("The arg should never be available this long, the GenArg is a convenience"),
(ArgState::Building, _) => {
panic!("Argument serialized before lambda. Likely an over-eager ToExpr impl")
},
(ArgState::Ready, _) => {
unreachable!("The arg should never be available this long, the GenArg is a convenience")
},
}
}
}
+3 -2
View File
@@ -168,8 +168,9 @@ impl CmdRunner {
self.ctl.spawn(async move {
let delta = until - Utc::now();
match delta.to_std() {
Err(_) =>
writeln!(log("debug"), "Negative sleep found ({delta}), requeuing as instant"),
Err(_) => {
writeln!(log("debug"), "Negative sleep found ({delta}), requeuing as instant")
},
Ok(delay) => cx.sleep(delay).await,
};
queue.push(Task::RunCommand(expr));
+3 -2
View File
@@ -86,8 +86,9 @@ impl Expr {
api::ExpressionKind::NewAtom(a) => ExprKind::Atom(
AtomHand::from_api(&a.associate(cx.sys.id()), pos.clone(), &mut cx.sys.cx().clone()).await,
),
api::ExpressionKind::Slot(tk) =>
return cx.sys.cx().exprs.take_expr(tk).expect("Invalid slot"),
api::ExpressionKind::Slot(tk) => {
return cx.sys.cx().exprs.take_expr(tk).expect("Invalid slot");
},
api::ExpressionKind::Seq(a, b) => {
let (apsb, bpsb) = psb.split();
ExprKind::Seq(
+3 -2
View File
@@ -142,8 +142,9 @@ impl Extension {
api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => {
this.0.cx.exprs.take_expr(rel.1);
},
api::ExtHostNotif::Log(api::Log { category, message }) =>
write!(log(&es(category).await.expect("Log category stale")), "{message}"),
api::ExtHostNotif::Log(api::Log { category, message }) => {
write!(log(&es(category).await.expect("Log category stale")), "{message}")
},
api::ExtHostNotif::Sweeped(data) => {
for i in
join_all(data.strings.into_iter().map(async |s| {
+14 -23
View File
@@ -120,11 +120,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
let mut produced = VecDeque::new();
let mut sub_cx = cx.sub(cx.pos_from(tail), &mut produced);
if !lex_once(&mut sub_cx, exactly_one).boxed_local().await? {
return Err(mk_errv(
is!("Unexpected end of source text"),
":: cannot be the last token",
[SrcRange::new(start..cx.get_pos(), &cx.pcx.src())],
));
return Err(mk_errv(is!("Unexpected end of source text"), ":: cannot be the last token", [
SrcRange::new(start..cx.get_pos(), &cx.pcx.src()),
]));
}
let pos = sub_cx.get_pos();
let body = produced.pop_front().expect("lex_once returned true");
@@ -154,11 +152,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
let mut produced = VecDeque::new();
let mut sub_cx = cx.sub(cx.pos_from(tail), &mut produced);
if !lex_once(&mut sub_cx, exactly_one).boxed_local().await? {
return Err(mk_errv(
is!("Unexpected end of file"),
"Expected a lambda argument and body",
[SrcRange::new(start..cx.get_pos(), &cx.pcx.src())],
));
return Err(mk_errv(is!("Unexpected end of file"), "Expected a lambda argument and body", [
SrcRange::new(start..cx.get_pos(), &cx.pcx.src()),
]));
}
let pos = sub_cx.get_pos();
cx.set_pos(pos);
@@ -175,11 +171,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
while !cx.strip_char(*rp) {
let mut sub_cx = cx.sub(cx.get_pos(), &mut body);
if !lex_once(&mut sub_cx, false).boxed_local().await? {
return Err(mk_errv(
is!("unclosed paren"),
format!("this {lp} has no matching {rp}"),
[SrcRange::new(start..start + 1, &cx.pcx.src())],
));
return Err(mk_errv(is!("unclosed paren"), format!("this {lp} has no matching {rp}"), [
SrcRange::new(start..start + 1, &cx.pcx.src()),
]));
}
let pos = sub_cx.get_pos();
cx.set_pos(pos);
@@ -222,11 +216,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
}
return Ok(true);
} else {
return Err(mk_errv(
is!("Unrecognized character"),
"The following syntax is meaningless.",
[SrcRange::new(start..start + 1, &cx.pcx.src())],
));
return Err(mk_errv(is!("Unrecognized character"), "The following syntax is meaningless.", [
SrcRange::new(start..start + 1, &cx.pcx.src()),
]));
};
cx.produced.push_back(ParsTokTree { tok, sr: cx.sr_to(start) });
Ok(true)
@@ -254,9 +246,8 @@ pub async fn sys_lex(cx: &mut LexCx<'_>) -> Option<OrcRes<Vec<ParsTokTree>>> {
cx_g.set_pos(pos1);
match lex_res {
Ok(false) => {
let errv = mk_errv(is!("End of file"), "Unexpected end of source text", [
cx_g.sr_to(pos),
]);
let errv =
mk_errv(is!("End of file"), "Unexpected end of source text", [cx_g.sr_to(pos)]);
Err(errv.to_api())
},
Ok(true) => {
+1 -3
View File
@@ -119,9 +119,7 @@ pub async fn parse_item(
n => parse_exportable_item(cx, path, comments, false, n.clone(), postdisc).await,
},
Some(_) =>
Err(mk_errv(is!("Expected a line type"), "All lines must begin with a keyword", [
item.sr()
])),
Err(mk_errv(is!("Expected a line type"), "All lines must begin with a keyword", [item.sr()])),
None => unreachable!("These lines are filtered and aggregated in earlier stages"),
}
}
+3 -2
View File
@@ -54,10 +54,11 @@ impl Parser for LetLine {
let mut strat = LetParsStrat(Vec::new());
let aliased = match parse_tokv(tail, &mut strat).await {
Ok(aliased) => aliased,
Err(e) =>
Err(e) => {
return Ok(vec![ParsedLine::cnst(&line.sr(), &comments, exported, name.clone(), vec![
x_tok(gen_expr::bot(e)).await.at(line.sr()),
])]),
])]);
},
};
// Dealiased mactree with reflexive trees missing
let tpl = flex_tok(async move |path| {
+3 -2
View File
@@ -131,13 +131,14 @@ impl Parser for MacroLine {
) -> Result<MacTok, Self::Err> {
let ph = match &next.tok {
GenTok::Name(name) => Ph { name: name.clone(), kind },
_ =>
_ => {
return Err(
token_errv(next, "Expected placeholder name", |tok| {
format!("Expected a single name, found {tok}")
})
.await,
),
);
},
};
self.0.push((ph.clone(), next.sr.clone()));
Ok(MacTok::Ph(ph))
+6 -4
View File
@@ -110,14 +110,16 @@ pub async fn parse_tok<'a, S: MacParseStrat<'a> + 'a>(
return parse_tok(nested, strat).boxed_local().await;
},
},
GenTok::Reflexive(FlexTree::Existing(flex)) =>
return Ok(ParseTokRes::Done(strat.with_reflexive(*flex, tree.sr()).await?)),
GenTok::Reflexive(FlexTree::Existing(flex)) => {
return Ok(ParseTokRes::Done(strat.with_reflexive(*flex, tree.sr()).await?));
},
GenTok::Handle(expr) => match TAtom::<PhAtom>::try_from_expr(expr.clone()).await {
Err(_) => MacTok::Value(expr.clone()),
Ok(ta) => return Ok(ParseTokRes::PhNeedsArg(ta.value.get())),
},
GenTok::NewExpr(_) | GenTok::Reflexive(FlexTree::New(_)) =>
panic!("parse_tok can only process clean input, newly constructed atom found"),
GenTok::NewExpr(_) | GenTok::Reflexive(FlexTree::New(_)) => {
panic!("parse_tok can only process clean input, newly constructed atom found")
},
GenTok::LambdaHead(_) => panic!("Lambda-head handled in the sequence parser"),
GenTok::S(p, body) =>
MacTok::S(*p, parse_tokv(Snippet::new(tree, body), strat).boxed_local().await?),
+3 -5
View File
@@ -95,11 +95,9 @@ pub async fn gen_match_macro_lib() -> Vec<GenMember> {
}
eprintln!("Before base case created");
let base_case = lam(async |_| {
bot(mk_errv(
is!("No branches match"),
"None of the patterns matches this value",
[rules.pos()],
))
bot(mk_errv(is!("No branches match"), "None of the patterns matches this value", [
rules.pos(),
]))
})
.await
.create()
+3 -2
View File
@@ -49,10 +49,11 @@ impl Lexer for PhLexer {
}
match prio_s.parse::<u8>() {
Ok(priority) => (tail, PhKind::Vector { at_least_one, priority }),
Err(e) =>
Err(e) => {
return Err(mk_errv(is!("Invalid placeholder priority"), e.to_string(), [
ctx.pos_tt(line, tail)
])),
]));
},
}
};
Ok((tail, x_tok(new_atom(PhAtom(phkind))).await.at(ctx.pos_tt(line, tail))))
+2 -2
View File
@@ -1,15 +1,15 @@
pub mod binary;
pub mod boolean;
pub mod bottom;
pub mod number;
pub mod ops;
pub mod option;
pub mod protocol;
pub mod record;
pub mod reflection;
pub mod result;
pub mod std_system;
pub mod stream;
pub mod string;
pub mod time;
pub mod tuple;
pub mod result;
pub mod bottom;
+3 -3
View File
@@ -1,6 +1,5 @@
use checklings::{Role, init_telemetry, section};
use never::Never;
use orchid_base::deadlock_probe::init_spinlock_probe;
use orchid_base::tokio::TokioSpawn;
use orchid_base::{IStr, Receipt, ReqHandle};
use orchid_extension::{self as ox, OrcReader, OrcWriter};
@@ -389,8 +388,9 @@ fn main() -> io::Result<ExitCode> {
file.read_to_string(&mut buf).unwrap();
},
(None, Some(line)) => buf = line.clone(),
(None, None) | (Some(_), Some(_)) =>
return Err("`lex` expected exactly one of --file and --line".to_string()),
(None, None) | (Some(_), Some(_)) => {
return Err("`lex` expected exactly one of --file and --line".to_string());
},
};
let pcx = mk_parse_cx(cx, sym!(usercode), systems);
let lexemes = lex(is(&buf).await, &pcx).await.map_err(|e| e.to_string())?;