From fdfb35e5317f85d378b54429f6669b13339b1951 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Tue, 22 Sep 2026 06:06:50 -0300 Subject: [PATCH] refactor(fs)!: return Watcher from watch and watchImmediate (#3610) * refactor(fs)!: return Watcher from watch and watchImmediate Both resolve to a `Watcher` resource instead of an `UnwatchFn` callback; call `await watcher.close()` to stop watching. `UnwatchFn` is removed and `Watcher` is now exported. * fix change file * fmt --- .changes/fs-watcher.md | 6 ++++ examples/api/src/views/FileSystem.svelte | 20 ++++++------ plugins/fs/api-iife.js | 2 +- plugins/fs/guest-js/index.ts | 41 +++++++++++++++--------- 4 files changed, 42 insertions(+), 27 deletions(-) create mode 100644 .changes/fs-watcher.md diff --git a/.changes/fs-watcher.md b/.changes/fs-watcher.md new file mode 100644 index 000000000..af446ff03 --- /dev/null +++ b/.changes/fs-watcher.md @@ -0,0 +1,6 @@ +--- +"fs": major +"fs-js": major +--- + +**Breaking:** `watch` and `watchImmediate` now resolve to a `Watcher` resource instead of an `UnwatchFn` callback. Call `await watcher.close()` to stop watching. The `UnwatchFn` type has been removed and `Watcher` is now exported. diff --git a/examples/api/src/views/FileSystem.svelte b/examples/api/src/views/FileSystem.svelte index bc35547ec..1a3e72d4b 100644 --- a/examples/api/src/views/FileSystem.svelte +++ b/examples/api/src/views/FileSystem.svelte @@ -17,7 +17,7 @@ let watchRecursive = $state(false) /** @type {fs.BaseDirectory | undefined} */ let baseDir = $state() - let unwatchFn + let watcher let unwatchPath = '' let isMobile = $state(false) @@ -161,15 +161,15 @@ } if (options.delayMs === 0) { fs.watchImmediate(watchPath, onMessage, options) - .then((fn) => { - unwatchFn = fn + .then((w) => { + watcher = w unwatchPath = watchPath }) .catch(onMessage) } else { fs.watch(watchPath, onMessage, options) - .then((fn) => { - unwatchFn = fn + .then((w) => { + watcher = w unwatchPath = watchPath }) .catch(onMessage) @@ -178,11 +178,11 @@ } function unwatch() { - if (unwatchFn) { + if (watcher) { onMessage(`Stopped watching ${unwatchPath} for changes`) - unwatchFn() + watcher.close() } - unwatchFn = undefined + watcher = undefined unwatchPath = undefined } @@ -190,8 +190,8 @@ if (file) { file.close() } - if (unwatchFn) { - unwatchFn() + if (watcher) { + watcher.close() } }) diff --git a/plugins/fs/api-iife.js b/plugins/fs/api-iife.js index bb015dfb7..cfe6fa109 100644 --- a/plugins/fs/api-iife.js +++ b/plugins/fs/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(t){"use strict";function e(t,e,n,i){if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var i,o,r,a,s;"function"==typeof SuppressedError&&SuppressedError;const c="__TAURI_TO_IPC_KEY__";class f{constructor(t){i.set(this,void 0),o.set(this,0),r.set(this,[]),a.set(this,void 0),n(this,i,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const s=t.index;if("end"in t)return void(s==e(this,o,"f")?this.cleanupCallback():n(this,a,s));const c=t.message;if(s==e(this,o,"f")){for(e(this,i,"f").call(this,c),n(this,o,e(this,o,"f")+1);e(this,o,"f")in e(this,r,"f");){const t=e(this,r,"f")[e(this,o,"f")];e(this,i,"f").call(this,t),delete e(this,r,"f")[e(this,o,"f")],n(this,o,e(this,o,"f")+1)}e(this,o,"f")===e(this,a,"f")&&this.cleanupCallback()}else e(this,r,"f")[s]=c})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){n(this,i,t)}get onmessage(){return e(this,i,"f")}[(i=new WeakMap,o=new WeakMap,r=new WeakMap,a=new WeakMap,c)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[c]()}}async function l(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}class u{get rid(){return e(this,s,"f")}constructor(t){s.set(this,void 0),n(this,s,t)}async close(){return l("plugin:resources|close",{rid:this.rid})}async[(s=new WeakMap,Symbol.asyncDispose)](){await this.close()}}var p,d;function w(t){return{isFile:t.isFile,isDirectory:t.isDirectory,isSymlink:t.isSymlink,size:t.size,mtime:null!==t.mtime?new Date(t.mtime):null,atime:null!==t.atime?new Date(t.atime):null,birthtime:null!==t.birthtime?new Date(t.birthtime):null,readonly:t.readonly,fileAttributes:t.fileAttributes,dev:t.dev,ino:t.ino,mode:t.mode,nlink:t.nlink,uid:t.uid,gid:t.gid,rdev:t.rdev,blksize:t.blksize,blocks:t.blocks}}t.BaseDirectory=void 0,(p=t.BaseDirectory||(t.BaseDirectory={}))[p.Audio=1]="Audio",p[p.Cache=2]="Cache",p[p.Config=3]="Config",p[p.Data=4]="Data",p[p.LocalData=5]="LocalData",p[p.Document=6]="Document",p[p.Download=7]="Download",p[p.Picture=8]="Picture",p[p.Public=9]="Public",p[p.Video=10]="Video",p[p.Resource=11]="Resource",p[p.Temp=12]="Temp",p[p.AppConfig=13]="AppConfig",p[p.AppData=14]="AppData",p[p.AppLocalData=15]="AppLocalData",p[p.AppCache=16]="AppCache",p[p.AppLog=17]="AppLog",p[p.Desktop=18]="Desktop",p[p.Executable=19]="Executable",p[p.Font=20]="Font",p[p.Home=21]="Home",p[p.Runtime=22]="Runtime",p[p.Template=23]="Template",t.SeekMode=void 0,(d=t.SeekMode||(t.SeekMode={}))[d.Start=0]="Start",d[d.Current=1]="Current",d[d.End=2]="End";class h extends u{async read(t){if(0===t.byteLength)return 0;const e=await l("plugin:fs|read",{rid:this.rid,len:t.byteLength}),n=function(t){const e=new Uint8ClampedArray(t),n=e.byteLength;let i=0;for(let t=0;tt instanceof URL?t.toString():t),options:n,onEvent:o}),a=new L(r);return()=>{a.close()}}return t.FileHandle=h,t.copyFile=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol||e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|copy_file",{fromPath:t instanceof URL?t.toString():t,toPath:e instanceof URL?e.toString():e,options:n})},t.create=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=await l("plugin:fs|create",{path:t instanceof URL?t.toString():t,options:e});return new h(n)},t.exists=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return await l("plugin:fs|exists",{path:t instanceof URL?t.toString():t,options:e})},t.lstat=async function(t,e){return w(await l("plugin:fs|lstat",{path:t instanceof URL?t.toString():t,options:e}))},t.mkdir=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|mkdir",{path:t instanceof URL?t.toString():t,options:e})},t.open=y,t.readDir=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return await l("plugin:fs|read_dir",{path:t instanceof URL?t.toString():t,options:e})},t.readFile=g,t.readTextFile=async function(t,e){const n=await g(t,e);return new TextDecoder(e?.encoding??"utf-8").decode(n)},t.readTextFileLines=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=t instanceof URL?t.toString():t;return await Promise.resolve({path:n,rid:null,async next(){const t=new TextDecoder(e?.encoding??"utf-8");if(null===this.rid){const i=t.encoding;this.rid=await l("plugin:fs|read_text_file_lines",{path:n,options:null!=e?{...e,encoding:i}:void 0})}const i=await l("plugin:fs|read_text_file_lines_next",{rid:this.rid}),o=i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i),r=1===o[o.byteLength-1];if(r)return this.rid=null,{value:null,done:r};return{value:t.decode(o.slice(0,o.byteLength-1)),done:r}},[Symbol.asyncIterator](){return this}})},t.remove=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|remove",{path:t instanceof URL?t.toString():t,options:e})},t.rename=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol||e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|rename",{oldPath:t instanceof URL?t.toString():t,newPath:e instanceof URL?e.toString():e,options:n})},t.size=async function(t){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return await l("plugin:fs|size",{path:t instanceof URL?t.toString():t})},t.startAccessingSecurityScopedResource=async function(t){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|start_accessing_security_scoped_resource",{path:t instanceof URL?t.toString():t})},t.stat=async function(t,e){return w(await l("plugin:fs|stat",{path:t instanceof URL?t.toString():t,options:e}))},t.stopAccessingSecurityScopedResource=async function(t){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|stop_accessing_security_scoped_resource",{path:t instanceof URL?t.toString():t})},t.truncate=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|truncate",{path:t instanceof URL?t.toString():t,len:e,options:n})},t.watch=async function(t,e,n){return await U(t,e,{delayMs:2e3,...n})},t.watchImmediate=async function(t,e,n){return await U(t,e,{...n,delayMs:void 0})},t.writeFile=R,t.writeTextFile=async function(t,e,n){await R(t,(new TextEncoder).encode(e),n)},t}({});Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_FS__=function(t){"use strict";function e(t,e,n,i){if("function"==typeof e?t!==e||!i:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?i:"a"===n?i.call(t):i?i.value:e.get(t)}function n(t,e,n,i,o){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var i,o,r,a,s;"function"==typeof SuppressedError&&SuppressedError;const c="__TAURI_TO_IPC_KEY__";class f{constructor(t){i.set(this,void 0),o.set(this,0),r.set(this,[]),a.set(this,void 0),n(this,i,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}(t=>{const s=t.index;if("end"in t)return void(s==e(this,o,"f")?this.cleanupCallback():n(this,a,s));const c=t.message;if(s==e(this,o,"f")){for(e(this,i,"f").call(this,c),n(this,o,e(this,o,"f")+1);e(this,o,"f")in e(this,r,"f");){const t=e(this,r,"f")[e(this,o,"f")];e(this,i,"f").call(this,t),delete e(this,r,"f")[e(this,o,"f")],n(this,o,e(this,o,"f")+1)}e(this,o,"f")===e(this,a,"f")&&this.cleanupCallback()}else e(this,r,"f")[s]=c})}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){n(this,i,t)}get onmessage(){return e(this,i,"f")}[(i=new WeakMap,o=new WeakMap,r=new WeakMap,a=new WeakMap,c)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[c]()}}async function l(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}class u{get rid(){return e(this,s,"f")}constructor(t){s.set(this,void 0),n(this,s,t)}async close(){return l("plugin:resources|close",{rid:this.rid})}async[(s=new WeakMap,Symbol.asyncDispose)](){await this.close()}}var p,d;function w(t){return{isFile:t.isFile,isDirectory:t.isDirectory,isSymlink:t.isSymlink,size:t.size,mtime:null!==t.mtime?new Date(t.mtime):null,atime:null!==t.atime?new Date(t.atime):null,birthtime:null!==t.birthtime?new Date(t.birthtime):null,readonly:t.readonly,fileAttributes:t.fileAttributes,dev:t.dev,ino:t.ino,mode:t.mode,nlink:t.nlink,uid:t.uid,gid:t.gid,rdev:t.rdev,blksize:t.blksize,blocks:t.blocks}}t.BaseDirectory=void 0,(p=t.BaseDirectory||(t.BaseDirectory={}))[p.Audio=1]="Audio",p[p.Cache=2]="Cache",p[p.Config=3]="Config",p[p.Data=4]="Data",p[p.LocalData=5]="LocalData",p[p.Document=6]="Document",p[p.Download=7]="Download",p[p.Picture=8]="Picture",p[p.Public=9]="Public",p[p.Video=10]="Video",p[p.Resource=11]="Resource",p[p.Temp=12]="Temp",p[p.AppConfig=13]="AppConfig",p[p.AppData=14]="AppData",p[p.AppLocalData=15]="AppLocalData",p[p.AppCache=16]="AppCache",p[p.AppLog=17]="AppLog",p[p.Desktop=18]="Desktop",p[p.Executable=19]="Executable",p[p.Font=20]="Font",p[p.Home=21]="Home",p[p.Runtime=22]="Runtime",p[p.Template=23]="Template",t.SeekMode=void 0,(d=t.SeekMode||(t.SeekMode={}))[d.Start=0]="Start",d[d.Current=1]="Current",d[d.End=2]="End";class h extends u{async read(t){if(0===t.byteLength)return 0;const e=await l("plugin:fs|read",{rid:this.rid,len:t.byteLength}),n=function(t){const e=new Uint8ClampedArray(t),n=e.byteLength;let i=0;for(let t=0;tt instanceof URL?t.toString():t),options:n,onEvent:o});return new L(r)}return t.FileHandle=h,t.Watcher=L,t.copyFile=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol||e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|copy_file",{fromPath:t instanceof URL?t.toString():t,toPath:e instanceof URL?e.toString():e,options:n})},t.create=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=await l("plugin:fs|create",{path:t instanceof URL?t.toString():t,options:e});return new h(n)},t.exists=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return await l("plugin:fs|exists",{path:t instanceof URL?t.toString():t,options:e})},t.lstat=async function(t,e){return w(await l("plugin:fs|lstat",{path:t instanceof URL?t.toString():t,options:e}))},t.mkdir=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|mkdir",{path:t instanceof URL?t.toString():t,options:e})},t.open=y,t.readDir=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return await l("plugin:fs|read_dir",{path:t instanceof URL?t.toString():t,options:e})},t.readFile=g,t.readTextFile=async function(t,e){const n=await g(t,e);return new TextDecoder(e?.encoding??"utf-8").decode(n)},t.readTextFileLines=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");const n=t instanceof URL?t.toString():t;return await Promise.resolve({path:n,rid:null,async next(){const t=new TextDecoder(e?.encoding??"utf-8");if(null===this.rid){const i=t.encoding;this.rid=await l("plugin:fs|read_text_file_lines",{path:n,options:null!=e?{...e,encoding:i}:void 0})}const i=await l("plugin:fs|read_text_file_lines_next",{rid:this.rid}),o=i instanceof ArrayBuffer?new Uint8Array(i):Uint8Array.from(i),r=1===o[o.byteLength-1];if(r)return this.rid=null,{value:null,done:r};return{value:t.decode(o.slice(0,o.byteLength-1)),done:r}},[Symbol.asyncIterator](){return this}})},t.remove=async function(t,e){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|remove",{path:t instanceof URL?t.toString():t,options:e})},t.rename=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol||e instanceof URL&&"file:"!==e.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|rename",{oldPath:t instanceof URL?t.toString():t,newPath:e instanceof URL?e.toString():e,options:n})},t.size=async function(t){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");return await l("plugin:fs|size",{path:t instanceof URL?t.toString():t})},t.startAccessingSecurityScopedResource=async function(t){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|start_accessing_security_scoped_resource",{path:t instanceof URL?t.toString():t})},t.stat=async function(t,e){return w(await l("plugin:fs|stat",{path:t instanceof URL?t.toString():t,options:e}))},t.stopAccessingSecurityScopedResource=async function(t){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|stop_accessing_security_scoped_resource",{path:t instanceof URL?t.toString():t})},t.truncate=async function(t,e,n){if(t instanceof URL&&"file:"!==t.protocol)throw new TypeError("Must be a file URL.");await l("plugin:fs|truncate",{path:t instanceof URL?t.toString():t,len:e,options:n})},t.watch=async function(t,e,n){return await U(t,e,{delayMs:2e3,...n})},t.watchImmediate=async function(t,e,n){return await U(t,e,{...n,delayMs:void 0})},t.writeFile=R,t.writeTextFile=async function(t,e,n){await R(t,(new TextEncoder).encode(e),n)},t}({});Object.defineProperty(window.__TAURI__,"fs",{value:__TAURI_PLUGIN_FS__})} diff --git a/plugins/fs/guest-js/index.ts b/plugins/fs/guest-js/index.ts index cf0a8182e..7b611a2ad 100644 --- a/plugins/fs/guest-js/index.ts +++ b/plugins/fs/guest-js/index.ts @@ -1283,19 +1283,18 @@ type WatchEventKindRemove = | { kind: 'folder' } | { kind: 'other' } -// TODO: Remove this in v3, return `Watcher` instead /** - * @since 2.0.0 + * A file system watcher. Call {@linkcode Watcher.close} to stop watching. + * + * @since 3.0.0 */ -type UnwatchFn = () => void - class Watcher extends Resource {} async function watchInternal( paths: string | string[] | URL | URL[], cb: (event: WatchEvent) => void, options: DebouncedWatchOptions -): Promise { +): Promise { const watchPaths = Array.isArray(paths) ? paths : [paths] for (const path of watchPaths) { @@ -1313,41 +1312,51 @@ async function watchInternal( onEvent }) - const watcher = new Watcher(rid) - - return () => { - void watcher.close() - } + return new Watcher(rid) } -// TODO: Return `Watcher` instead in v3 /** * Watch changes (after a delay) on files or directories. * + * @example + * ```typescript + * import { watch, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const watcher = await watch('app.conf', (event) => console.log(event), { baseDir: BaseDirectory.AppConfig }); + * // when you're done watching: + * await watcher.close(); + * ``` + * * @since 2.0.0 */ async function watch( paths: string | string[] | URL | URL[], cb: (event: WatchEvent) => void, options?: DebouncedWatchOptions -): Promise { +): Promise { return await watchInternal(paths, cb, { delayMs: 2000, ...options }) } -// TODO: Return `Watcher` instead in v3 /** * Watch changes on files or directories. * + * @example + * ```typescript + * import { watchImmediate, BaseDirectory } from '@tauri-apps/plugin-fs'; + * const watcher = await watchImmediate('app.conf', (event) => console.log(event), { baseDir: BaseDirectory.AppConfig }); + * // when you're done watching: + * await watcher.close(); + * ``` + * * @since 2.0.0 */ async function watchImmediate( paths: string | string[] | URL | URL[], cb: (event: WatchEvent) => void, options?: WatchOptions -): Promise { +): Promise { return await watchInternal(paths, cb, { ...options, delayMs: undefined @@ -1475,13 +1484,13 @@ export type { WatchEventKindAccess, WatchEventKindCreate, WatchEventKindModify, - WatchEventKindRemove, - UnwatchFn + WatchEventKindRemove } export { BaseDirectory, FileHandle, + Watcher, create, open, copyFile,