mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
refactor(fs): reduce overhead of watch (#2613)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
fs: minor
|
||||
fs-js: minor
|
||||
---
|
||||
|
||||
Reduce the overhead of `watch` and `unwatch`
|
||||
Generated
-1
@@ -6599,7 +6599,6 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"toml",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"fs:allow-mkdir",
|
||||
"fs:allow-remove",
|
||||
"fs:allow-write-text-file",
|
||||
"fs:read-meta",
|
||||
"fs:scope-download-recursive",
|
||||
"fs:scope-resource-recursive",
|
||||
{
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
import * as fs from "@tauri-apps/plugin-fs";
|
||||
import { convertFileSrc } from "@tauri-apps/api/core";
|
||||
import { arrayBufferToBase64 } from "../lib/utils";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let onMessage;
|
||||
export let insecureRenderHtml;
|
||||
|
||||
let path = "";
|
||||
let img;
|
||||
/** @type {fs.FileHandle} */
|
||||
let file;
|
||||
let renameTo;
|
||||
let watchPath = "";
|
||||
let watchDebounceDelay = 0;
|
||||
let watchDebounceDelay = "0";
|
||||
let watchRecursive = false;
|
||||
let unwatchFn;
|
||||
let unwatchPath = "";
|
||||
@@ -118,7 +120,7 @@
|
||||
.getElementById("file-save")
|
||||
.addEventListener("click", function () {
|
||||
fs.writeTextFile(path, fileInput.value, {
|
||||
dir: getDir(),
|
||||
baseDir: getDir(),
|
||||
}).catch(onMessage);
|
||||
});
|
||||
});
|
||||
@@ -170,6 +172,15 @@
|
||||
unwatchFn = undefined;
|
||||
unwatchPath = undefined;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
if (file) {
|
||||
file.close();
|
||||
}
|
||||
if (unwatchFn) {
|
||||
unwatchFn();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
|
||||
@@ -35,7 +35,6 @@ tauri = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true }
|
||||
anyhow = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
glob = { workspace = true }
|
||||
# TODO: Remove `serialization-compat-6` in v3
|
||||
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:
|
||||
|
||||
```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
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -101,6 +101,7 @@ const COMMANDS: &[(&str, &[&str])] = &[
|
||||
("fstat", &[]),
|
||||
("exists", &[]),
|
||||
("watch", &[]),
|
||||
// TODO: Remove this in v3
|
||||
("unwatch", &[]),
|
||||
("size", &[]),
|
||||
];
|
||||
|
||||
@@ -1245,15 +1245,44 @@ type WatchEventKindRemove =
|
||||
| { kind: 'folder' }
|
||||
| { kind: 'other' }
|
||||
|
||||
// TODO: Remove this in v3, return `Watcher` instead
|
||||
/**
|
||||
* @since 2.0.0
|
||||
*/
|
||||
type UnwatchFn = () => void
|
||||
|
||||
async function unwatch(rid: number): Promise<void> {
|
||||
await invoke('plugin:fs|unwatch', { rid })
|
||||
class Watcher extends Resource {}
|
||||
|
||||
async function watchInternal(
|
||||
paths: string | string[] | URL | URL[],
|
||||
cb: (event: WatchEvent) => void,
|
||||
options: DebouncedWatchOptions
|
||||
): Promise<UnwatchFn> {
|
||||
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,
|
||||
onEvent
|
||||
})
|
||||
|
||||
const watcher = new Watcher(rid)
|
||||
|
||||
return () => {
|
||||
void watcher.close()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Return `Watcher` instead in v3
|
||||
/**
|
||||
* Watch changes (after a delay) on files or directories.
|
||||
*
|
||||
@@ -1264,34 +1293,13 @@ async function watch(
|
||||
cb: (event: WatchEvent) => void,
|
||||
options?: DebouncedWatchOptions
|
||||
): Promise<UnwatchFn> {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
return await watchInternal(paths, cb, {
|
||||
delayMs: 2000,
|
||||
...options
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Return `Watcher` instead in v3
|
||||
/**
|
||||
* Watch changes on files or directories.
|
||||
*
|
||||
@@ -1302,32 +1310,10 @@ async function watchImmediate(
|
||||
cb: (event: WatchEvent) => void,
|
||||
options?: WatchOptions
|
||||
): Promise<UnwatchFn> {
|
||||
const opts = {
|
||||
recursive: false,
|
||||
return await watchInternal(paths, cb, {
|
||||
...options,
|
||||
delayMs: null
|
||||
}
|
||||
|
||||
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
|
||||
delayMs: undefined
|
||||
})
|
||||
|
||||
return () => {
|
||||
void unwatch(rid)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -150,11 +150,6 @@ pub fn open<R: Runtime>(
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CopyFileOptions {
|
||||
|
||||
@@ -397,7 +397,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
commands::create,
|
||||
commands::open,
|
||||
commands::copy_file,
|
||||
commands::close,
|
||||
commands::mkdir,
|
||||
commands::read_dir,
|
||||
commands::read,
|
||||
@@ -420,8 +419,6 @@ pub fn init<R: Runtime>() -> TauriPlugin<R, Option<config::Config>> {
|
||||
commands::size,
|
||||
#[cfg(feature = "watch")]
|
||||
watcher::watch,
|
||||
#[cfg(feature = "watch")]
|
||||
watcher::unwatch
|
||||
])
|
||||
.setup(|app, api| {
|
||||
let scope = Scope {
|
||||
|
||||
+43
-98
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use notify_debouncer_full::{new_debouncer, DebounceEventResult, Debouncer, RecommendedCache};
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use notify_debouncer_full::{new_debouncer, DebouncedEvent, Debouncer, RecommendedCache};
|
||||
use serde::Deserialize;
|
||||
use tauri::{
|
||||
ipc::{Channel, CommandScope, GlobalScope},
|
||||
@@ -11,15 +11,7 @@ use tauri::{
|
||||
Manager, Resource, ResourceId, Runtime, Webview,
|
||||
};
|
||||
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{channel, Receiver},
|
||||
Mutex,
|
||||
},
|
||||
thread::spawn,
|
||||
time::Duration,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{
|
||||
commands::{resolve_path, CommandResult},
|
||||
@@ -27,79 +19,44 @@ use crate::{
|
||||
SafeFilePath,
|
||||
};
|
||||
|
||||
struct InnerWatcher {
|
||||
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 {}
|
||||
|
||||
#[allow(unused)]
|
||||
enum WatcherKind {
|
||||
Debouncer(Debouncer<RecommendedWatcher, RecommendedCache>),
|
||||
Watcher(RecommendedWatcher),
|
||||
}
|
||||
|
||||
fn watch_raw(on_event: Channel<Event>, rx: Receiver<notify::Result<Event>>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
impl Resource for WatcherKind {}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WatchOptions {
|
||||
base_dir: Option<BaseDirectory>,
|
||||
#[serde(default)]
|
||||
recursive: bool,
|
||||
delay_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn watch<R: Runtime>(
|
||||
pub fn watch<R: Runtime>(
|
||||
webview: Webview<R>,
|
||||
paths: Vec<SafeFilePath>,
|
||||
options: WatchOptions,
|
||||
on_event: Channel<Event>,
|
||||
on_event: Channel<notify::Event>,
|
||||
global_scope: GlobalScope<Entry>,
|
||||
command_scope: CommandScope<Entry>,
|
||||
) -> CommandResult<ResourceId> {
|
||||
let mut resolved_paths = Vec::with_capacity(paths.capacity());
|
||||
for path in paths {
|
||||
resolved_paths.push(resolve_path(
|
||||
&webview,
|
||||
&global_scope,
|
||||
&command_scope,
|
||||
path,
|
||||
options.base_dir,
|
||||
)?);
|
||||
}
|
||||
let resolved_paths = paths
|
||||
.into_iter()
|
||||
.map(|path| {
|
||||
resolve_path(
|
||||
&webview,
|
||||
&global_scope,
|
||||
&command_scope,
|
||||
path,
|
||||
options.base_dir,
|
||||
)
|
||||
})
|
||||
.collect::<CommandResult<Vec<_>>>()?;
|
||||
|
||||
let recursive_mode = if options.recursive {
|
||||
RecursiveMode::Recursive
|
||||
@@ -107,52 +64,40 @@ pub async fn watch<R: Runtime>(
|
||||
RecursiveMode::NonRecursive
|
||||
};
|
||||
|
||||
let kind = if let Some(delay) = options.delay_ms {
|
||||
let (tx, rx) = channel();
|
||||
let mut debouncer = new_debouncer(Duration::from_millis(delay), None, tx)?;
|
||||
let watcher_kind = if let Some(delay) = options.delay_ms {
|
||||
let mut debouncer = new_debouncer(
|
||||
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 {
|
||||
debouncer.watch(path, recursive_mode)?;
|
||||
}
|
||||
watch_debounced(on_event, rx);
|
||||
WatcherKind::Debouncer(debouncer)
|
||||
} else {
|
||||
let (tx, rx) = channel();
|
||||
let mut watcher = RecommendedWatcher::new(tx, Config::default())?;
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
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 {
|
||||
watcher.watch(path, recursive_mode)?;
|
||||
}
|
||||
watch_raw(on_event, rx);
|
||||
WatcherKind::Watcher(watcher)
|
||||
};
|
||||
|
||||
let rid = webview
|
||||
.resources_table()
|
||||
.add(WatcherResource::new(kind, resolved_paths));
|
||||
let rid = webview.resources_table().add(watcher_kind);
|
||||
|
||||
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(())
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user