@browsersync/bslive 0.0.5 → 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 +32 -18
- package/bin.js +6 -0
- package/bslive/Cargo.toml +35 -0
- package/{build.rs → bslive/build.rs} +1 -1
- package/bslive/src/lib.rs +130 -0
- 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 +3 -1
- package/index.js +3 -2
- package/package.json +22 -19
- package/run.sh +6 -0
- package/src/lib.rs +0 -9
|
@@ -0,0 +1,167 @@
|
|
|
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 Debounce<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: Option<St::Item>,
|
|
26
|
+
#[pin]
|
|
27
|
+
child_ended: bool
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
pub trait StreamOpsExt: Stream {
|
|
32
|
+
fn debounce(self, debounce_time: Duration) -> Debounce<Self>
|
|
33
|
+
where
|
|
34
|
+
Self: Sized + Unpin,
|
|
35
|
+
{
|
|
36
|
+
Debounce::new(self, debounce_time)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl<T: ?Sized> StreamOpsExt for T where T: Stream {}
|
|
41
|
+
|
|
42
|
+
impl<St> Debounce<St>
|
|
43
|
+
where
|
|
44
|
+
St: Stream + Unpin,
|
|
45
|
+
{
|
|
46
|
+
#[allow(dead_code)]
|
|
47
|
+
fn new(stream: St, debounce_time: Duration) -> Debounce<St> {
|
|
48
|
+
Debounce {
|
|
49
|
+
value: stream,
|
|
50
|
+
dropped_count: 0,
|
|
51
|
+
delay: tokio::time::sleep(debounce_time),
|
|
52
|
+
debounce_time,
|
|
53
|
+
last_state: None,
|
|
54
|
+
child_ended: false,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
impl<St, Item> Stream for Debounce<St>
|
|
60
|
+
where
|
|
61
|
+
St: Stream<Item = Item>,
|
|
62
|
+
Item: Clone + Unpin + Debug + 'static,
|
|
63
|
+
{
|
|
64
|
+
type Item = 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 = Some(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
|
+
match (*me.last_state).clone() {
|
|
96
|
+
Some(v) => {
|
|
97
|
+
trace!("buffered {} events", me.dropped_count);
|
|
98
|
+
*me.last_state = None;
|
|
99
|
+
*me.dropped_count = 0;
|
|
100
|
+
trace!("sending value");
|
|
101
|
+
Poll::Ready(Some(v))
|
|
102
|
+
}
|
|
103
|
+
None => {
|
|
104
|
+
if *me.child_ended {
|
|
105
|
+
Poll::Ready(None)
|
|
106
|
+
} else {
|
|
107
|
+
Poll::Pending
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Poll::Pending => Poll::Pending,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[cfg(test)]
|
|
118
|
+
mod test {
|
|
119
|
+
use super::*;
|
|
120
|
+
use tokio::sync::mpsc;
|
|
121
|
+
use tokio::time::sleep;
|
|
122
|
+
use tokio::time::Instant;
|
|
123
|
+
use tokio_stream::wrappers::ReceiverStream;
|
|
124
|
+
use tokio_stream::StreamExt;
|
|
125
|
+
#[tokio::test]
|
|
126
|
+
async fn test_stream() {
|
|
127
|
+
let (tx, rx) = mpsc::channel::<&str>(10);
|
|
128
|
+
let handle = tokio::spawn(async move {
|
|
129
|
+
let events = ["A", "B", "C", "D", "E", "F"];
|
|
130
|
+
|
|
131
|
+
// 6 events all happening together
|
|
132
|
+
for evt in events {
|
|
133
|
+
tx.send(evt).await.unwrap();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// a gap in events, just under the debounce duration
|
|
137
|
+
sleep(Duration::from_millis(50)).await;
|
|
138
|
+
|
|
139
|
+
tx.send("G").await.unwrap();
|
|
140
|
+
tx.send("H").await.unwrap();
|
|
141
|
+
tx.send("I").await.unwrap();
|
|
142
|
+
tx.send("J").await.unwrap();
|
|
143
|
+
|
|
144
|
+
// drop the sender to complete the stream
|
|
145
|
+
drop(tx);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
let start_time = Instant::now();
|
|
149
|
+
let stream = Box::pin(
|
|
150
|
+
ReceiverStream::new(rx)
|
|
151
|
+
.debounce(Duration::from_millis(100))
|
|
152
|
+
.collect::<Vec<_>>(),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
let results = stream.await;
|
|
156
|
+
let end_time = Instant::now();
|
|
157
|
+
let total_duration = end_time
|
|
158
|
+
.checked_duration_since(start_time)
|
|
159
|
+
.expect("checked");
|
|
160
|
+
|
|
161
|
+
println!("{:?}", results);
|
|
162
|
+
println!("duration: {:?}", total_duration);
|
|
163
|
+
|
|
164
|
+
assert!(handle.await.is_ok());
|
|
165
|
+
assert_eq!(vec!["J"], results);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
use crate::actor::FsWatcher;
|
|
2
|
+
use crate::watch_path_handler::RequestWatchPath;
|
|
3
|
+
use crate::{Debounce, FsEvent, FsEventKind};
|
|
4
|
+
use actix::{Actor, Addr};
|
|
5
|
+
use std::fs::File;
|
|
6
|
+
use std::io::Write;
|
|
7
|
+
use std::path::{Path, PathBuf};
|
|
8
|
+
use std::time::Duration;
|
|
9
|
+
use tempfile::TempDir;
|
|
10
|
+
|
|
11
|
+
use crate::filter::Filter;
|
|
12
|
+
use tokio::time::sleep;
|
|
13
|
+
|
|
14
|
+
struct A {
|
|
15
|
+
events: Vec<FsEvent>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
impl Actor for A {
|
|
19
|
+
type Context = actix::Context<Self>;
|
|
20
|
+
}
|
|
21
|
+
#[derive(actix::Message)]
|
|
22
|
+
#[rtype(result = "Vec<FsEvent>")]
|
|
23
|
+
struct GetEvents;
|
|
24
|
+
|
|
25
|
+
impl actix::Handler<GetEvents> for A {
|
|
26
|
+
type Result = Vec<FsEvent>;
|
|
27
|
+
|
|
28
|
+
fn handle(&mut self, _msg: GetEvents, _ctx: &mut Self::Context) -> Self::Result {
|
|
29
|
+
self.events.clone()
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
impl actix::Handler<FsEvent> for A {
|
|
33
|
+
type Result = ();
|
|
34
|
+
|
|
35
|
+
fn handle(&mut self, msg: FsEvent, _ctx: &mut Self::Context) -> Self::Result {
|
|
36
|
+
self.events.push(msg);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn create_file(base: &Path, name: &str) -> PathBuf {
|
|
41
|
+
let file_path = base.join(name);
|
|
42
|
+
let mut file = File::create(&file_path).expect("create file");
|
|
43
|
+
file.write_all(name.as_bytes())
|
|
44
|
+
.expect(format!("write {name}").as_str());
|
|
45
|
+
file_path
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
struct TestCase {
|
|
49
|
+
addr: Addr<FsWatcher>,
|
|
50
|
+
recip_addr: Addr<A>,
|
|
51
|
+
dir: PathBuf,
|
|
52
|
+
#[allow(dead_code)]
|
|
53
|
+
tmp_dir: TempDir,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl TestCase {
|
|
57
|
+
pub fn new(debounce: Debounce, filter: Option<Filter>) -> Self {
|
|
58
|
+
let tmp_dir = tempfile::tempdir().unwrap();
|
|
59
|
+
let mut fs = FsWatcher::new(tmp_dir.path(), 0);
|
|
60
|
+
fs.with_debounce(debounce);
|
|
61
|
+
if let Some(filter) = filter {
|
|
62
|
+
fs.with_filter(filter);
|
|
63
|
+
}
|
|
64
|
+
let addr = fs.start();
|
|
65
|
+
let a = A { events: vec![] };
|
|
66
|
+
let recip_addr = a.start();
|
|
67
|
+
Self {
|
|
68
|
+
recip_addr: recip_addr.clone(),
|
|
69
|
+
addr: addr.clone(),
|
|
70
|
+
dir: tmp_dir.path().to_path_buf(),
|
|
71
|
+
tmp_dir,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async fn watch(&self) {
|
|
76
|
+
let r = RequestWatchPath {
|
|
77
|
+
recipients: vec![self.recip_addr.clone().recipient()],
|
|
78
|
+
path: self.dir.to_path_buf(),
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
let _ = self.addr.send(r).await;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async fn write_file(&self, p: &str) {
|
|
85
|
+
create_file(self.dir.as_path(), p);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async fn get_events_after(&self, d: Duration) -> Vec<FsEvent> {
|
|
89
|
+
sleep(d).await;
|
|
90
|
+
let events = self.recip_addr.send(GetEvents).await.unwrap();
|
|
91
|
+
events
|
|
92
|
+
}
|
|
93
|
+
async fn change_events_after(&self, d: Duration) -> (Vec<FsEvent>, Vec<FsEvent>, usize) {
|
|
94
|
+
sleep(d).await;
|
|
95
|
+
let events = self.recip_addr.send(GetEvents).await.unwrap();
|
|
96
|
+
let len = events.len();
|
|
97
|
+
let (change_events, other_events) = events
|
|
98
|
+
.into_iter()
|
|
99
|
+
.partition(|e| matches!(e.kind, FsEventKind::Change(..)));
|
|
100
|
+
(change_events, other_events, len)
|
|
101
|
+
}
|
|
102
|
+
async fn buffered_change_after(&self, d: Duration) -> (Vec<FsEvent>, Vec<FsEvent>, usize) {
|
|
103
|
+
sleep(d).await;
|
|
104
|
+
let events = self.recip_addr.send(GetEvents).await.unwrap();
|
|
105
|
+
let len = events.len();
|
|
106
|
+
let (change_events, other_events) = events
|
|
107
|
+
.into_iter()
|
|
108
|
+
.partition(|e| matches!(e.kind, FsEventKind::ChangeBuffered(..)));
|
|
109
|
+
(change_events, other_events, len)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
fn file_names(events: Vec<FsEvent>) -> Vec<String> {
|
|
113
|
+
events
|
|
114
|
+
.into_iter()
|
|
115
|
+
.flat_map(|evt| match evt.kind {
|
|
116
|
+
FsEventKind::Change(_) => vec![],
|
|
117
|
+
FsEventKind::ChangeBuffered(buf) => buf
|
|
118
|
+
.events
|
|
119
|
+
.iter()
|
|
120
|
+
.map(|p| p.absolute.to_path_buf())
|
|
121
|
+
.collect(),
|
|
122
|
+
FsEventKind::PathAdded(_) => vec![],
|
|
123
|
+
FsEventKind::PathRemoved(_) => vec![],
|
|
124
|
+
})
|
|
125
|
+
.map(|pb| pb.file_name().unwrap().to_string_lossy().to_string())
|
|
126
|
+
.collect()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async fn test_single_file_impl() -> Result<(), Box<dyn std::error::Error>> {
|
|
131
|
+
let tc = TestCase::new(Debounce::trailing_ms(10), None);
|
|
132
|
+
tc.watch().await;
|
|
133
|
+
tc.write_file("test_file.txt").await;
|
|
134
|
+
let events = tc.get_events_after(Duration::from_millis(300)).await;
|
|
135
|
+
assert_eq!(events.len(), 2);
|
|
136
|
+
assert_eq!(
|
|
137
|
+
matches!(events.get(0).unwrap().kind, FsEventKind::PathAdded(..)),
|
|
138
|
+
true
|
|
139
|
+
);
|
|
140
|
+
assert_eq!(
|
|
141
|
+
matches!(events.get(1).unwrap().kind, FsEventKind::Change(..)),
|
|
142
|
+
true
|
|
143
|
+
);
|
|
144
|
+
Ok(())
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#[actix_rt::test]
|
|
148
|
+
async fn test_single_file() -> Result<(), Box<dyn std::error::Error>> {
|
|
149
|
+
test_single_file_impl().await
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async fn test_trailing_drops_impl() -> Result<(), Box<dyn std::error::Error>> {
|
|
153
|
+
let tc = TestCase::new(Debounce::trailing_ms(10), None);
|
|
154
|
+
tc.watch().await;
|
|
155
|
+
tc.write_file("test_file.txt").await;
|
|
156
|
+
tc.write_file("test_file.css").await;
|
|
157
|
+
let (change, other, _total_count) = tc.change_events_after(Duration::from_millis(500)).await;
|
|
158
|
+
assert_eq!(change.len(), 1, "Should be a single change event");
|
|
159
|
+
assert_eq!(other.len(), 1, "Should be 2 in total");
|
|
160
|
+
Ok(())
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[actix_rt::test]
|
|
164
|
+
async fn test_trailing_drops() -> Result<(), Box<dyn std::error::Error>> {
|
|
165
|
+
test_trailing_drops_impl().await
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async fn test_buffer_impl() -> Result<(), Box<dyn std::error::Error>> {
|
|
169
|
+
let tc = TestCase::new(Debounce::buffered_ms(10), None);
|
|
170
|
+
tc.watch().await;
|
|
171
|
+
tc.write_file("test_file.txt").await;
|
|
172
|
+
tc.write_file("test_file.css").await;
|
|
173
|
+
let (change, other, total_count) = tc.buffered_change_after(Duration::from_millis(500)).await;
|
|
174
|
+
assert_eq!(change.len(), 1, "Should be 1 change event (buffered)");
|
|
175
|
+
assert_eq!(other.len(), 1, "Should be 1 other event (path added)");
|
|
176
|
+
assert_eq!(total_count, 2, "Should be 2 in total");
|
|
177
|
+
|
|
178
|
+
let names = TestCase::file_names(change);
|
|
179
|
+
assert!(names.contains(&"test_file.txt".to_string()));
|
|
180
|
+
assert!(names.contains(&"test_file.css".to_string()));
|
|
181
|
+
Ok(())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
#[actix_rt::test]
|
|
185
|
+
async fn test_buffer() -> Result<(), Box<dyn std::error::Error>> {
|
|
186
|
+
test_buffer_impl().await
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async fn test_buffer_filter_impl() -> Result<(), Box<dyn std::error::Error>> {
|
|
190
|
+
let tc = TestCase::new(
|
|
191
|
+
Debounce::buffered_ms(10),
|
|
192
|
+
Some(Filter::Extension {
|
|
193
|
+
ext: "css".to_string(),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
tc.watch().await;
|
|
197
|
+
tc.write_file("test_file.txt").await;
|
|
198
|
+
tc.write_file("test_file.css").await;
|
|
199
|
+
let (change, other, total_count) = tc.buffered_change_after(Duration::from_millis(500)).await;
|
|
200
|
+
assert_eq!(change.len(), 1, "Should be 1 change event (buffered)");
|
|
201
|
+
assert_eq!(other.len(), 1, "Should be 1 other event (path added)");
|
|
202
|
+
assert_eq!(total_count, 2, "Should be 2 in total");
|
|
203
|
+
|
|
204
|
+
let names = TestCase::file_names(change);
|
|
205
|
+
assert_eq!(names.len(), 1);
|
|
206
|
+
assert!(names.contains(&"test_file.css".to_string()));
|
|
207
|
+
Ok(())
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[actix_rt::test]
|
|
211
|
+
async fn test_buffer_filter() -> Result<(), Box<dyn std::error::Error>> {
|
|
212
|
+
test_buffer_filter_impl().await
|
|
213
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use crate::actor::FsWatcher;
|
|
2
|
+
use crate::{FsEvent, FsEventKind, FsWatchError, PathAddedEvent};
|
|
3
|
+
use actix::{ActorContext, Handler, Recipient};
|
|
4
|
+
use notify::{RecursiveMode, Watcher};
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
|
|
7
|
+
#[derive(actix::Message)]
|
|
8
|
+
#[rtype(result = "Result<(), FsWatchError>")]
|
|
9
|
+
pub struct RequestWatchPath {
|
|
10
|
+
pub recipients: Vec<Recipient<FsEvent>>,
|
|
11
|
+
pub path: PathBuf,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl Handler<RequestWatchPath> for FsWatcher {
|
|
15
|
+
type Result = Result<(), FsWatchError>;
|
|
16
|
+
|
|
17
|
+
// todo: ensure this isn't sent for every input change
|
|
18
|
+
fn handle(&mut self, msg: RequestWatchPath, _ctx: &mut Self::Context) -> Self::Result {
|
|
19
|
+
tracing::trace!(path = ?msg.path, "-> WatchPath");
|
|
20
|
+
if let Some(watcher) = self.watcher.as_mut() {
|
|
21
|
+
match watcher.watch(&msg.path, RecursiveMode::Recursive) {
|
|
22
|
+
Ok(_) => {
|
|
23
|
+
let new_recipients = msg
|
|
24
|
+
.recipients
|
|
25
|
+
.into_iter()
|
|
26
|
+
.filter(|r| !self.receivers.contains(r))
|
|
27
|
+
.collect::<Vec<_>>();
|
|
28
|
+
self.receivers.extend(new_recipients);
|
|
29
|
+
|
|
30
|
+
tracing::debug!(path = ?msg.path, "👀 watching! {} receivers", self.receivers.len());
|
|
31
|
+
tracing::debug!(?self.cwd);
|
|
32
|
+
tracing::debug!(?self.receivers);
|
|
33
|
+
|
|
34
|
+
let matched = msg.path == self.cwd;
|
|
35
|
+
|
|
36
|
+
let relative = if matched {
|
|
37
|
+
msg.path.clone()
|
|
38
|
+
} else {
|
|
39
|
+
match msg.path.strip_prefix(&self.cwd) {
|
|
40
|
+
Ok(stripped) => stripped.to_path_buf(),
|
|
41
|
+
Err(e) => {
|
|
42
|
+
tracing::debug!(?e, "could not extract the CWD from a path");
|
|
43
|
+
msg.path.clone()
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
for recip in &self.receivers {
|
|
48
|
+
let evt = FsEventKind::PathAdded(PathAddedEvent {
|
|
49
|
+
path: relative.clone(),
|
|
50
|
+
debounce: self.debounce,
|
|
51
|
+
});
|
|
52
|
+
recip.do_send(FsEvent {
|
|
53
|
+
kind: evt,
|
|
54
|
+
ctx: self.ctx.clone(),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
Err(err) => {
|
|
59
|
+
tracing::error!("cannot watch: {}", err);
|
|
60
|
+
_ctx.stop();
|
|
61
|
+
return Err(FsWatchError::Watcher(err));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
67
|
+
}
|