refactor(fs): reduce overhead of `watch` (#2613)

pull/2549/head
Tony 2 months ago committed by GitHub
parent 5e78988f72
commit dac4d53724
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,6 @@
---
fs: minor
fs-js: minor
---
Reduce the overhead of `watch` and `unwatch`

1
Cargo.lock generated

@ -6599,7 +6599,6 @@ dependencies = [
"thiserror 2.0.12", "thiserror 2.0.12",
"toml", "toml",
"url", "url",
"uuid",
] ]
[[package]] [[package]]

@ -69,6 +69,7 @@
"fs:allow-mkdir", "fs:allow-mkdir",
"fs:allow-remove", "fs:allow-remove",
"fs:allow-write-text-file", "fs:allow-write-text-file",
"fs:read-meta",
"fs:scope-download-recursive", "fs:scope-download-recursive",
"fs:scope-resource-recursive", "fs:scope-resource-recursive",
{ {

@ -2,16 +2,18 @@
import * as fs from "@tauri-apps/plugin-fs"; import * as fs from "@tauri-apps/plugin-fs";
import { convertFileSrc } from "@tauri-apps/api/core"; import { convertFileSrc } from "@tauri-apps/api/core";
import { arrayBufferToBase64 } from "../lib/utils"; import { arrayBufferToBase64 } from "../lib/utils";
import { onDestroy } from "svelte";
export let onMessage; export let onMessage;
export let insecureRenderHtml; export let insecureRenderHtml;
let path = ""; let path = "";
let img; let img;
/** @type {fs.FileHandle} */
let file; let file;
let renameTo; let renameTo;
let watchPath = ""; let watchPath = "";
let watchDebounceDelay = 0; let watchDebounceDelay = "0";
let watchRecursive = false; let watchRecursive = false;
let unwatchFn; let unwatchFn;
let unwatchPath = ""; let unwatchPath = "";
@ -118,7 +120,7 @@
.getElementById("file-save") .getElementById("file-save")
.addEventListener("click", function () { .addEventListener("click", function () {
fs.writeTextFile(path, fileInput.value, { fs.writeTextFile(path, fileInput.value, {
dir: getDir(), baseDir: getDir(),
}).catch(onMessage); }).catch(onMessage);
}); });
}); });
@ -170,6 +172,15 @@
unwatchFn = undefined; unwatchFn = undefined;
unwatchPath = undefined; unwatchPath = undefined;
} }
onDestroy(() => {
if (file) {
file.close();
}
if (unwatchFn) {
unwatchFn();
}
})
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">

