@react-native-gpui/fs 0.0.0-stage → 0.0.1-alpha.1

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.
@@ -0,0 +1,392 @@
1
+ //! The watcher behind `watch` and `unwatch`.
2
+ //!
3
+ //! `notify` gives each watcher a thread of its own and calls the handler on it.
4
+ //! That handler cannot borrow the module — it may outlive any single call and
5
+ //! runs on a thread the module does not own — so it is handed a clone of the
6
+ //! event emitter and an `Arc` of the debounce table instead. The module only
7
+ //! stores the `RecommendedWatcher`; dropping that value stops the OS watch, and
8
+ //! with it the thread, which is how a JS reload closes every watcher at once.
9
+
10
+ use std::collections::HashMap;
11
+ use std::path::{Path, PathBuf};
12
+ use std::sync::{Arc, Mutex};
13
+ use std::time::{Duration, Instant};
14
+
15
+ use notify::event::EventKind;
16
+ use notify::Watcher as _;
17
+ use notify::{RecommendedWatcher, RecursiveMode};
18
+ use rn_gpui_module::ModuleError;
19
+
20
+ use crate::{errors, RNFsModule, WatchEvent};
21
+
22
+ /// How long a path stays quiet after being reported.
23
+ ///
24
+ /// An editor saving a file usually writes it several times in a burst (write,
25
+ /// rename over the original, a truncated open), and a recursive watch on a build
26
+ /// directory can see dozens of events for one logical change. Fifty milliseconds
27
+ /// collapses a burst into the single event a caller can act on without making a
28
+ /// genuinely repeated edit invisible.
29
+ const DEBOUNCE: Duration = Duration::from_millis(50);
30
+
31
+ /// An installed watch, kept alive by the module's table.
32
+ ///
33
+ /// The field is never read: it exists so that removing the entry drops it, which
34
+ /// is what stops notify's thread and the platform watch.
35
+ pub struct Watcher {
36
+ _watcher: RecommendedWatcher,
37
+ }
38
+
39
+ /// What was last reported for a path, and when.
40
+ #[derive(Clone, Copy)]
41
+ struct LastReport {
42
+ at: Instant,
43
+ kind: EventKindTag,
44
+ }
45
+
46
+ /// Which of the three stable names an event carries.
47
+ ///
48
+ /// A copy of the mapping `kind_name` produces, so the debounce can compare two
49
+ /// events without allocating the string it will not send.
50
+ #[derive(Clone, Copy, PartialEq, Eq, Hash)]
51
+ enum EventKindTag {
52
+ Create,
53
+ Modify,
54
+ Remove,
55
+ Other,
56
+ }
57
+
58
+ /// Per-path timestamps of the last event that was reported.
59
+ ///
60
+ /// Shared with the handler through an `Arc` because the handler owns its state
61
+ /// for as long as the watcher lives, which is longer than any borrow of the
62
+ /// module. Paths that fall out of the window are pruned on every report, so a
63
+ /// long-running recursive watch cannot grow this without bound.
64
+ ///
65
+ /// The rule, in one sentence: **a burst is collapsed per path and per kind, and a
66
+ /// removal is never collapsed away.** Keeping only the first event of a burst
67
+ /// lost the end of it — a create followed by a remove reported only `create`, so
68
+ /// a caller was told about a file that no longer existed. Two adjustments fix
69
+ /// that without giving up the collapsing that makes an editor's write burst
70
+ /// readable:
71
+ ///
72
+ /// - a path's window is per kind, so a `remove` after a `create` is a different
73
+ /// event rather than a suppressed one;
74
+ /// - a removal always reports, whatever the window, because it is the last state
75
+ /// a path can reach and swallowing it leaves the caller's view permanently
76
+ /// wrong. The cost is a possible duplicate removal for one logical deletion,
77
+ /// which a watcher callback can tolerate; a missing one it cannot.
78
+ #[derive(Default)]
79
+ struct Debounce {
80
+ last: Mutex<HashMap<(PathBuf, EventKindTag), Instant>>,
81
+ }
82
+
83
+ impl Debounce {
84
+ /// Whether this event should be delivered now.
85
+ ///
86
+ /// Takes `now` rather than reading the clock so the window can be tested
87
+ /// without sleeping.
88
+ fn should_report_at(&self, path: &Path, kind: EventKindTag, now: Instant) -> bool {
89
+ let mut last = self.last.lock().unwrap_or_else(|error| error.into_inner());
90
+ let key = (path.to_path_buf(), kind);
91
+ let report = match last.get(&key).copied() {
92
+ Some(previous) => {
93
+ kind == EventKindTag::Remove || now.saturating_duration_since(previous) >= DEBOUNCE
94
+ }
95
+ None => true,
96
+ };
97
+ if report {
98
+ last.retain(|_, previous| now.saturating_duration_since(*previous) < DEBOUNCE);
99
+ last.insert(key, now);
100
+ }
101
+ report
102
+ }
103
+
104
+ fn should_report(&self, path: &Path, kind: EventKindTag) -> bool {
105
+ self.should_report_at(path, kind, Instant::now())
106
+ }
107
+ }
108
+
109
+ /// The stable string the JS layer matches on.
110
+ ///
111
+ /// Everything that is not a create, modify or remove — access events, and the
112
+ /// catch-all notify reports when a backend cannot be more precise — becomes
113
+ /// `other` rather than leaking a platform-specific spelling.
114
+ fn kind_name(kind: &EventKind) -> &'static str {
115
+ match kind {
116
+ EventKind::Create(_) => "create",
117
+ EventKind::Modify(_) => "modify",
118
+ EventKind::Remove(_) => "remove",
119
+ _ => "other",
120
+ }
121
+ }
122
+
123
+ /// The tag `kind_name` corresponds to, for the debounce to compare by.
124
+ fn kind_tag(kind: &EventKind) -> EventKindTag {
125
+ match kind {
126
+ EventKind::Create(_) => EventKindTag::Create,
127
+ EventKind::Modify(_) => EventKindTag::Modify,
128
+ EventKind::Remove(_) => EventKindTag::Remove,
129
+ _ => EventKindTag::Other,
130
+ }
131
+ }
132
+
133
+ /// An event path as the caller should see it.
134
+ ///
135
+ /// Node reports the path relative to the watch root, and a name alone for a
136
+ /// single-file watch. The platform reports an absolute path, and on macOS a
137
+ /// *canonical* one: FSEvents resolves `/var` to `/private/var`, so a plain string
138
+ /// prefix check against the path the caller gave strips nothing and the callback
139
+ /// receives an absolute path. Both roots are therefore tried, and the given one
140
+ /// last so a caller watching through a symlink still gets a relative name.
141
+ fn relative_event_path(path: &Path, roots: &[PathBuf], watching_file: bool) -> String {
142
+ if watching_file {
143
+ return path
144
+ .file_name()
145
+ .map(|name| name.to_string_lossy().into_owned())
146
+ .unwrap_or_else(|| path.to_string_lossy().into_owned());
147
+ }
148
+ for root in roots {
149
+ if let Ok(rest) = path.strip_prefix(root) {
150
+ // Normalised to `/`: Node reports relative paths with forward
151
+ // slashes on every platform, and a Windows caller would otherwise get
152
+ // `nested\file.txt` where Node gives `nested/file.txt`.
153
+ return rest.to_string_lossy().replace('\\', "/");
154
+ }
155
+ }
156
+ path.to_string_lossy().into_owned()
157
+ }
158
+
159
+ /// A notify failure as the error JS expects.
160
+ ///
161
+ /// Most failures wrap an `io::Error` that already maps through `errors::from_io`;
162
+ /// the ones that do not are described well enough by notify to be reported with
163
+ /// the closest Node code.
164
+ fn watcher_error(error: notify::Error, path: &str) -> ModuleError {
165
+ use notify::ErrorKind;
166
+ let io = match error.kind {
167
+ ErrorKind::Io(error) => error,
168
+ ErrorKind::PathNotFound => std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
169
+ ErrorKind::MaxFilesWatch => {
170
+ std::io::Error::new(std::io::ErrorKind::Other, "OS file watch limit reached")
171
+ }
172
+ other => std::io::Error::new(std::io::ErrorKind::Other, format!("{other:?}")),
173
+ };
174
+ errors::from_io(io, path)
175
+ }
176
+
177
+ /// Starts watching `path`, returning the id events carry.
178
+ pub fn watch(module: &RNFsModule, path: String, recursive: bool) -> Result<f64, ModuleError> {
179
+ // Node fails a watch on a missing path immediately, and so must this: the
180
+ // platform watchers otherwise accept the path and then go silent, which is
181
+ // indistinguishable at the call site from a directory nothing ever touches.
182
+ let target = PathBuf::from(&path);
183
+ if !target.exists() {
184
+ return Err(errors::from_io(
185
+ std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
186
+ &path,
187
+ ));
188
+ }
189
+
190
+ let id = module.take_watcher_id();
191
+ let debounce = Arc::new(Debounce::default());
192
+ let events = module.events.clone();
193
+ // The canonical root is what macOS actually reports inside, and the given
194
+ // root is what the caller passed; keeping both means the relative name
195
+ // survives either spelling.
196
+ let watching_file = target.is_file();
197
+ let roots: Vec<PathBuf> = {
198
+ let mut roots = Vec::new();
199
+ if let Ok(canonical) = target.canonicalize() {
200
+ roots.push(canonical);
201
+ }
202
+ roots.push(target.clone());
203
+ roots
204
+ };
205
+ let handler = move |result: notify::Result<notify::Event>| {
206
+ // A backend error has nowhere to go: it is not tied to a call, and
207
+ // reporting it through the event stream would make it look like a file
208
+ // change. notify's thread keeps running, so later events still arrive.
209
+ let Ok(event) = result else { return };
210
+ let kind = kind_name(&event.kind);
211
+ let tag = kind_tag(&event.kind);
212
+ // One platform event can name several paths — a directory move, for
213
+ // instance — and a caller wants one delivery per path.
214
+ for path in &event.paths {
215
+ if !debounce.should_report(path, tag) {
216
+ continue;
217
+ }
218
+ events.on_watch_event(WatchEvent {
219
+ id: f64::from(id),
220
+ kind: kind.to_string(),
221
+ path: relative_event_path(path, &roots, watching_file),
222
+ });
223
+ }
224
+ };
225
+
226
+ let mut watcher =
227
+ notify::recommended_watcher(handler).map_err(|error| watcher_error(error, &path))?;
228
+ let mode = if recursive {
229
+ RecursiveMode::Recursive
230
+ } else {
231
+ RecursiveMode::NonRecursive
232
+ };
233
+ watcher
234
+ .watch(&target, mode)
235
+ .map_err(|error| watcher_error(error, &path))?;
236
+
237
+ module
238
+ .watchers
239
+ .lock()
240
+ .unwrap_or_else(|error| error.into_inner())
241
+ .insert(id, Watcher { _watcher: watcher });
242
+ Ok(f64::from(id))
243
+ }
244
+
245
+ /// Stops a watcher. An id that is already gone is not a failure: JS may close a
246
+ /// watcher twice, or close one whose module has already dropped it.
247
+ pub fn unwatch(module: &RNFsModule, id: f64) {
248
+ let Ok(id) = crate::module::descriptor(id) else {
249
+ return;
250
+ };
251
+ // Removing the entry drops the watcher, which stops notify's thread.
252
+ module
253
+ .watchers
254
+ .lock()
255
+ .unwrap_or_else(|error| error.into_inner())
256
+ .remove(&id);
257
+ }
258
+
259
+ #[cfg(test)]
260
+ mod tests {
261
+ use super::*;
262
+ use notify::event::{CreateKind, ModifyKind, RemoveKind};
263
+
264
+ #[test]
265
+ fn event_kinds_map_to_stable_names() {
266
+ assert_eq!(kind_name(&EventKind::Create(CreateKind::File)), "create");
267
+ assert_eq!(kind_name(&EventKind::Create(CreateKind::Any)), "create");
268
+ assert_eq!(kind_name(&EventKind::Modify(ModifyKind::Any)), "modify");
269
+ assert_eq!(kind_name(&EventKind::Remove(RemoveKind::Any)), "remove");
270
+ assert_eq!(kind_name(&EventKind::Any), "other");
271
+ }
272
+
273
+ #[test]
274
+ fn the_first_event_for_a_path_is_reported() {
275
+ let debounce = Debounce::default();
276
+ let now = Instant::now();
277
+ assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, now));
278
+ }
279
+
280
+ #[test]
281
+ fn a_burst_is_collapsed_into_one_event() {
282
+ let debounce = Debounce::default();
283
+ let start = Instant::now();
284
+ assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, start));
285
+ assert!(!debounce.should_report_at(
286
+ Path::new("/a"),
287
+ EventKindTag::Modify,
288
+ start + Duration::from_millis(10)
289
+ ));
290
+ assert!(!debounce.should_report_at(
291
+ Path::new("/a"),
292
+ EventKindTag::Modify,
293
+ start + Duration::from_millis(49)
294
+ ));
295
+ assert!(debounce.should_report_at(
296
+ Path::new("/a"),
297
+ EventKindTag::Modify,
298
+ start + Duration::from_millis(50)
299
+ ));
300
+ }
301
+
302
+ #[test]
303
+ fn a_remove_inside_a_burst_is_not_swallowed() {
304
+ let debounce = Debounce::default();
305
+ let start = Instant::now();
306
+ let path = Path::new("/a");
307
+ // A create and the removal that follows it are two states of the path,
308
+ // not one event seen twice: reporting only the create tells a caller
309
+ // about a file that is already gone.
310
+ assert!(debounce.should_report_at(path, EventKindTag::Create, start));
311
+ assert!(debounce.should_report_at(
312
+ path,
313
+ EventKindTag::Remove,
314
+ start + Duration::from_millis(5)
315
+ ));
316
+ // A second removal for the same deletion may arrive; a missing one may
317
+ // not, so this is deliberately the lenient side of the trade.
318
+ assert!(debounce.should_report_at(
319
+ path,
320
+ EventKindTag::Remove,
321
+ start + Duration::from_millis(10)
322
+ ));
323
+ }
324
+
325
+ #[test]
326
+ fn a_modify_burst_still_collapses() {
327
+ let debounce = Debounce::default();
328
+ let start = Instant::now();
329
+ let path = Path::new("/a");
330
+ assert!(debounce.should_report_at(path, EventKindTag::Modify, start));
331
+ // Two writes a millisecond apart are one logical change.
332
+ assert!(!debounce.should_report_at(
333
+ path,
334
+ EventKindTag::Modify,
335
+ start + Duration::from_millis(1)
336
+ ));
337
+ }
338
+
339
+ #[test]
340
+ fn a_canonical_root_still_yields_a_relative_name() {
341
+ // macOS reports canonical paths, so a watch on `/var/...` sees
342
+ // `/private/var/...`. Both roots are tried, and the caller gets a name
343
+ // relative to the one they asked about either way.
344
+ let canonical = PathBuf::from("/private/var/folders/x/tmp");
345
+ let given = PathBuf::from("/var/folders/x/tmp");
346
+ let roots = vec![canonical.clone(), given];
347
+ let event = canonical.join("nested").join("file.txt");
348
+ assert_eq!(
349
+ relative_event_path(&event, &roots, false),
350
+ "nested/file.txt"
351
+ );
352
+ // A path outside every root is passed through rather than mangled.
353
+ assert_eq!(
354
+ relative_event_path(Path::new("/elsewhere/file.txt"), &roots, false),
355
+ "/elsewhere/file.txt"
356
+ );
357
+ // Watching a single file reports its name, which is what Node does.
358
+ assert_eq!(relative_event_path(&event, &roots, true), "file.txt");
359
+ }
360
+
361
+ #[test]
362
+ fn debouncing_is_per_path() {
363
+ let debounce = Debounce::default();
364
+ let start = Instant::now();
365
+ assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, start));
366
+ assert!(debounce.should_report_at(
367
+ Path::new("/b"),
368
+ EventKindTag::Modify,
369
+ start + Duration::from_millis(1)
370
+ ));
371
+ assert!(!debounce.should_report_at(
372
+ Path::new("/a"),
373
+ EventKindTag::Modify,
374
+ start + Duration::from_millis(2)
375
+ ));
376
+ }
377
+
378
+ #[test]
379
+ fn paths_outside_the_window_are_forgotten() {
380
+ let debounce = Debounce::default();
381
+ let start = Instant::now();
382
+ assert!(debounce.should_report_at(Path::new("/a"), EventKindTag::Modify, start));
383
+ assert!(debounce.should_report_at(
384
+ Path::new("/b"),
385
+ EventKindTag::Modify,
386
+ start + Duration::from_millis(60)
387
+ ));
388
+ let last = debounce.last.lock().unwrap();
389
+ assert_eq!(last.len(), 1, "the stale path should have been pruned");
390
+ assert!(last.contains_key(&(PathBuf::from("/b"), EventKindTag::Modify)));
391
+ }
392
+ }
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@react-native-gpui/fs",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "File system access for React Native GPUI apps",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/streetwriters/react-native-gpui-modules.git",
9
+ "directory": "modules/fs"
10
+ },
11
+ "homepage": "https://github.com/streetwriters/react-native-gpui-modules/tree/main/modules/fs#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/streetwriters/react-native-gpui-modules/issues"
14
+ },
15
+ "main": "src/index.ts",
16
+ "files": [
17
+ "src",
18
+ "desktop",
19
+ "react-native.config.js",
20
+ "README.md",
21
+ "LICENSE",
22
+ "CHANGELOG.md",
23
+ "!**/*.test.js",
24
+ "!**/*.test.mjs",
25
+ "!**/*.test.ts",
26
+ "!**/*.test.cjs"
27
+ ],
28
+ "codegenConfig": {
29
+ "name": "RNFsModuleSpec",
30
+ "type": "modules",
31
+ "jsSrcsDir": "src"
32
+ },
33
+ "peerDependencies": {
34
+ "react-native": "0.87.x"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "tag": "latest"
39
+ }
40
+ }
@@ -0,0 +1,9 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ desktop: {
5
+ crate: 'rn-gpui-fs',
6
+ },
7
+ },
8
+ },
9
+ };
@@ -0,0 +1,235 @@
1
+ import type { CodegenTypes, TurboModule } from 'react-native';
2
+ import { TurboModuleRegistry } from 'react-native';
3
+
4
+ /**
5
+ * How a path entry is classified: 0 file, 1 directory, 2 symlink, 3 other.
6
+ *
7
+ * A plain number rather than a string or enum so it crosses the bridge without a
8
+ * lookup table and survives being read back out of a plain object in JS.
9
+ */
10
+ export type FileKind = number;
11
+
12
+ export type StatResult = {
13
+ size: number;
14
+ mode: number;
15
+ kind: FileKind;
16
+ atimeMs: number;
17
+ mtimeMs: number;
18
+ ctimeMs: number;
19
+ birthtimeMs: number;
20
+ dev: number;
21
+ ino: number;
22
+ nlink: number;
23
+ uid: number;
24
+ gid: number;
25
+ };
26
+
27
+ export type DirEntry = {
28
+ name: string;
29
+ kind: FileKind;
30
+ parentPath: string;
31
+ };
32
+
33
+ export type DialogFilter = {
34
+ name: string;
35
+ extensions: Array<string>;
36
+ };
37
+
38
+ export type OpenDialogOptions = {
39
+ title?: string;
40
+ defaultPath?: string;
41
+ multiple: boolean;
42
+ directory: boolean;
43
+ filters: Array<DialogFilter>;
44
+ };
45
+
46
+ export type SaveDialogOptions = {
47
+ title?: string;
48
+ defaultPath?: string;
49
+ defaultName?: string;
50
+ filters: Array<DialogFilter>;
51
+ };
52
+
53
+ export type KnownPaths = {
54
+ home: string;
55
+ documents: string;
56
+ downloads: string;
57
+ desktop: string;
58
+ temp: string;
59
+ appData: string;
60
+ appCache: string;
61
+ appLogs: string;
62
+ };
63
+
64
+ export type WatchEvent = {
65
+ id: number;
66
+ kind: string;
67
+ path: string;
68
+ };
69
+
70
+ export type DownloadHeader = {
71
+ name: string;
72
+ value: string;
73
+ };
74
+
75
+ export type DownloadOptions = {
76
+ id: number;
77
+ method: string;
78
+ headers: Array<DownloadHeader>;
79
+ };
80
+
81
+ /**
82
+ * What a download resolves with. A non-2xx answer is not an error here: it
83
+ * comes back with its `status` and a small `body`, and no file is written.
84
+ */
85
+ export type DownloadResult = {
86
+ status: number;
87
+ headers: Array<DownloadHeader>;
88
+ bytes: number;
89
+ body?: string;
90
+ };
91
+
92
+ export type DownloadProgress = {
93
+ id: number;
94
+ received: number;
95
+ // -1 when the server did not say how long the body is.
96
+ total: number;
97
+ };
98
+
99
+ export type UploadOptions = {
100
+ id: number;
101
+ method: string;
102
+ headers: Array<DownloadHeader>;
103
+ // Where in the file the body starts.
104
+ offset: number;
105
+ // How many bytes to send; -1 sends everything from `offset`.
106
+ length: number;
107
+ };
108
+
109
+ /**
110
+ * What an upload resolves with. Any status is a result; `body` is the first
111
+ * part of the response.
112
+ */
113
+ export type UploadResult = {
114
+ status: number;
115
+ headers: Array<DownloadHeader>;
116
+ body: string;
117
+ };
118
+
119
+ export type UploadProgress = {
120
+ id: number;
121
+ sent: number;
122
+ total: number;
123
+ };
124
+
125
+ /**
126
+ * The file system module: one instance, on its own thread, so a slow file never
127
+ * blocks JS or the UI.
128
+ *
129
+ * A method that returns a value runs on the JS thread; `void` and `Promise`
130
+ * methods run on the module's thread. The writers below therefore return
131
+ * `boolean` — a `void` method would be moved off the JS thread, which is wrong
132
+ * for a `*Sync` call. They always return true and throw on failure.
133
+ */
134
+ export interface Spec extends TurboModule {
135
+ // Text is separate from bytes so a `utf8` read or write needs no buffer.
136
+ readFile(path: string): Promise<ArrayBuffer>;
137
+ readTextFile(path: string): Promise<string>;
138
+ writeFile(
139
+ path: string,
140
+ data: ArrayBuffer,
141
+ flags: number,
142
+ mode: number,
143
+ ): Promise<void>;
144
+ writeTextFile(
145
+ path: string,
146
+ data: string,
147
+ flags: number,
148
+ mode: number,
149
+ ): Promise<void>;
150
+
151
+ readdir(path: string, recursive: boolean): Promise<Array<DirEntry>>;
152
+ // `followSymlinks` false is `lstat`.
153
+ stat(path: string, followSymlinks: boolean): Promise<StatResult>;
154
+ mkdir(path: string, recursive: boolean, mode: number): Promise<void>;
155
+ rm(path: string, recursive: boolean, force: boolean): Promise<void>;
156
+ rename(from: string, to: string): Promise<void>;
157
+ copy(from: string, to: string, recursive: boolean): Promise<void>;
158
+ access(path: string, mode: number): Promise<void>;
159
+ realpath(path: string): Promise<string>;
160
+ readlink(path: string): Promise<string>;
161
+ // `type` is Node's, and only Windows acts on it: `'dir'` and `'junction'`
162
+ // create a directory link, `null` decides from the resolved target, and a
163
+ // plain `'file'` is a file link. Unix ignores it, because the syscall does.
164
+ // Without it a Windows directory link was created as a file link and could
165
+ // not be traversed.
166
+ symlink(target: string, path: string, type: string | null): Promise<void>;
167
+ truncate(path: string, length: number): Promise<void>;
168
+ utimes(path: string, atimeMs: number, mtimeMs: number): Promise<void>;
169
+ chmod(path: string, mode: number): Promise<void>;
170
+ mkdtemp(prefix: string): Promise<string>;
171
+
172
+ // File handles. `position` -1 means "at the current offset".
173
+ open(path: string, flags: number, mode: number): Promise<number>;
174
+ read(fd: number, length: number, position: number): Promise<ArrayBuffer>;
175
+ write(fd: number, data: ArrayBuffer, position: number): Promise<number>;
176
+ fstat(fd: number): Promise<StatResult>;
177
+ fsync(fd: number): Promise<void>;
178
+ ftruncate(fd: number, length: number): Promise<void>;
179
+ close(fd: number): Promise<void>;
180
+
181
+ showOpenDialog(options: OpenDialogOptions): Promise<Array<string>>;
182
+ showSaveDialog(options: SaveDialogOptions): Promise<string | null>;
183
+ reveal(path: string): Promise<void>;
184
+ openExternal(path: string): Promise<void>;
185
+
186
+ // Returns the watcher id; events arrive through `onWatchEvent`.
187
+ watch(path: string, recursive: boolean): Promise<number>;
188
+ unwatch(id: number): void;
189
+
190
+ readFileSync(path: string): ArrayBuffer;
191
+ readTextFileSync(path: string): string;
192
+ writeFileSync(
193
+ path: string,
194
+ data: ArrayBuffer,
195
+ flags: number,
196
+ mode: number,
197
+ ): boolean;
198
+ writeTextFileSync(
199
+ path: string,
200
+ data: string,
201
+ flags: number,
202
+ mode: number,
203
+ ): boolean;
204
+ statSync(path: string, followSymlinks: boolean): StatResult | null;
205
+ existsSync(path: string): boolean;
206
+ readdirSync(path: string, recursive: boolean): Array<DirEntry>;
207
+ mkdirSync(path: string, recursive: boolean, mode: number): boolean;
208
+ rmSync(path: string, recursive: boolean, force: boolean): boolean;
209
+ renameSync(from: string, to: string): boolean;
210
+ copyFileSync(from: string, to: string): boolean;
211
+
212
+ download(
213
+ url: string,
214
+ path: string,
215
+ options: DownloadOptions,
216
+ ): Promise<DownloadResult>;
217
+ // Returns whether a download with that id was running.
218
+ cancelDownload(id: number): boolean;
219
+
220
+ upload(
221
+ url: string,
222
+ path: string,
223
+ options: UploadOptions,
224
+ ): Promise<UploadResult>;
225
+ // Returns whether an upload with that id was running.
226
+ cancelUpload(id: number): boolean;
227
+
228
+ getPaths(): KnownPaths;
229
+
230
+ readonly onWatchEvent: CodegenTypes.EventEmitter<WatchEvent>;
231
+ readonly onDownloadProgress: CodegenTypes.EventEmitter<DownloadProgress>;
232
+ readonly onUploadProgress: CodegenTypes.EventEmitter<UploadProgress>;
233
+ }
234
+
235
+ export default TurboModuleRegistry.getEnforcing<Spec>('RNFsModule');