@browsersync/bslive 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +30 -2
- package/bin.js +3 -2
- package/bslive/Cargo.toml +16 -1
- package/bslive/build.rs +1 -1
- package/bslive/src/lib.rs +120 -4
- package/bsnext/Cargo.toml +21 -0
- package/bsnext/src/main.rs +110 -0
- package/crates/bsnext_client/Cargo.toml +8 -0
- package/crates/bsnext_client/build.rs +53 -0
- package/crates/bsnext_client/dist/index.js +6493 -0
- package/crates/bsnext_client/generated/dto.ts +116 -0
- package/crates/bsnext_client/generated/schema.ts +187 -0
- package/crates/bsnext_client/index.html +14 -0
- package/crates/bsnext_client/package-lock.json +2059 -0
- package/crates/bsnext_client/package.json +25 -0
- package/crates/bsnext_client/src/lib.rs +1 -0
- package/crates/bsnext_client/style.css +3 -0
- package/crates/bsnext_client/ts/console.ts +25 -0
- package/crates/bsnext_client/ts/index.ts +73 -0
- package/crates/bsnext_client/tsconfig.json +16 -0
- package/crates/bsnext_core/Cargo.toml +43 -0
- package/crates/bsnext_core/src/dir_loader.rs +230 -0
- package/crates/bsnext_core/src/dto.rs +281 -0
- package/crates/bsnext_core/src/handlers/mod.rs +1 -0
- package/crates/bsnext_core/src/handlers/proxy.rs +95 -0
- package/crates/bsnext_core/src/lib.rs +11 -0
- package/crates/bsnext_core/src/meta/mod.rs +5 -0
- package/crates/bsnext_core/src/not_found/mod.rs +2 -0
- package/crates/bsnext_core/src/not_found/not_found.html +20 -0
- package/crates/bsnext_core/src/not_found/not_found_service.rs +41 -0
- package/crates/bsnext_core/src/not_found/route_list.rs +49 -0
- package/crates/bsnext_core/src/panic_handler.rs +32 -0
- package/crates/bsnext_core/src/raw_loader.rs +226 -0
- package/crates/bsnext_core/src/server/actor.rs +92 -0
- package/crates/bsnext_core/src/server/error.rs +41 -0
- package/crates/bsnext_core/src/server/handler_change.rs +85 -0
- package/crates/bsnext_core/src/server/handler_listen.rs +157 -0
- package/crates/bsnext_core/src/server/handler_patch.rs +42 -0
- package/crates/bsnext_core/src/server/handler_routes_updated.rs +27 -0
- package/crates/bsnext_core/src/server/handler_stop.rs +38 -0
- package/crates/bsnext_core/src/server/mod.rs +10 -0
- package/crates/bsnext_core/src/server/router/mod.rs +112 -0
- package/crates/bsnext_core/src/server/router/tests.rs +204 -0
- package/crates/bsnext_core/src/server/signals.rs +11 -0
- package/crates/bsnext_core/src/server/state.rs +19 -0
- package/crates/bsnext_core/src/servers_supervisor/actor.rs +199 -0
- package/crates/bsnext_core/src/servers_supervisor/file_changed_handler.rs +41 -0
- package/crates/bsnext_core/src/servers_supervisor/get_servers_handler.rs +23 -0
- package/crates/bsnext_core/src/servers_supervisor/input_changed_handler.rs +21 -0
- package/crates/bsnext_core/src/servers_supervisor/mod.rs +6 -0
- package/crates/bsnext_core/src/servers_supervisor/start_handler.rs +82 -0
- package/crates/bsnext_core/src/servers_supervisor/stop_handler.rs +26 -0
- package/crates/bsnext_core/src/ws/mod.rs +164 -0
- package/crates/bsnext_example/Cargo.toml +10 -0
- package/crates/bsnext_example/src/basic.rs +51 -0
- package/crates/bsnext_example/src/lib.rs +26 -0
- package/crates/bsnext_example/src/lit.rs +37 -0
- package/crates/bsnext_example/src/md.rs +18 -0
- package/crates/bsnext_fs/Cargo.toml +22 -0
- package/crates/bsnext_fs/src/actor.rs +122 -0
- package/crates/bsnext_fs/src/buffered_debounce.rs +166 -0
- package/crates/bsnext_fs/src/filter.rs +30 -0
- package/crates/bsnext_fs/src/inner_fs_event_handler.rs +94 -0
- package/crates/bsnext_fs/src/lib.rs +141 -0
- package/crates/bsnext_fs/src/remove_path_handler.rs +46 -0
- package/crates/bsnext_fs/src/stop_handler.rs +15 -0
- package/crates/bsnext_fs/src/stream.rs +167 -0
- package/crates/bsnext_fs/src/test/mod.rs +213 -0
- package/crates/bsnext_fs/src/watch_path_handler.rs +67 -0
- package/crates/bsnext_fs/src/watcher.rs +348 -0
- package/crates/bsnext_input/Cargo.toml +22 -0
- package/crates/bsnext_input/src/input_test/mod.rs +151 -0
- package/crates/bsnext_input/src/lib.rs +153 -0
- package/crates/bsnext_input/src/md.rs +541 -0
- package/crates/bsnext_input/src/paths.rs +64 -0
- package/crates/bsnext_input/src/route.rs +185 -0
- package/crates/bsnext_input/src/route_manifest.rs +186 -0
- package/crates/bsnext_input/src/server_config.rs +88 -0
- package/crates/bsnext_input/src/target.rs +7 -0
- package/crates/bsnext_input/src/watch_opt_test/mod.rs +68 -0
- package/crates/bsnext_input/src/yml.rs +1 -0
- package/crates/bsnext_output/Cargo.toml +16 -0
- package/crates/bsnext_output/src/json.rs +11 -0
- package/crates/bsnext_output/src/lib.rs +25 -0
- package/crates/bsnext_output/src/pretty.rs +147 -0
- package/crates/bsnext_resp/Cargo.toml +13 -0
- package/crates/bsnext_resp/src/js/snippet.html +1 -0
- package/crates/bsnext_resp/src/js/ws.js +1 -0
- package/crates/bsnext_resp/src/lib.rs +79 -0
- package/crates/bsnext_system/Cargo.toml +29 -0
- package/crates/bsnext_system/src/args.rs +43 -0
- package/crates/bsnext_system/src/lib.rs +227 -0
- package/crates/bsnext_system/src/monitor.rs +241 -0
- package/crates/bsnext_system/src/monitor_any_watchables.rs +127 -0
- package/crates/bsnext_system/src/start_kind/snapshots/bsnext_system__start_kind__start_from_paths__test__test-2.snap +11 -0
- package/crates/bsnext_system/src/start_kind/snapshots/bsnext_system__start_kind__start_from_paths__test__test.snap +29 -0
- package/crates/bsnext_system/src/start_kind/start_from_example.rs +49 -0
- package/crates/bsnext_system/src/start_kind/start_from_inputs.rs +67 -0
- package/crates/bsnext_system/src/start_kind/start_from_paths.rs +51 -0
- package/crates/bsnext_system/src/start_kind.rs +56 -0
- package/crates/bsnext_system/src/startup.rs +51 -0
- package/crates/bsnext_tracing/Cargo.toml +10 -0
- package/crates/bsnext_tracing/src/lib.rs +89 -0
- package/examples/basic/input.yml +5 -0
- package/examples/basic/public/index.html +15 -0
- package/examples/basic/public/reset.css +52 -0
- package/examples/basic/public/script.js +1 -0
- package/examples/basic/public/styles.css +3 -0
- package/examples/kitchen-sink/a-file.md +1 -0
- package/examples/kitchen-sink/api/1.json +1 -0
- package/examples/kitchen-sink/api/2.json +3 -0
- package/examples/kitchen-sink/app.js +1 -0
- package/examples/kitchen-sink/input.html +185 -0
- package/examples/kitchen-sink/input.yml +133 -0
- package/examples/kitchen-sink/styles-2.css +3 -0
- package/examples/lit/index.html +21 -0
- package/examples/lit/input.yml +5 -0
- package/examples/lit/lit.js +82 -0
- package/examples/lit/package-lock.json +62 -0
- package/examples/lit/package.json +15 -0
- package/examples/md-single/frontmatter.md +35 -0
- package/examples/md-single/md-single.md +35 -0
- package/examples/openai/.nvm +1 -0
- package/examples/openai/build.mjs +21 -0
- package/examples/openai/index.html +13 -0
- package/examples/openai/input.yml +59 -0
- package/examples/openai/package-lock.json +720 -0
- package/examples/openai/package.json +21 -0
- package/examples/openai/src/index.js +21 -0
- package/examples/openai/sse/01.txt +8 -0
- package/examples/proxy-simple/input.yml +9 -0
- package/examples/single/input.yaml +26 -0
- package/index.d.ts +2 -0
- package/index.js +2 -1
- package/package.json +17 -17
- package/run.sh +6 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
use bsnext_input::route::{Route, RouteKind};
|
|
2
|
+
use bsnext_input::server_config::Identity;
|
|
3
|
+
use bsnext_input::{
|
|
4
|
+
server_config::{self},
|
|
5
|
+
Input,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
9
|
+
pub struct LitExample;
|
|
10
|
+
|
|
11
|
+
impl LitExample {
|
|
12
|
+
pub fn into_input(self, identity: Option<Identity>) -> Input {
|
|
13
|
+
let server = server_config::ServerConfig {
|
|
14
|
+
identity: identity.unwrap_or_else(Identity::named),
|
|
15
|
+
routes: vec![
|
|
16
|
+
Route {
|
|
17
|
+
path: "/".to_string(),
|
|
18
|
+
kind: RouteKind::Html {
|
|
19
|
+
html: include_str!("../../../examples/lit/index.html").to_owned(),
|
|
20
|
+
},
|
|
21
|
+
..Default::default()
|
|
22
|
+
},
|
|
23
|
+
Route {
|
|
24
|
+
path: "/lit.js".to_string(),
|
|
25
|
+
kind: RouteKind::Raw {
|
|
26
|
+
raw: include_str!("../../../examples/lit/lit.js").to_owned(),
|
|
27
|
+
},
|
|
28
|
+
..Default::default()
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
watchers: vec![],
|
|
32
|
+
};
|
|
33
|
+
Input {
|
|
34
|
+
servers: vec![server],
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
use bsnext_input::server_config::Identity;
|
|
2
|
+
use bsnext_input::{md, Input};
|
|
3
|
+
|
|
4
|
+
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
5
|
+
pub struct MdExample;
|
|
6
|
+
|
|
7
|
+
impl MdExample {
|
|
8
|
+
pub fn into_input(self, identity: Option<Identity>) -> Input {
|
|
9
|
+
let input_str = include_str!("../../../examples/md-single/md-single.md");
|
|
10
|
+
let mut input = md::md_to_input(input_str).expect("example cannot fail?");
|
|
11
|
+
let server = input
|
|
12
|
+
.servers
|
|
13
|
+
.first_mut()
|
|
14
|
+
.expect("example must have 1 server");
|
|
15
|
+
server.identity = identity.unwrap_or_else(Identity::named);
|
|
16
|
+
input
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "bsnext_fs"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
7
|
+
|
|
8
|
+
[dependencies]
|
|
9
|
+
notify = "6.1.1"
|
|
10
|
+
pin-project-lite = "0.2.14"
|
|
11
|
+
glob-match = { version = "0.2.1" }
|
|
12
|
+
|
|
13
|
+
actix = { workspace = true }
|
|
14
|
+
actix-rt = { workspace = true }
|
|
15
|
+
tokio = { workspace = true }
|
|
16
|
+
tokio-stream = { workspace = true }
|
|
17
|
+
futures = { workspace = true }
|
|
18
|
+
futures-util = { workspace = true }
|
|
19
|
+
tracing = { workspace = true }
|
|
20
|
+
tracing-subscriber = { workspace = true }
|
|
21
|
+
thiserror = { workspace = true }
|
|
22
|
+
tempfile = { workspace = true }
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
use crate::buffered_debounce::BufferedStreamOpsExt;
|
|
2
|
+
use crate::inner_fs_event_handler::{InnerChangeEvent, MultipleInnerChangeEvent};
|
|
3
|
+
use crate::stream::StreamOpsExt;
|
|
4
|
+
use crate::{watcher, Debounce, FsEvent, FsEventContext};
|
|
5
|
+
use actix::{Actor, Addr, AsyncContext, Recipient, Running};
|
|
6
|
+
use actix_rt::Arbiter;
|
|
7
|
+
use futures_util::StreamExt;
|
|
8
|
+
|
|
9
|
+
use std::path::{Path, PathBuf};
|
|
10
|
+
use std::sync::Arc;
|
|
11
|
+
|
|
12
|
+
use crate::filter::Filter;
|
|
13
|
+
use tokio::sync::{broadcast, mpsc};
|
|
14
|
+
use tokio_stream::wrappers::ReceiverStream;
|
|
15
|
+
|
|
16
|
+
pub struct FsWatcher {
|
|
17
|
+
pub watcher: Option<notify::FsEventWatcher>,
|
|
18
|
+
raw_fs_stream: Arc<broadcast::Sender<InnerChangeEvent>>,
|
|
19
|
+
pub receivers: Vec<Recipient<FsEvent>>,
|
|
20
|
+
pub ctx: FsEventContext,
|
|
21
|
+
pub debounce: Debounce,
|
|
22
|
+
pub filters: Vec<Filter>,
|
|
23
|
+
pub cwd: PathBuf,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl FsWatcher {
|
|
27
|
+
pub fn new(cwd: &Path, id: u64) -> Self {
|
|
28
|
+
// todo: does this need to be a broadcast::channel? would an unbounded mpsc be better?
|
|
29
|
+
let (raw_fs_sender, _) = broadcast::channel::<InnerChangeEvent>(1000);
|
|
30
|
+
|
|
31
|
+
Self {
|
|
32
|
+
watcher: None,
|
|
33
|
+
raw_fs_stream: Arc::new(raw_fs_sender),
|
|
34
|
+
receivers: vec![],
|
|
35
|
+
ctx: FsEventContext::Other { id },
|
|
36
|
+
cwd: cwd.to_path_buf(),
|
|
37
|
+
filters: vec![],
|
|
38
|
+
debounce: Default::default(),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn for_input(cwd: &Path, id: u64) -> Self {
|
|
43
|
+
let mut s = Self::new(cwd, id);
|
|
44
|
+
s.ctx = FsEventContext::InputFile { id };
|
|
45
|
+
s
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub fn with_debounce(&mut self, d: Debounce) {
|
|
49
|
+
self.debounce = d
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
pub fn with_filter(&mut self, f: Filter) {
|
|
53
|
+
self.filters.push(f)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn setup_fs_streams(&mut self, a: Addr<FsWatcher>) {
|
|
57
|
+
let raw_fs_events_sender = self.raw_fs_stream.clone();
|
|
58
|
+
let (debounce_sender, debounce_receiver) = mpsc::channel::<InnerChangeEvent>(1);
|
|
59
|
+
|
|
60
|
+
Arbiter::current().spawn({
|
|
61
|
+
let debounce = self.debounce;
|
|
62
|
+
async move {
|
|
63
|
+
match debounce {
|
|
64
|
+
Debounce::Trailing { duration } => {
|
|
65
|
+
let stream = ReceiverStream::new(debounce_receiver).debounce(duration);
|
|
66
|
+
let mut debounced_stream = Box::pin(stream);
|
|
67
|
+
while let Some(v) = debounced_stream.next().await {
|
|
68
|
+
a.do_send(v);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
Debounce::Buffered { duration } => {
|
|
72
|
+
let stream =
|
|
73
|
+
ReceiverStream::new(debounce_receiver).buffered_debounce(duration);
|
|
74
|
+
let mut debounced_stream = Box::pin(stream);
|
|
75
|
+
while let Some(v) = debounced_stream.next().await {
|
|
76
|
+
a.do_send(MultipleInnerChangeEvent { events: v })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Listen for noisy FS events and pump them into the broadcast channel
|
|
84
|
+
Arbiter::current().spawn({
|
|
85
|
+
async move {
|
|
86
|
+
let debounce_sender = debounce_sender.clone();
|
|
87
|
+
let mut raw_fs_receiver = raw_fs_events_sender.subscribe();
|
|
88
|
+
while let Ok(raw_event) = raw_fs_receiver.recv().await {
|
|
89
|
+
tracing::trace!(?raw_event);
|
|
90
|
+
match debounce_sender.send(raw_event).await {
|
|
91
|
+
Ok(_) => {}
|
|
92
|
+
Err(e) => tracing::error!(?e),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl Actor for FsWatcher {
|
|
101
|
+
type Context = actix::Context<Self>;
|
|
102
|
+
|
|
103
|
+
fn started(&mut self, ctx: &mut Self::Context) {
|
|
104
|
+
tracing::trace!(actor.name = "FsWatcher", actor.lifecyle = "started");
|
|
105
|
+
let self_address = ctx.address();
|
|
106
|
+
let rx2 = self.raw_fs_stream.clone();
|
|
107
|
+
match watcher::create_watcher(rx2, &self.cwd) {
|
|
108
|
+
Ok(watcher) => self.watcher = Some(watcher),
|
|
109
|
+
Err(e) => tracing::error!(?e, "could not create watcher"),
|
|
110
|
+
}
|
|
111
|
+
self.setup_fs_streams(self_address);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn stopping(&mut self, _ctx: &mut Self::Context) -> Running {
|
|
115
|
+
tracing::trace!(actor.name = "FsWatcher", actor.lifecyle = "stopping");
|
|
116
|
+
Running::Stop
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fn stopped(&mut self, _ctx: &mut Self::Context) {
|
|
120
|
+
tracing::trace!(actor.name = "FsWatcher", actor.lifecyle = "stopped");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
use core::pin::Pin;
|
|
2
|
+
use core::task::{Context, Poll};
|
|
3
|
+
use std::fmt::Debug;
|
|
4
|
+
use std::time::Duration;
|
|
5
|
+
|
|
6
|
+
use futures::{Future, Stream};
|
|
7
|
+
use pin_project_lite::pin_project;
|
|
8
|
+
|
|
9
|
+
use tokio::time::{Instant, Sleep};
|
|
10
|
+
|
|
11
|
+
use tracing::trace;
|
|
12
|
+
|
|
13
|
+
pin_project! {
|
|
14
|
+
#[must_use = "streams do nothing unless polled"]
|
|
15
|
+
pub struct BufferedDebounce<St: Stream> {
|
|
16
|
+
#[pin]
|
|
17
|
+
dropped_count: usize,
|
|
18
|
+
#[pin]
|
|
19
|
+
value: St,
|
|
20
|
+
#[pin]
|
|
21
|
+
delay: Sleep,
|
|
22
|
+
#[pin]
|
|
23
|
+
debounce_time: Duration,
|
|
24
|
+
#[pin]
|
|
25
|
+
last_state: Vec<St::Item>,
|
|
26
|
+
#[pin]
|
|
27
|
+
child_ended: bool
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub trait BufferedStreamOpsExt: Stream {
|
|
32
|
+
fn buffered_debounce(self, debounce_time: Duration) -> BufferedDebounce<Self>
|
|
33
|
+
where
|
|
34
|
+
Self: Sized + Unpin,
|
|
35
|
+
{
|
|
36
|
+
BufferedDebounce::new(self, debounce_time)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl<T: ?Sized> BufferedStreamOpsExt for T where T: Stream {}
|
|
41
|
+
|
|
42
|
+
impl<St> BufferedDebounce<St>
|
|
43
|
+
where
|
|
44
|
+
St: Stream + Unpin,
|
|
45
|
+
{
|
|
46
|
+
#[allow(dead_code)]
|
|
47
|
+
fn new(stream: St, debounce_time: Duration) -> BufferedDebounce<St> {
|
|
48
|
+
BufferedDebounce {
|
|
49
|
+
value: stream,
|
|
50
|
+
dropped_count: 0,
|
|
51
|
+
delay: tokio::time::sleep(debounce_time),
|
|
52
|
+
debounce_time,
|
|
53
|
+
last_state: vec![],
|
|
54
|
+
child_ended: false,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl<St, Item> Stream for BufferedDebounce<St>
|
|
60
|
+
where
|
|
61
|
+
St: Stream<Item = Item>,
|
|
62
|
+
Item: Clone + Unpin + Debug + 'static,
|
|
63
|
+
{
|
|
64
|
+
type Item = Vec<St::Item>;
|
|
65
|
+
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
66
|
+
let mut me = self.project();
|
|
67
|
+
trace!("+ polled!");
|
|
68
|
+
|
|
69
|
+
match me.value.poll_next(cx) {
|
|
70
|
+
Poll::Ready(Some(v)) => {
|
|
71
|
+
trace!("recorded a child value");
|
|
72
|
+
me.last_state.push(v);
|
|
73
|
+
*me.dropped_count += 1;
|
|
74
|
+
|
|
75
|
+
trace!("resetting the deadline to be `debounce_time` from `now`");
|
|
76
|
+
let dur = *me.debounce_time;
|
|
77
|
+
me.delay.as_mut().reset(Instant::now() + dur);
|
|
78
|
+
|
|
79
|
+
trace!("wake to ensure we're polled again");
|
|
80
|
+
cx.waker().wake_by_ref();
|
|
81
|
+
return Poll::Pending;
|
|
82
|
+
}
|
|
83
|
+
Poll::Ready(None) => {
|
|
84
|
+
trace!("child ended, nothing more to do?");
|
|
85
|
+
*me.child_ended = true;
|
|
86
|
+
}
|
|
87
|
+
Poll::Pending => {
|
|
88
|
+
trace!("child was pending, nothing to do");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
match me.delay.poll(cx) {
|
|
93
|
+
Poll::Ready(_) => {
|
|
94
|
+
trace!("timer elapsed");
|
|
95
|
+
if !me.last_state.is_empty() {
|
|
96
|
+
trace!("buffered {} events", me.dropped_count);
|
|
97
|
+
let v = me.last_state.drain(..).collect::<Vec<_>>();
|
|
98
|
+
*me.dropped_count = 0;
|
|
99
|
+
trace!("sending value");
|
|
100
|
+
Poll::Ready(Some(v))
|
|
101
|
+
} else if *me.child_ended {
|
|
102
|
+
Poll::Ready(None)
|
|
103
|
+
} else {
|
|
104
|
+
Poll::Pending
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
Poll::Pending => Poll::Pending,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
#[cfg(test)]
|
|
113
|
+
mod test {
|
|
114
|
+
use super::*;
|
|
115
|
+
use tokio::sync::mpsc;
|
|
116
|
+
use tokio::time::sleep;
|
|
117
|
+
use tokio::time::Instant;
|
|
118
|
+
use tokio_stream::wrappers::ReceiverStream;
|
|
119
|
+
use tokio_stream::StreamExt;
|
|
120
|
+
|
|
121
|
+
#[tokio::test]
|
|
122
|
+
async fn test_buffered_debounce_stream() {
|
|
123
|
+
let (tx, rx) = mpsc::channel::<&str>(10);
|
|
124
|
+
let handle = tokio::spawn(async move {
|
|
125
|
+
let events = ["A", "B", "C", "D", "E", "F"];
|
|
126
|
+
|
|
127
|
+
// 6 events all happening together
|
|
128
|
+
for evt in events {
|
|
129
|
+
tx.send(evt).await.unwrap();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// a gap in events, just under the debounce duration
|
|
133
|
+
sleep(Duration::from_millis(200)).await;
|
|
134
|
+
|
|
135
|
+
tx.send("G").await.unwrap();
|
|
136
|
+
tx.send("H").await.unwrap();
|
|
137
|
+
tx.send("I").await.unwrap();
|
|
138
|
+
tx.send("J").await.unwrap();
|
|
139
|
+
|
|
140
|
+
// drop the sender to complete the stream
|
|
141
|
+
drop(tx);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let start_time = Instant::now();
|
|
145
|
+
let stream = Box::pin(
|
|
146
|
+
ReceiverStream::new(rx)
|
|
147
|
+
.buffered_debounce(Duration::from_millis(100))
|
|
148
|
+
.collect::<Vec<_>>(),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
let results = stream.await;
|
|
152
|
+
let end_time = Instant::now();
|
|
153
|
+
let total_duration = end_time
|
|
154
|
+
.checked_duration_since(start_time)
|
|
155
|
+
.expect("checked");
|
|
156
|
+
|
|
157
|
+
println!("{:?}", results);
|
|
158
|
+
println!("duration: {:?}", total_duration);
|
|
159
|
+
|
|
160
|
+
assert!(handle.await.is_ok());
|
|
161
|
+
assert_eq!(
|
|
162
|
+
vec![vec!["A", "B", "C", "D", "E", "F"], vec!["G", "H", "I", "J"]],
|
|
163
|
+
results
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use crate::PathDescription;
|
|
2
|
+
use glob_match::glob_match;
|
|
3
|
+
|
|
4
|
+
#[derive(Debug, Clone)]
|
|
5
|
+
pub enum Filter {
|
|
6
|
+
None,
|
|
7
|
+
Extension { ext: String },
|
|
8
|
+
Glob { glob: String },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
impl PathFilter for Filter {
|
|
12
|
+
fn filter(&self, pd: &PathDescription) -> bool {
|
|
13
|
+
match self {
|
|
14
|
+
Filter::None => false,
|
|
15
|
+
Filter::Extension { ext } => pd
|
|
16
|
+
.absolute
|
|
17
|
+
.extension()
|
|
18
|
+
.is_some_and(|x| x.to_string_lossy() == *ext),
|
|
19
|
+
Filter::Glob { glob } => {
|
|
20
|
+
let target = pd.relative.unwrap_or(pd.absolute);
|
|
21
|
+
let did_match = glob_match(glob, target.to_string_lossy().to_string().as_str());
|
|
22
|
+
did_match
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
pub trait PathFilter {
|
|
29
|
+
fn filter(&self, pd: &PathDescription) -> bool;
|
|
30
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
use crate::actor::FsWatcher;
|
|
2
|
+
use crate::filter::PathFilter;
|
|
3
|
+
use crate::{
|
|
4
|
+
BufferedChangeEvent, ChangeEvent, FsEvent, FsEventKind, PathDescription, PathDescriptionOwned,
|
|
5
|
+
};
|
|
6
|
+
use actix::Handler;
|
|
7
|
+
use std::collections::BTreeSet;
|
|
8
|
+
use std::path::PathBuf;
|
|
9
|
+
use std::time::Instant;
|
|
10
|
+
|
|
11
|
+
#[derive(actix::Message, Hash, PartialEq, Eq, Ord, PartialOrd, Debug, Clone)]
|
|
12
|
+
#[rtype(result = "()")]
|
|
13
|
+
pub struct InnerChangeEvent {
|
|
14
|
+
pub absolute_path: PathBuf,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl Handler<InnerChangeEvent> for FsWatcher {
|
|
18
|
+
type Result = ();
|
|
19
|
+
fn handle(&mut self, msg: InnerChangeEvent, _ctx: &mut Self::Context) -> Self::Result {
|
|
20
|
+
tracing::debug!(?self.ctx, "InnerChangeEvent for FsWatcher");
|
|
21
|
+
tracing::debug!(" └ sending to {} receivers", self.receivers.len());
|
|
22
|
+
let relative = match msg.absolute_path.strip_prefix(&self.cwd) {
|
|
23
|
+
Ok(stripped) => stripped.to_path_buf(),
|
|
24
|
+
Err(e) => {
|
|
25
|
+
tracing::debug!(?e, "could not extract the CWD from a path");
|
|
26
|
+
msg.absolute_path.clone()
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
for x in &self.receivers {
|
|
30
|
+
x.do_send(FsEvent {
|
|
31
|
+
kind: FsEventKind::Change(ChangeEvent {
|
|
32
|
+
absolute_path: msg.absolute_path.clone(),
|
|
33
|
+
path: relative.clone(),
|
|
34
|
+
}),
|
|
35
|
+
ctx: self.ctx.clone(),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[derive(actix::Message, Hash, PartialEq, Eq, Ord, PartialOrd, Debug, Clone)]
|
|
42
|
+
#[rtype(result = "()")]
|
|
43
|
+
pub struct MultipleInnerChangeEvent {
|
|
44
|
+
pub events: Vec<InnerChangeEvent>,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
impl Handler<MultipleInnerChangeEvent> for FsWatcher {
|
|
48
|
+
type Result = ();
|
|
49
|
+
fn handle(&mut self, msg: MultipleInnerChangeEvent, _ctx: &mut Self::Context) -> Self::Result {
|
|
50
|
+
tracing::debug!(?self.ctx, "MultipleInnerChangeEvent for FsWatcher");
|
|
51
|
+
tracing::debug!(" └ got {} events to process", msg.events.len());
|
|
52
|
+
let now = Instant::now();
|
|
53
|
+
let unique = msg.events.iter().collect::<BTreeSet<_>>();
|
|
54
|
+
let original_len = msg.events.len();
|
|
55
|
+
let unique_len = unique.len();
|
|
56
|
+
tracing::debug!(" └ {} unique", unique.len());
|
|
57
|
+
|
|
58
|
+
let filtered = unique
|
|
59
|
+
.iter()
|
|
60
|
+
.map(|inner| PathDescription {
|
|
61
|
+
absolute: &inner.absolute_path,
|
|
62
|
+
relative: inner.absolute_path.strip_prefix(&self.cwd).ok(),
|
|
63
|
+
})
|
|
64
|
+
.filter(|pd| {
|
|
65
|
+
if self.filters.is_empty() {
|
|
66
|
+
true
|
|
67
|
+
} else {
|
|
68
|
+
self.filters.iter().any(|filter| filter.filter(pd))
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.collect::<Vec<PathDescription>>();
|
|
72
|
+
|
|
73
|
+
tracing::debug!(" └ accepted {} after filtering", filtered.len());
|
|
74
|
+
|
|
75
|
+
let time = Instant::now() - now;
|
|
76
|
+
tracing::debug!("took {}ms to process event", time.as_millis());
|
|
77
|
+
|
|
78
|
+
if filtered.is_empty() {
|
|
79
|
+
tracing::debug!("no changes to send. original: {original_len}, filtered: {unique_len}");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for recipient in &self.receivers {
|
|
84
|
+
let evt = FsEventKind::ChangeBuffered(BufferedChangeEvent {
|
|
85
|
+
// this might look expensive, but in reality not expecting more than 1 receiver
|
|
86
|
+
events: filtered.iter().map(PathDescriptionOwned::from).collect(),
|
|
87
|
+
});
|
|
88
|
+
recipient.do_send(FsEvent {
|
|
89
|
+
kind: evt,
|
|
90
|
+
ctx: self.ctx.clone(),
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
pub mod actor;
|
|
2
|
+
mod buffered_debounce;
|
|
3
|
+
pub mod filter;
|
|
4
|
+
pub mod inner_fs_event_handler;
|
|
5
|
+
pub mod remove_path_handler;
|
|
6
|
+
pub mod stop_handler;
|
|
7
|
+
mod stream;
|
|
8
|
+
#[cfg(test)]
|
|
9
|
+
mod test;
|
|
10
|
+
pub mod watch_path_handler;
|
|
11
|
+
mod watcher;
|
|
12
|
+
|
|
13
|
+
use std::path::{Path, PathBuf};
|
|
14
|
+
use std::time::Duration;
|
|
15
|
+
|
|
16
|
+
// use tokio_stream::StreamExt;
|
|
17
|
+
|
|
18
|
+
#[derive(Debug, Copy, Clone)]
|
|
19
|
+
pub enum Debounce {
|
|
20
|
+
Trailing { duration: Duration },
|
|
21
|
+
Buffered { duration: Duration },
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Default for Debounce {
|
|
25
|
+
fn default() -> Self {
|
|
26
|
+
Self::Trailing {
|
|
27
|
+
duration: Duration::from_millis(300),
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl Debounce {
|
|
33
|
+
pub fn trailing_ms(ms: u64) -> Self {
|
|
34
|
+
Self::Trailing {
|
|
35
|
+
duration: Duration::from_millis(ms),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
pub fn buffered_ms(ms: u64) -> Self {
|
|
39
|
+
Self::Buffered {
|
|
40
|
+
duration: Duration::from_millis(ms),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
impl Debounce {
|
|
46
|
+
pub fn duration(&self) -> &Duration {
|
|
47
|
+
match self {
|
|
48
|
+
Debounce::Trailing { duration } => duration,
|
|
49
|
+
Debounce::Buffered { duration } => duration,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, Clone)]
|
|
55
|
+
pub enum FsEventContext {
|
|
56
|
+
InputFile { id: u64 },
|
|
57
|
+
Other { id: u64 },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
impl FsEventContext {
|
|
61
|
+
pub fn id(&self) -> u64 {
|
|
62
|
+
match self {
|
|
63
|
+
FsEventContext::InputFile { id } => *id,
|
|
64
|
+
FsEventContext::Other { id } => *id,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl Default for FsEventContext {
|
|
70
|
+
fn default() -> Self {
|
|
71
|
+
Self::Other { id: 1 }
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
#[derive(actix::Message, Debug, Clone)]
|
|
76
|
+
#[rtype(result = "()")]
|
|
77
|
+
pub struct FsEvent {
|
|
78
|
+
pub kind: FsEventKind,
|
|
79
|
+
pub ctx: FsEventContext,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[derive(Debug, Clone)]
|
|
83
|
+
pub enum FsEventKind {
|
|
84
|
+
Change(ChangeEvent),
|
|
85
|
+
ChangeBuffered(BufferedChangeEvent),
|
|
86
|
+
PathAdded(PathAddedEvent),
|
|
87
|
+
PathRemoved(PathRemovedEvent),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[derive(actix::Message, Debug, Clone)]
|
|
91
|
+
#[rtype(result = "()")]
|
|
92
|
+
pub struct ChangeEvent {
|
|
93
|
+
pub absolute_path: PathBuf,
|
|
94
|
+
pub path: PathBuf,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
#[derive(Debug, Clone)]
|
|
98
|
+
pub struct PathDescription<'a> {
|
|
99
|
+
pub absolute: &'a Path,
|
|
100
|
+
pub relative: Option<&'a Path>,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[derive(Debug, Clone)]
|
|
104
|
+
pub struct PathDescriptionOwned {
|
|
105
|
+
pub absolute: PathBuf,
|
|
106
|
+
pub relative: Option<PathBuf>,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
impl<'a> From<&'a PathDescription<'_>> for PathDescriptionOwned {
|
|
110
|
+
fn from(value: &'a PathDescription<'_>) -> Self {
|
|
111
|
+
Self {
|
|
112
|
+
relative: value.relative.map(ToOwned::to_owned),
|
|
113
|
+
absolute: value.absolute.to_owned(),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#[derive(actix::Message, Debug, Clone)]
|
|
119
|
+
#[rtype(result = "()")]
|
|
120
|
+
pub struct BufferedChangeEvent {
|
|
121
|
+
pub events: Vec<PathDescriptionOwned>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[derive(actix::Message, Debug, Clone)]
|
|
125
|
+
#[rtype(result = "()")]
|
|
126
|
+
pub struct PathAddedEvent {
|
|
127
|
+
pub path: PathBuf,
|
|
128
|
+
pub debounce: Debounce,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
#[derive(actix::Message, Debug, Clone)]
|
|
132
|
+
#[rtype(result = "()")]
|
|
133
|
+
pub struct PathRemovedEvent {
|
|
134
|
+
pub path: PathBuf,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[derive(thiserror::Error, Debug)]
|
|
138
|
+
pub enum FsWatchError {
|
|
139
|
+
#[error("Watcher error, original error: {0}")]
|
|
140
|
+
Watcher(#[from] notify::Error),
|
|
141
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
use crate::actor::FsWatcher;
|
|
2
|
+
use crate::{FsEvent, FsEventKind, FsWatchError, PathRemovedEvent};
|
|
3
|
+
use actix::Handler;
|
|
4
|
+
use notify::Watcher;
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
|
|
7
|
+
#[derive(actix::Message)]
|
|
8
|
+
#[rtype(result = "Result<(), FsWatchError>")]
|
|
9
|
+
pub struct RemoveWatchPath {
|
|
10
|
+
pub path: PathBuf,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl Handler<RemoveWatchPath> for FsWatcher {
|
|
14
|
+
type Result = Result<(), FsWatchError>;
|
|
15
|
+
|
|
16
|
+
fn handle(&mut self, msg: RemoveWatchPath, _ctx: &mut Self::Context) -> Self::Result {
|
|
17
|
+
tracing::trace!(path = ?msg.path, "-> RemoveWatchPath");
|
|
18
|
+
if let Some(watcher) = self.watcher.as_mut() {
|
|
19
|
+
match watcher.unwatch(&msg.path) {
|
|
20
|
+
Ok(_) => {
|
|
21
|
+
tracing::info!(?msg.path, "removed!");
|
|
22
|
+
let relative = match msg.path.strip_prefix(&self.cwd) {
|
|
23
|
+
Ok(stripped) => stripped.to_path_buf(),
|
|
24
|
+
Err(e) => {
|
|
25
|
+
tracing::debug!(?e, "could not extract the CWD from a path");
|
|
26
|
+
msg.path.clone()
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
for recip in &self.receivers {
|
|
30
|
+
let evt = FsEventKind::PathRemoved(PathRemovedEvent {
|
|
31
|
+
path: relative.clone(),
|
|
32
|
+
});
|
|
33
|
+
recip.do_send(FsEvent {
|
|
34
|
+
kind: evt,
|
|
35
|
+
ctx: self.ctx.clone(),
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
Err(e) => {
|
|
40
|
+
tracing::debug!(?e, "could not remove");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
Ok(())
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
use crate::actor::FsWatcher;
|
|
2
|
+
use actix::{ActorContext, Handler};
|
|
3
|
+
|
|
4
|
+
#[derive(actix::Message, Debug, Clone)]
|
|
5
|
+
#[rtype(result = "()")]
|
|
6
|
+
pub struct StopWatcher;
|
|
7
|
+
|
|
8
|
+
impl Handler<StopWatcher> for FsWatcher {
|
|
9
|
+
type Result = ();
|
|
10
|
+
|
|
11
|
+
fn handle(&mut self, _msg: StopWatcher, ctx: &mut Self::Context) -> Self::Result {
|
|
12
|
+
self.watcher = None;
|
|
13
|
+
ctx.stop();
|
|
14
|
+
}
|
|
15
|
+
}
|