@ -35,7 +35,6 @@ tauri = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
url = { workspace = true } url = { workspace = true }
anyhow = "1" anyhow = "1"
uuid = { version = "1", features = ["v4"] }
glob = { workspace = true } glob = { workspace = true }
# TODO: Remove `serialization-compat-6` in v3 # TODO: Remove `serialization-compat-6` in v3
notify = { version = "8", optional = true, features = [ notify = { version = "8", optional = true, features = [

@ -68,9 +68,9 @@ fn main() {
Afterwards all the plugin's APIs are available through the JavaScript guest bindings: Afterwards all the plugin's APIs are available through the JavaScript guest bindings:
```javascript ```javascript
import { metadata } from '@tauri-apps/plugin-fs' import { stat } from '@tauri-apps/plugin-fs'
await metadata('/path/to/file') await stat('/path/to/file')
``` ```
## Contributing ## Contributing

File diff suppressed because one or more lines are too long

@ -101,6 +101,7 @@ const COMMANDS: &[(&str, &[&str])] = &[
("fstat", &[]), ("fstat", &[]),
("exists", &[]), ("exists", &[]),
("watch", &[]), ("watch", &[]),
// TODO: Remove this in v3
("unwatch", &[]), ("unwatch", &[]),
("size", &[]), ("size", &[]),
]; ];

@ -1245,31 +1245,19 @@ type WatchEventKindRemove =
| { kind: 'folder' } | { kind: 'folder' }
| { kind: 'other' } | { kind: 'other' }
// TODO: Remove this in v3, return `Watcher` instead
/** /**
* @since 2.0.0 * @since 2.0.0
*/ */
type UnwatchFn = () => void type UnwatchFn = () => void
async function unwatch(rid: number): Promise<void> { class Watcher extends Resource {}
await invoke('plugin:fs|unwatch', { rid })
}
/** async function watchInternal(
* Watch changes (after a delay) on files or directories.
*
* @since 2.0.0
*/
async function watch(
paths: string | string[] | URL | URL[], paths: string | string[] | URL | URL[],
cb: (event: WatchEvent) => void, cb: (event: WatchEvent) => void,
options?: DebouncedWatchOptions options: DebouncedWatchOptions
): Promise<UnwatchFn> { ): Promise<UnwatchFn> {
const opts = {
recursive: false,
delayMs: 2000,
...options
}
const watchPaths = Array.isArray(paths) ? paths : [paths] const watchPaths = Array.isArray(paths) ? paths : [paths]
for (const path of watchPaths) { for (const path of watchPaths) {
@ -1283,15 +1271,35 @@ async function watch(
const rid: number = await invoke('plugin:fs|watch', { const rid: number = await invoke('plugin:fs|watch', {
paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)), paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)),
options: opts, options,
onEvent onEvent
}) })
const watcher = new Watcher(rid)
return () => { return () => {
void unwatch(rid) void watcher.close()
} }
} }
// TODO: Return `Watcher` instead in v3
/**
* Watch changes (after a delay) on files or directories.
*
* @since 2.0.0
*/
async function watch(
paths: string | string[] | URL | URL[],
cb: (event: WatchEvent) => void,
options?: DebouncedWatchOptions
): Promise<UnwatchFn> {
return await watchInternal(paths, cb, {
delayMs: 2000,
...options
})
}
// TODO: Return `Watcher` instead in v3
/** /**
* Watch changes on files or directories. * Watch changes on files or directories.
* *
@ -1302,32 +1310,10 @@ async function watchImmediate(
cb: (event: WatchEvent) => void, cb: (event: WatchEvent) => void,
options?: WatchOptions options?: WatchOptions
): Promise<UnwatchFn> { ): Promise<UnwatchFn> {
const opts = { return await watchInternal(paths, cb, {
recursive: false,
...options, ...options,
delayMs: null delayMs: undefined
}
const watchPaths = Array.isArray(paths) ? paths : [paths]
for (const path of watchPaths) {
if (path instanceof URL && path.protocol !== 'file:') {
throw new TypeError('Must be a file URL.')
}
}
const onEvent = new Channel<WatchEvent>()
onEvent.onmessage = cb
const rid: number = await invoke('plugin:fs|watch', {
paths: watchPaths.map((p) => (p instanceof URL ? p.toString() : p)),
options: opts,
onEvent
}) })
return () => {
void unwatch(rid)
}
} }
/** /**

@ -150,11 +150,6 @@ pub fn open<R: Runtime>(
Ok(rid) Ok(rid)
} }
#[tauri::command]
pub fn close<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> CommandResult<()> {
webview.resources_table().close(rid).map_err(Into::into)
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct CopyFileOptions { pub struct CopyFileOptions {

@ -397,7 +397,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
commands::create, commands::create,
commands::open, commands::open,
commands::copy_file, commands::copy_file,
commands::close,
commands::mkdir, commands::mkdir,
commands::read_dir, commands::read_dir,
commands::read, commands::read,
@ -420,8 +419,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
commands::size, commands::size,
#[cfg(feature = "watch")] #[cfg(feature = "watch")]
watcher::watch, watcher::watch,
#[cfg(feature = "watch")]
watcher::unwatch
]) ])
.setup(|app, api| { .setup(|app, api| {
let scope = Scope { let scope = Scope {

@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache}; use notify_debouncer_full::{new_debouncer, DebouncedEvent, Debouncer, RecommendedCache};
use serde::Deserialize; use serde::Deserialize;
use tauri::{ use tauri::{
ipc::{Channel, CommandScope, GlobalScope}, ipc::{Channel, CommandScope, GlobalScope},
@ -11,15 +11,7 @@ use tauri::{
Manager, Resource, ResourceId, Runtime, Webview, Manager, Resource, ResourceId, Runtime, Webview,
}; };
use std::{ use std::time::Duration;
path::PathBuf,
sync::{
mpsc::{channel, Receiver},
Mutex,
},
thread::spawn,
time::Duration,
};
use crate::{ use crate::{
commands::{resolve_path, CommandResult}, commands::{resolve_path, CommandResult},
@ -27,79 +19,44 @@ use crate::{
SafeFilePath, SafeFilePath,
}; };
struct InnerWatcher { #[allow(unused)]
pub kind: WatcherKind,
paths: Vec<PathBuf>,
}
pub struct WatcherResource(Mutex<InnerWatcher>);
impl WatcherResource {
fn new(kind: WatcherKind, paths: Vec<PathBuf>) -> Self {
Self(Mutex::new(InnerWatcher { kind, paths }))
}
fn with_lock<R, F: FnMut(&mut InnerWatcher) -> R>(&self, mut f: F) -> R {
let mut watcher = self.0.lock().unwrap();
f(&mut watcher)
}
}
impl Resource for WatcherResource {}
enum WatcherKind { enum WatcherKind {
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>), Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
Watcher(RecommendedWatcher), Watcher(RecommendedWatcher),
} }
fn watch_raw(on_event: Channel<Event>, rx: Receiver<notify::Result<Event>>) { impl Resource for WatcherKind {}
spawn(move || {
while let Ok(event) = rx.recv() {
if let Ok(event) = event {
// TODO: Should errors be emitted too?
let _ = on_event.send(event);
}
}
});
}
fn watch_debounced(on_event: Channel<Event>, rx: Receiver<DebounceEventResult>) {
spawn(move || {
while let Ok(Ok(events)) = rx.recv() {
for event in events {
// TODO: Should errors be emitted too?
let _ = on_event.send(event.event);
}
}
});
}
#[derive(Clone, Deserialize)] #[derive(Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct WatchOptions { pub struct WatchOptions {
base_dir: Option<BaseDirectory>, base_dir: Option<BaseDirectory>,
#[serde(default)]
recursive: bool, recursive: bool,
delay_ms: Option<u64>, delay_ms: Option<u64>,
} }
#[tauri::command] #[tauri::command]
pub async fn watch<R: Runtime>( pub fn watch<R: Runtime>(
webview: Webview<R>, webview: Webview<R>,
paths: Vec<SafeFilePath>, paths: Vec<SafeFilePath>,
options: WatchOptions, options: WatchOptions,
on_event: Channel<Event>, on_event: Channel<notify::Event>,
global_scope: GlobalScope<Entry>, global_scope: GlobalScope<Entry>,
command_scope: CommandScope<Entry>, command_scope: CommandScope<Entry>,
) -> CommandResult<ResourceId> { ) -> CommandResult<ResourceId> {
let mut resolved_paths = Vec::with_capacity(paths.capacity()); let resolved_paths = paths
for path in paths { .into_iter()
resolved_paths.push(resolve_path( .map(|path| {
&webview, resolve_path(
&global_scope, &webview,
&command_scope, &global_scope,
path, &command_scope,
options.base_dir, path,
)?); options.base_dir,
} )
})
.collect::<CommandResult<Vec<_>>>()?;
let recursive_mode = if options.recursive { let recursive_mode = if options.recursive {
RecursiveMode::Recursive RecursiveMode::Recursive
@ -107,52 +64,40 @@ pub async fn watch<R: Runtime>(
RecursiveMode::NonRecursive RecursiveMode::NonRecursive
}; };
let kind = if let Some(delay) = options.delay_ms { let watcher_kind = if let Some(delay) = options.delay_ms {
let (tx, rx) = channel(); let mut debouncer = new_debouncer(
let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?; Duration::from_millis(delay),
None,
move |events: Result<Vec<DebouncedEvent>, Vec<notify::Error>>| {
if let Ok(events) = events {
for event in events {
// TODO: Should errors be emitted too?
let _ = on_event.send(event.event);
}
}
},
)?;
for path in &resolved_paths { for path in &resolved_paths {
debouncer.watch(path, recursive_mode)?; debouncer.watch(path, recursive_mode)?;
} }
watch_debounced(on_event, rx);
WatcherKind::Debouncer(debouncer) WatcherKind::Debouncer(debouncer)
} else { } else {
let (tx, rx) = channel(); let mut watcher = RecommendedWatcher::new(
let mut watcher = RecommendedWatcher::new(tx, Config::default())?; move |event| {
if let Ok(event) = event {
// TODO: Should errors be emitted too?
let _ = on_event.send(event);
}
},
Config::default(),
)?;
for path in &resolved_paths { for path in &resolved_paths {
watcher.watch(path, recursive_mode)?; watcher.watch(path, recursive_mode)?;
} }
watch_raw(on_event, rx);
WatcherKind::Watcher(watcher) WatcherKind::Watcher(watcher)
}; };
let rid = webview let rid = webview.resources_table().add(watcher_kind);
.resources_table()
.add(WatcherResource::new(kind, resolved_paths));
Ok(rid) Ok(rid)
} }
#[tauri::command]
pub async fn unwatch<R: Runtime>(webview: Webview<R>, rid: ResourceId) -> CommandResult<()> {
let watcher = webview.resources_table().take::<WatcherResource>(rid)?;
WatcherResource::with_lock(&watcher, |watcher| {
match &mut watcher.kind {
WatcherKind::Debouncer(ref mut debouncer) => {
for path in &watcher.paths {
debouncer.unwatch(path).map_err(|e| {
format!("failed to unwatch path: {} with error: {e}", path.display())
})?;
}
}
WatcherKind::Watcher(ref mut w) => {
for path in &watcher.paths {
w.unwatch(path).map_err(|e| {
format!("failed to unwatch path: {} with error: {e}", path.display())
})?;
}
}
}
Ok(())
})
}

Loading…
Cancel
Save