mirror of
https://github.com/enso-org/enso.git
synced 2025-01-02 10:01:43 +03:00
Reconnecting WebSocket (https://github.com/enso-org/ide/pull/1537)
Original commit: a185215c71
This commit is contained in:
parent
99b0ae7ecb
commit
93c54c777b
@ -62,8 +62,9 @@ features = [
|
||||
'CloseEvent',
|
||||
'Document',
|
||||
'Element',
|
||||
"ErrorEvent",
|
||||
"MessageEvent",
|
||||
'ErrorEvent',
|
||||
'EventTarget',
|
||||
'MessageEvent',
|
||||
'HtmlElement',
|
||||
'Node',
|
||||
'WebSocket',
|
||||
|
@ -42,5 +42,6 @@ pub enum TransportEvent {
|
||||
/// A socket has been opened.
|
||||
Opened,
|
||||
/// A socket has been closed by the peer.
|
||||
/// This event may be also emitted when reconnecting has failed.
|
||||
Closed,
|
||||
}
|
||||
|
@ -29,6 +29,11 @@ impl TestWithLocalPoolExecutor {
|
||||
Self {executor,running_task_count}
|
||||
}
|
||||
|
||||
/// Check if there are any uncompleted tasks in the pool.
|
||||
pub fn has_ongoing_task(&self) -> bool {
|
||||
self.running_task_count.get() > 0
|
||||
}
|
||||
|
||||
/// Spawn new task in executor.
|
||||
pub fn run_task<Task>(&mut self, task: Task)
|
||||
where Task : Future<Output=()> + 'static {
|
||||
@ -47,7 +52,7 @@ impl TestWithLocalPoolExecutor {
|
||||
pub fn when_stalled<Callback>(&mut self, callback:Callback)
|
||||
where Callback : FnOnce() {
|
||||
self.run_until_stalled();
|
||||
if self.running_task_count.get() > 0 {
|
||||
if self.has_ongoing_task() {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
@ -59,7 +64,7 @@ impl TestWithLocalPoolExecutor {
|
||||
pub fn when_stalled_run_task<Task>(&mut self, task : Task)
|
||||
where Task : Future<Output=()> + 'static {
|
||||
self.run_until_stalled();
|
||||
if self.running_task_count.get() > 0 {
|
||||
if self.has_ongoing_task() {
|
||||
self.run_task(task);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
//! Module defining `JsExecutor` - an executor that tries running until stalled
|
||||
//! on each animation frame callback call.
|
||||
|
||||
pub mod test;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use ensogl::control::callback;
|
||||
|
8
gui/src/rust/ide/src/executor/web/test.rs
Normal file
8
gui/src/rust/ide/src/executor/web/test.rs
Normal file
@ -0,0 +1,8 @@
|
||||
//! Utilities for tests related to the web-based executors.
|
||||
|
||||
/// Set up a global animation-frame-based executor.
|
||||
/// Leaks it handle so it will run indefinitely.
|
||||
/// To be used in asynchronous wasm tests.
|
||||
pub fn setup_and_forget() {
|
||||
std::mem::forget(crate::initializer::setup_global_executor());
|
||||
}
|
@ -279,8 +279,8 @@ async fn create_project_model
|
||||
) -> FallibleResult<model::Project> {
|
||||
info!(logger, "Establishing Language Server connection.");
|
||||
let client_id = Uuid::new_v4();
|
||||
let json_ws = WebSocket::new_opened(logger,json_endpoint).await?;
|
||||
let binary_ws = WebSocket::new_opened(logger,binary_endpoint).await?;
|
||||
let json_ws = WebSocket::new_opened(logger,&json_endpoint).await?;
|
||||
let binary_ws = WebSocket::new_opened(logger,&binary_endpoint).await?;
|
||||
let client_json = language_server::Client::new(json_ws);
|
||||
let client_binary = binary::Client::new(logger,binary_ws);
|
||||
crate::executor::global::spawn(client_json.runner());
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use ensogl_system_web::closure::storage::OptionalFmMutClosure;
|
||||
use ensogl_system_web::js_to_string;
|
||||
use ensogl_system_web::event::listener::Slot;
|
||||
use failure::Error;
|
||||
use futures::channel::mpsc;
|
||||
use json_rpc::Transport;
|
||||
@ -11,9 +11,6 @@ use json_rpc::TransportEvent;
|
||||
use utils::channel;
|
||||
use wasm_bindgen::JsCast;
|
||||
use web_sys::BinaryType;
|
||||
use web_sys::CloseEvent;
|
||||
use web_sys::Event;
|
||||
use web_sys::MessageEvent;
|
||||
|
||||
|
||||
|
||||
@ -36,6 +33,14 @@ pub enum ConnectingError {
|
||||
FailedToConnect,
|
||||
}
|
||||
|
||||
impl ConnectingError {
|
||||
/// Create a `ConstructionError` value from a JS value describing an error.
|
||||
pub fn construction_error(js_val:impl AsRef<JsValue>) -> Self {
|
||||
let text = js_to_string(js_val);
|
||||
ConnectingError::ConstructionError(text)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error that may occur when attempting to send the data over WebSocket
|
||||
/// transport.
|
||||
#[derive(Clone,Debug,Fail)]
|
||||
@ -51,7 +56,7 @@ pub enum SendingError {
|
||||
impl SendingError {
|
||||
/// Constructs from the error yielded by one of the JS's WebSocket sending functions.
|
||||
pub fn from_send_error(error:JsValue) -> SendingError {
|
||||
SendingError::FailedToSend(js_to_string(error))
|
||||
SendingError::FailedToSend(js_to_string(&error))
|
||||
}
|
||||
}
|
||||
|
||||
@ -97,54 +102,204 @@ impl State {
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === JS Events ===
|
||||
// =================
|
||||
|
||||
/// Description of events that can be emitted by JS WebSocket.
|
||||
pub mod event {
|
||||
use super::*;
|
||||
use ensogl_system_web::event::Type;
|
||||
|
||||
/// Represents WebSocket.open event.
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum Open{}
|
||||
impl Type for Open {
|
||||
type Interface = web_sys::Event;
|
||||
type Target = web_sys::WebSocket;
|
||||
const NAME:&'static str = "open";
|
||||
}
|
||||
|
||||
/// Represents WebSocket.close event.
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum Close{}
|
||||
impl Type for Close {
|
||||
type Interface = web_sys::CloseEvent;
|
||||
type Target = web_sys::WebSocket;
|
||||
const NAME:&'static str = "close";
|
||||
}
|
||||
|
||||
/// Represents WebSocket.message event.
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum Message{}
|
||||
impl Type for Message {
|
||||
type Interface = web_sys::MessageEvent;
|
||||
type Target = web_sys::WebSocket;
|
||||
const NAME:&'static str = "message";
|
||||
}
|
||||
|
||||
/// Represents WebSocket.error event.
|
||||
#[derive(Clone,Copy,Debug)]
|
||||
pub enum Error{}
|
||||
impl Type for Error {
|
||||
type Interface = web_sys::Event;
|
||||
type Target = web_sys::WebSocket;
|
||||
const NAME:&'static str = "error";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Model ===
|
||||
// =============
|
||||
|
||||
/// An owning wrapper over JS `WebSocket` object and callbacks to its signals.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug)]
|
||||
#[allow(missing_docs)]
|
||||
struct Model {
|
||||
// === User-provided callbacks ===
|
||||
pub on_close : Slot<event::Close>,
|
||||
pub on_message : Slot<event::Message>,
|
||||
pub on_open : Slot<event::Open>,
|
||||
pub on_error : Slot<event::Error>,
|
||||
|
||||
|
||||
// === Internal ===
|
||||
pub logger : Logger,
|
||||
pub socket : web_sys::WebSocket,
|
||||
/// Special callback on "close" event. As it must be invoked after `on_close`, care should be
|
||||
/// taken to keep it registered as an event listener *after* `on_close` registration.
|
||||
/// By default `Model` takes care of it by itself.
|
||||
pub on_close_internal : Slot<event::Close>,
|
||||
/// When enabled, the WS will try to automatically reconnect whenever connection is lost.
|
||||
pub auto_reconnect : bool,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Wraps given WebSocket object.
|
||||
pub fn new(socket:web_sys::WebSocket, logger:Logger) -> Model {
|
||||
socket.set_binary_type(BinaryType::Arraybuffer);
|
||||
Model {
|
||||
on_close : Slot::new(&socket, &logger),
|
||||
on_message : Slot::new(&socket, &logger),
|
||||
on_open : Slot::new(&socket, &logger),
|
||||
on_error : Slot::new(&socket, &logger),
|
||||
on_close_internal : Slot::new(&socket, &logger),
|
||||
auto_reconnect : true,
|
||||
logger,
|
||||
socket,
|
||||
}
|
||||
}
|
||||
|
||||
/// Close the socket.
|
||||
pub fn close(&mut self, reason:&str) -> Result<(),JsValue> {
|
||||
// If socket was manually requested to close, it should not try to reconnect then.
|
||||
self.auto_reconnect = false;
|
||||
let normal_closure = 1000;
|
||||
self.socket.close_with_code_and_reason(normal_closure,reason)?;
|
||||
self.clear_callbacks();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all the available callbacks.
|
||||
pub fn clear_callbacks(&mut self) {
|
||||
// We list explicitly all the fields, to get a compiler error when a new slot as added
|
||||
// but not handled here.
|
||||
#[allow(clippy::unneeded_field_pattern)]
|
||||
let Self{
|
||||
// Callback slots to be cleared.
|
||||
on_close, on_error, on_message, on_open, on_close_internal,
|
||||
// Explicitly ignored non-slot fields.
|
||||
auto_reconnect:_, logger:_, socket:_
|
||||
} = self;
|
||||
// We don't care if removing actually removed anything.
|
||||
// If callbacks were not set, then they are clear from the start.
|
||||
on_close.clear_callback();
|
||||
on_error.clear_callback();
|
||||
on_message.clear_callback();
|
||||
on_open.clear_callback();
|
||||
on_close_internal.clear_callback()
|
||||
}
|
||||
|
||||
/// Establish a new WS connection, using the same URL as the previous one.
|
||||
/// All callbacks will be transferred to the new connection.
|
||||
pub fn reconnect(&mut self) -> Result<(),JsValue> {
|
||||
if !self.auto_reconnect {
|
||||
return Err(js_sys::Error::new("Reconnecting has been disabled").into());
|
||||
}
|
||||
|
||||
let url = self.socket.url();
|
||||
info!(self.logger, "Reconnecting WS to {url}.");
|
||||
|
||||
let new_ws = web_sys::WebSocket::new(&url)?;
|
||||
|
||||
self.on_close. set_target(&new_ws);
|
||||
self.on_error. set_target(&new_ws);
|
||||
self.on_message. set_target(&new_ws);
|
||||
self.on_open. set_target(&new_ws);
|
||||
self.on_close_internal.set_target(&new_ws);
|
||||
self.socket = new_ws;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Model {
|
||||
fn drop(&mut self) {
|
||||
info!(self.logger, "Dropping WS model.");
|
||||
if let Err(e) = self.close("Rust Value has been dropped.") {
|
||||
error!(self.logger,"Error when closing socket due to being dropped: {js_to_string(&e)}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === WebSocket ===
|
||||
// =================
|
||||
|
||||
/// Wrapper over JS `WebSocket` object and callbacks to its signals.
|
||||
#[derive(Debug)]
|
||||
/// Wrapper over JS `WebSocket` meant for general use.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
pub struct WebSocket {
|
||||
#[allow(missing_docs)]
|
||||
pub logger : Logger,
|
||||
/// Handle to the JS `WebSocket` object.
|
||||
pub ws : web_sys::WebSocket,
|
||||
/// Handle to a closure connected to `WebSocket.onmessage`.
|
||||
pub on_message : OptionalFmMutClosure<MessageEvent>,
|
||||
/// Handle to a closure connected to `WebSocket.onclose`.
|
||||
pub on_close : OptionalFmMutClosure<CloseEvent>,
|
||||
/// Handle to a closure connected to `WebSocket.onopen`.
|
||||
pub on_open : OptionalFmMutClosure<Event>,
|
||||
/// Handle to a closure connected to `WebSocket.onerror`.
|
||||
pub on_error : OptionalFmMutClosure<Event>,
|
||||
pub logger : Logger,
|
||||
model : Rc<RefCell<Model>>,
|
||||
}
|
||||
|
||||
impl WebSocket {
|
||||
/// Wraps given WebSocket object.
|
||||
pub fn new
|
||||
(ws:web_sys::WebSocket, parent:impl AnyLogger, name:impl AsRef<str>) -> WebSocket {
|
||||
ws.set_binary_type(BinaryType::Arraybuffer);
|
||||
WebSocket {
|
||||
ws,
|
||||
logger : Logger::sub(parent,name),
|
||||
on_message : default(),
|
||||
on_close : default(),
|
||||
on_open : default(),
|
||||
on_error : default(),
|
||||
}
|
||||
/// Wrap given raw JS WebSocket object.
|
||||
pub fn new(ws:web_sys::WebSocket, parent:impl AnyLogger) -> WebSocket {
|
||||
let logger = Logger::sub(parent,ws.url());
|
||||
let model = Rc::new(RefCell::new(Model::new(ws,logger.clone())));
|
||||
WebSocket {logger,model}
|
||||
}
|
||||
|
||||
/// Establish connection with endpoint defined by the given URL and wrap it.
|
||||
/// Asynchronous, because it waits until connection is established.
|
||||
pub async fn new_opened
|
||||
(parent:impl AnyLogger, url:impl Str) -> Result<WebSocket,ConnectingError> {
|
||||
let ws = web_sys::WebSocket::new(url.as_ref()).map_err(|e| {
|
||||
ConnectingError::ConstructionError(js_to_string(e))
|
||||
})?;
|
||||
let mut wst = WebSocket::new(ws,&parent,url.into());
|
||||
(parent:impl AnyLogger, url:&str) -> Result<WebSocket,ConnectingError> {
|
||||
let ws = web_sys::WebSocket::new(url).map_err(ConnectingError::construction_error)?;
|
||||
let mut wst = WebSocket::new(ws,&parent);
|
||||
wst.wait_until_open().await?;
|
||||
Ok(wst)
|
||||
}
|
||||
|
||||
/// Generate a callback to be invoked when socket needs reconnecting.
|
||||
fn reconnect_trigger(&self) -> impl FnMut(web_sys::CloseEvent) {
|
||||
let model = Rc::downgrade(&self.model);
|
||||
let logger = self.logger.clone();
|
||||
move |_| {
|
||||
if let Some(model) = model.upgrade() {
|
||||
if let Err(e) = model.borrow_mut().reconnect() {
|
||||
error!(logger,"Failed to reconnect: {js_to_string(&e)}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Awaits until `open` signal has been emitted. Clears any callbacks on
|
||||
/// this `WebSocket`, if any has been set.
|
||||
async fn wait_until_open(&mut self) -> Result<(),ConnectingError> {
|
||||
@ -152,6 +307,7 @@ impl WebSocket {
|
||||
// We shall wait for whatever comes first.
|
||||
let (transmitter, mut receiver) = mpsc::unbounded::<Result<(),()>>();
|
||||
let transmitter_clone = transmitter.clone();
|
||||
|
||||
self.set_on_close(move |_| {
|
||||
// Note [mwu] Ignore argument, `CloseEvent` here contains rubbish
|
||||
// anyway, nothing useful to pass to caller. Error code or reason
|
||||
@ -164,7 +320,8 @@ impl WebSocket {
|
||||
|
||||
match receiver.next().await {
|
||||
Some(Ok(())) => {
|
||||
self.clear_callbacks();
|
||||
self.model.borrow_mut().clear_callbacks();
|
||||
self.model.borrow_mut().on_close_internal.set_callback(self.reconnect_trigger());
|
||||
info!(self.logger, "Connection opened.");
|
||||
Ok(())
|
||||
}
|
||||
@ -174,43 +331,35 @@ impl WebSocket {
|
||||
|
||||
/// Checks the current state of the connection.
|
||||
pub fn state(&self) -> State {
|
||||
State::query_ws(&self.ws)
|
||||
State::query_ws(&self.model.borrow().socket)
|
||||
}
|
||||
|
||||
fn with_borrow_mut_model<R>(&mut self, f:impl FnOnce(&mut Model) -> R) -> R {
|
||||
with(self.model.borrow_mut(), |mut model| f(model.deref_mut()))
|
||||
}
|
||||
|
||||
/// Sets callback for the `close` event.
|
||||
pub fn set_on_close(&mut self, f:impl FnMut(CloseEvent) + 'static) {
|
||||
self.on_close.wrap(f);
|
||||
self.ws.set_onclose(self.on_close.js_ref());
|
||||
pub fn set_on_close(&mut self, f:impl FnMut(web_sys::CloseEvent) + 'static) {
|
||||
self.with_borrow_mut_model(move |model| {
|
||||
model.on_close.set_callback(f);
|
||||
// Force internal callback to be after the user-defined one.
|
||||
model.on_close_internal.reattach();
|
||||
});
|
||||
}
|
||||
|
||||
/// Sets callback for the `error` event.
|
||||
pub fn set_on_error(&mut self, f:impl FnMut(Event) + 'static) {
|
||||
self.on_error.wrap(f);
|
||||
self.ws.set_onerror(self.on_error.js_ref());
|
||||
pub fn set_on_error(&mut self, f:impl FnMut(web_sys::Event) + 'static) {
|
||||
self.with_borrow_mut_model(move |model| model.on_error.set_callback(f))
|
||||
}
|
||||
|
||||
/// Sets callback for the `message` event.
|
||||
pub fn set_on_message(&mut self, f:impl FnMut(MessageEvent) + 'static) {
|
||||
self.on_message.wrap(f);
|
||||
self.ws.set_onmessage(self.on_message.js_ref());
|
||||
pub fn set_on_message(&mut self, f:impl FnMut(web_sys::MessageEvent) + 'static) {
|
||||
self.with_borrow_mut_model(move |model| model.on_message.set_callback(f))
|
||||
}
|
||||
|
||||
/// Sets callback for the `open` event.
|
||||
pub fn set_on_open(&mut self, f:impl FnMut(Event) + 'static) {
|
||||
self.on_open.wrap(f);
|
||||
self.ws.set_onopen(self.on_open.js_ref());
|
||||
}
|
||||
|
||||
/// Clears all the available callbacks.
|
||||
pub fn clear_callbacks(&mut self) {
|
||||
self.on_close .clear();
|
||||
self.on_error .clear();
|
||||
self.on_message.clear();
|
||||
self.on_open .clear();
|
||||
self.ws.set_onclose(None);
|
||||
self.ws.set_onerror(None);
|
||||
self.ws.set_onmessage(None);
|
||||
self.ws.set_onopen(None);
|
||||
pub fn set_on_open(&mut self, f:impl FnMut(web_sys::Event) + 'static) {
|
||||
self.with_borrow_mut_model(move |model| model.on_open.set_callback(f))
|
||||
}
|
||||
|
||||
/// Executes a given function with a mutable reference to the socket.
|
||||
@ -218,7 +367,9 @@ impl WebSocket {
|
||||
///
|
||||
/// Fails if the socket is not opened or if the sending function failed.
|
||||
/// The error from `F` shall be translated into `SendingError`.
|
||||
pub fn send_with_open_socket<F,R>(&mut self, f:F) -> Result<R,Error>
|
||||
///
|
||||
/// WARNING: `f` works under borrow_mut and must not give away control.
|
||||
fn send_with_open_socket<F,R>(&mut self, f:F) -> Result<R,Error>
|
||||
where F : FnOnce(&mut web_sys::WebSocket) -> Result<R,JsValue> {
|
||||
// Sending through the closed WebSocket can return Ok() with error only
|
||||
// appearing in the log. We explicitly check for this to get failure as
|
||||
@ -230,7 +381,7 @@ impl WebSocket {
|
||||
if state != State::Open {
|
||||
Err(SendingError::NotOpen(state).into())
|
||||
} else {
|
||||
let result = f(&mut self.ws);
|
||||
let result = f(&mut self.model.borrow_mut().socket);
|
||||
result.map_err(|e| SendingError::from_send_error(e).into())
|
||||
}
|
||||
}
|
||||
@ -238,13 +389,13 @@ impl WebSocket {
|
||||
|
||||
impl Transport for WebSocket {
|
||||
fn send_text(&mut self, message:&str) -> Result<(), Error> {
|
||||
info!(self.logger, "Sending text message of length {message.len()}");
|
||||
info!(self.logger, "Sending text message of length {message.len()}.");
|
||||
debug!(self.logger, "Message contents: {message}");
|
||||
self.send_with_open_socket(|ws| ws.send_with_str(message))
|
||||
}
|
||||
|
||||
fn send_binary(&mut self, message:&[u8]) -> Result<(), Error> {
|
||||
info!(self.logger, "Sending binary message of length {message.len()}");
|
||||
info!(self.logger, "Sending binary message of length {message.len()}.");
|
||||
debug!(self.logger,|| format!("Message contents: {:x?}", message));
|
||||
// TODO [mwu]
|
||||
// Here we workaround issue from wasm-bindgen 0.2.58:
|
||||
@ -291,3 +442,43 @@ impl Transport for WebSocket {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use ensogl::system::web;
|
||||
use std::time::Duration;
|
||||
|
||||
|
||||
/// Provisional code allowing testing WS behavior and its events.
|
||||
/// Keeping it for future debug purposes.
|
||||
/// To run uncomment attribute line and invoke:
|
||||
/// `cargo watch -- wasm-pack test .\ide\ --chrome -- websocket_test`
|
||||
//#[wasm_bindgen_test::wasm_bindgen_test]
|
||||
#[allow(dead_code)]
|
||||
async fn websocket_tests() {
|
||||
web::set_stdout();
|
||||
executor::web::test::setup_and_forget();
|
||||
let logger = DefaultTraceLogger::new("Test");
|
||||
info!(logger,"Started");
|
||||
|
||||
// Create WebSocket
|
||||
let ws = WebSocket::new_opened(&logger,"ws://localhost:30445").await;
|
||||
let mut ws = ws.expect("Couldn't connect to WebSocket server.");
|
||||
info!(logger,"WebSocket opened: {ws:?}");
|
||||
|
||||
// Log events
|
||||
let handler = ws.establish_event_stream().for_each(f!([logger](event) {
|
||||
info!(logger,"Socket emitted event: {event:?}");
|
||||
futures::future::ready(())
|
||||
}));
|
||||
|
||||
// Spawn task to process events stream.
|
||||
executor::global::spawn(handler);
|
||||
|
||||
// Close socket after some delay.
|
||||
web::sleep(Duration::from_secs(20)).await;
|
||||
info!(logger,"Finished");
|
||||
}
|
||||
}
|
||||
|
@ -277,7 +277,7 @@ impl Instance {
|
||||
use enso_frp::web::js_to_string;
|
||||
let logger = self.model.logger.clone();
|
||||
error!(logger,"Failed to trigger initial preprocessor update from JS: \
|
||||
{js_to_string(js_error)}");
|
||||
{js_to_string(&js_error)}");
|
||||
}
|
||||
self
|
||||
}
|
||||
|
@ -32,8 +32,12 @@ impl <Arg> OptionalFmMutClosure<Arg> {
|
||||
}
|
||||
|
||||
/// Stores the given closure.
|
||||
pub fn store(&mut self, closure:Closure<dyn FnMut(Arg)>) {
|
||||
pub fn store(&mut self, closure:Closure<dyn FnMut(Arg)>) -> &Function {
|
||||
self.closure = Some(closure);
|
||||
// TODO [mwu]: `insert` should be used when we bump rustc - and then get rid of unwrap.
|
||||
// Blocked by https://github.com/enso-org/ide/issues/1028
|
||||
// The `unwrap` call is safe, because the line above set closure to `Some`.
|
||||
self.js_ref().unwrap()
|
||||
}
|
||||
|
||||
/// Obtain JS reference to the closure (that can be passed e.g. as a callback
|
||||
@ -43,12 +47,12 @@ impl <Arg> OptionalFmMutClosure<Arg> {
|
||||
}
|
||||
|
||||
/// Wraps given function into a Closure.
|
||||
pub fn wrap(&mut self, f:impl ClosureFn<Arg>) {
|
||||
pub fn wrap(&mut self, f:impl ClosureFn<Arg>) -> &Function {
|
||||
let boxed = Box::new(f);
|
||||
// Note: [mwu] Not sure exactly why, but compiler sometimes require this
|
||||
// explicit type below and sometimes does not.
|
||||
let wrapped:Closure<dyn FnMut(Arg)> = Closure::wrap(boxed);
|
||||
self.store(wrapped);
|
||||
self.store(wrapped)
|
||||
}
|
||||
|
||||
/// Clears the current closure.
|
||||
@ -57,4 +61,20 @@ impl <Arg> OptionalFmMutClosure<Arg> {
|
||||
pub fn clear(&mut self) {
|
||||
self.closure = None;
|
||||
}
|
||||
|
||||
/// Register this closure as an event handler.
|
||||
/// No action is taken if there is no closure stored.
|
||||
pub fn add_listener<EventType:crate::event::Type>(&self, target:&EventType::Target) {
|
||||
if let Some(function) = self.js_ref() {
|
||||
EventType::add_listener(target, function)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister this closure as an event handler. The closure must be the same as when it was
|
||||
/// registered.
|
||||
pub fn remove_listener<EventType:crate::event::Type>(&self, target:&EventType::Target) {
|
||||
if let Some(function) = self.js_ref() {
|
||||
EventType::remove_listener(target, function)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
51
gui/src/rust/lib/system/web/src/event.rs
Normal file
51
gui/src/rust/lib/system/web/src/event.rs
Normal file
@ -0,0 +1,51 @@
|
||||
//! Utilities for DOM events.
|
||||
|
||||
pub mod listener;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use js_sys::Function;
|
||||
use web_sys::EventTarget;
|
||||
|
||||
|
||||
|
||||
// =============
|
||||
// === Event ===
|
||||
// =============
|
||||
|
||||
/// This trait represents a type of event that may fire from some specific JS `EventTarget`.
|
||||
///
|
||||
/// For example, `WebSocket.close` is such an event, where `close` is event type and `WebSocket` is
|
||||
/// the `EventTarget`.
|
||||
///
|
||||
/// The purpose is to increase type safety by grouping event type name, event target type and event
|
||||
/// value type together.
|
||||
///
|
||||
/// Typically this trait is to be implemented for uncreatable types, created for the sole
|
||||
/// purpose of denoting a particular event type within a context of an event target.
|
||||
pub trait Type {
|
||||
/// The event value -- i.e. the Rust type of a value that will be passed as an argument
|
||||
/// to the listener.
|
||||
/// For example `web_sys::CloseEvent`.
|
||||
type Interface : AsRef<web_sys::Event>;
|
||||
|
||||
/// The type of the EventTarget object that fires this type of event, e.g. `web_sys::WebSocket`.
|
||||
type Target : AsRef<EventTarget> + AsRef<JsValue> + Clone + PartialEq;
|
||||
|
||||
/// The type of the event as a string. For example `"close"`.
|
||||
const NAME:&'static str;
|
||||
|
||||
/// Add a given function to the event's target as an event listener. It will be called each
|
||||
/// time event fires until listener is removed through `remove_listener`.
|
||||
fn add_listener(target:&Self::Target, listener:&Function) {
|
||||
// The unwrap here is safe, as the `addEventListener` never throws.
|
||||
EventTarget::add_event_listener_with_callback(target.as_ref(), Self::NAME, listener).unwrap()
|
||||
}
|
||||
|
||||
/// Remove the event listener. The `add_listener` method should have been called before with
|
||||
/// the very same function argument.
|
||||
fn remove_listener(target:&Self::Target, listener:&Function) {
|
||||
// The unwrap here is safe, as the `addEventListener` never throws.
|
||||
EventTarget::remove_event_listener_with_callback(target.as_ref(), Self::NAME, listener).unwrap()
|
||||
}
|
||||
}
|
117
gui/src/rust/lib/system/web/src/event/listener.rs
Normal file
117
gui/src/rust/lib/system/web/src/event/listener.rs
Normal file
@ -0,0 +1,117 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::closure::storage::ClosureFn;
|
||||
use crate::closure::storage::OptionalFmMutClosure;
|
||||
|
||||
|
||||
|
||||
// ============
|
||||
// === Slot ===
|
||||
// ============
|
||||
|
||||
/// A Slot stores a callback and manages its connection with JS `EventTarget`.
|
||||
///
|
||||
/// Both callback and the target can be set independently using `set_target` and `set_callback`.
|
||||
/// Additionally, callback can be cleared at any point by `clear_callback`.
|
||||
///
|
||||
/// When both target and callback are set, slot ensures that the callback is registered as an
|
||||
/// event listener in the target.
|
||||
///
|
||||
/// When changing target, `Slot` reattaches callback.
|
||||
///
|
||||
/// `Slot` owns callback and wraps it into JS closure. `Slot` also keeps reference to the target,
|
||||
/// so it must not be leaked.
|
||||
#[derive(Derivative)]
|
||||
#[derivative(Debug(bound="EventType::Interface: Debug"))]
|
||||
pub struct Slot<EventType:crate::event::Type> {
|
||||
logger : Logger,
|
||||
#[derivative(Debug="ignore")]
|
||||
target : Option<EventType::Target>,
|
||||
js_closure : OptionalFmMutClosure<EventType::Interface>,
|
||||
}
|
||||
|
||||
impl<EventType:crate::event::Type> Slot<EventType> {
|
||||
/// Create a new `Slot`. As the initial target is provided, the listener will register once it
|
||||
/// gets a callback (see [[set_callback]]).
|
||||
pub fn new(target:&EventType::Target, logger:impl AnyLogger) -> Self {
|
||||
Self {
|
||||
logger : Logger::sub(logger, EventType::NAME),
|
||||
target : Some(target.clone()),
|
||||
js_closure : default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register the event listener if both target and callback are set.
|
||||
fn add_if_active(&mut self) {
|
||||
if let (Some(target), Some(function)) = (self.target.as_ref(), self.js_closure.js_ref()) {
|
||||
debug!(self.logger,"Attaching the callback.");
|
||||
EventType::add_listener(target, function)
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister the event listener if both target and callback are set.
|
||||
fn remove_if_active(&mut self) {
|
||||
if let (Some(target), Some(function)) = (self.target.as_ref(), self.js_closure.js_ref()) {
|
||||
debug!(self.logger,"Detaching the callback.");
|
||||
EventType::remove_listener(target, function)
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a new target.
|
||||
///
|
||||
/// If callback is set, it will be reattached as a listener to a newly set target.
|
||||
pub fn set_target(&mut self, target:&EventType::Target) {
|
||||
// Prevent spurious reattaching that could affect listeners order.
|
||||
if Some(target) != self.target.as_ref() {
|
||||
self.remove_if_active();
|
||||
self.target = Some(target.clone());
|
||||
self.add_if_active()
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear event target.
|
||||
///
|
||||
/// If callback is set, it will be unregistered.
|
||||
pub fn clear_target(&mut self, target:&EventType::Target) {
|
||||
// Prevent spurious reattaching that could affect listeners order.
|
||||
if Some(target) != self.target.as_ref() {
|
||||
self.remove_if_active();
|
||||
self.target = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Assign a new event callback closure and register it in the target.
|
||||
///
|
||||
/// If the listener was registered with the previous closure, it will unregister first.
|
||||
///
|
||||
/// Caveat: using this method will move the event listener to the end of the registered
|
||||
/// callbacks. This will affect the order of callback calls.
|
||||
pub fn set_callback(&mut self, f:impl ClosureFn<EventType::Interface>) {
|
||||
self.remove_if_active();
|
||||
self.js_closure.wrap(f);
|
||||
self.add_if_active()
|
||||
}
|
||||
|
||||
/// Erase the callback.
|
||||
///
|
||||
/// The stored closure will be dropped and event listener unregistered.
|
||||
pub fn clear_callback(&mut self) {
|
||||
self.remove_if_active();
|
||||
self.js_closure.clear();
|
||||
}
|
||||
|
||||
/// Detach and attach the listener to the target.
|
||||
///
|
||||
/// The purpose is to move this slot to the end of the listeners list.
|
||||
pub fn reattach(&mut self) {
|
||||
self.remove_if_active();
|
||||
self.add_if_active();
|
||||
}
|
||||
}
|
||||
|
||||
/// Unregister listener on drop.
|
||||
impl<EventType:crate::event::Type> Drop for Slot<EventType> {
|
||||
fn drop(&mut self) {
|
||||
self.remove_if_active();
|
||||
}
|
||||
}
|
@ -7,11 +7,14 @@
|
||||
|
||||
pub mod clipboard;
|
||||
pub mod closure;
|
||||
pub mod event;
|
||||
pub mod resize_observer;
|
||||
pub mod platform;
|
||||
|
||||
/// Common types that should be visible across the whole crate.
|
||||
pub mod prelude {
|
||||
pub use enso_logger::*;
|
||||
pub use enso_logger::DefaultInfoLogger as Logger;
|
||||
pub use enso_prelude::*;
|
||||
pub use wasm_bindgen::prelude::*;
|
||||
}
|
||||
@ -77,11 +80,15 @@ impl From<JsValue> for Error {
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
/// Converts given `JsValue` into a `String`. Uses JS's `String` function,
|
||||
/// see: https://www.w3schools.com/jsref/jsref_string.asp
|
||||
#[allow(unsafe_code)]
|
||||
#[wasm_bindgen(js_name="String")]
|
||||
pub fn js_to_string(s: JsValue) -> String;
|
||||
fn js_to_string_inner(s:&JsValue) -> String;
|
||||
}
|
||||
|
||||
/// Converts given `JsValue` into a `String`. Uses JS's `String` function,
|
||||
/// see: https://www.w3schools.com/jsref/jsref_string.asp
|
||||
pub fn js_to_string(s:impl AsRef<JsValue>) -> String {
|
||||
js_to_string_inner(s.as_ref())
|
||||
}
|
||||
|
||||
|
||||
@ -651,7 +658,7 @@ pub fn reflect_get_nested_string(target:&JsValue, keys:&[&str]) -> Result<String
|
||||
if tgt.is_undefined() {
|
||||
Err(Error("Key was not present in the target."))
|
||||
} else {
|
||||
Ok(js_to_string(tgt))
|
||||
Ok(js_to_string(&tgt))
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user