@gjsify/fs 0.1.15 → 0.3.0

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.
Files changed (67) hide show
  1. package/lib/esm/callback.js +22 -13
  2. package/lib/esm/cp.js +253 -0
  3. package/lib/esm/dir.js +160 -0
  4. package/lib/esm/fd-ops.js +189 -0
  5. package/lib/esm/file-handle.js +263 -84
  6. package/lib/esm/fs-watcher.js +88 -4
  7. package/lib/esm/glob.js +164 -0
  8. package/lib/esm/index.js +128 -2
  9. package/lib/esm/promises.js +90 -27
  10. package/lib/esm/read-stream.js +53 -43
  11. package/lib/esm/stat-watcher.js +121 -0
  12. package/lib/esm/statfs.js +57 -0
  13. package/lib/esm/sync.js +70 -52
  14. package/lib/esm/utils.js +7 -0
  15. package/lib/esm/utimes.js +62 -0
  16. package/lib/esm/write-stream.js +2 -5
  17. package/lib/types/cp.d.ts +18 -0
  18. package/lib/types/cp.spec.d.ts +2 -0
  19. package/lib/types/dir.d.ts +29 -0
  20. package/lib/types/dir.spec.d.ts +2 -0
  21. package/lib/types/fd-ops.d.ts +57 -0
  22. package/lib/types/fd-ops.spec.d.ts +2 -0
  23. package/lib/types/file-handle.d.ts +34 -4
  24. package/lib/types/fs-watcher.d.ts +9 -2
  25. package/lib/types/glob.d.ts +8 -0
  26. package/lib/types/glob.spec.d.ts +2 -0
  27. package/lib/types/index.d.ts +51 -1
  28. package/lib/types/promises.d.ts +31 -4
  29. package/lib/types/read-stream.d.ts +3 -1
  30. package/lib/types/stat-watcher.d.ts +21 -0
  31. package/lib/types/statfs.d.ts +35 -0
  32. package/lib/types/statfs.spec.d.ts +2 -0
  33. package/lib/types/sync.d.ts +4 -7
  34. package/lib/types/utils.d.ts +2 -0
  35. package/lib/types/utimes.d.ts +13 -0
  36. package/lib/types/utimes.spec.d.ts +2 -0
  37. package/lib/types/watch.spec.d.ts +2 -0
  38. package/lib/types/watchfile.spec.d.ts +2 -0
  39. package/lib/types/write-stream.d.ts +1 -2
  40. package/package.json +12 -12
  41. package/src/callback.ts +22 -13
  42. package/src/cp.spec.ts +181 -0
  43. package/src/cp.ts +328 -0
  44. package/src/dir.spec.ts +204 -0
  45. package/src/dir.ts +199 -0
  46. package/src/fd-ops.spec.ts +234 -0
  47. package/src/fd-ops.ts +251 -0
  48. package/src/file-handle.ts +264 -94
  49. package/src/fs-watcher.ts +101 -6
  50. package/src/glob.spec.ts +201 -0
  51. package/src/glob.ts +205 -0
  52. package/src/index.ts +74 -0
  53. package/src/promises.ts +94 -29
  54. package/src/read-stream.ts +49 -43
  55. package/src/stat-watcher.ts +116 -0
  56. package/src/statfs.spec.ts +67 -0
  57. package/src/statfs.ts +92 -0
  58. package/src/streams.spec.ts +58 -0
  59. package/src/sync.ts +75 -57
  60. package/src/test.mts +13 -2
  61. package/src/utils.ts +10 -0
  62. package/src/utimes.spec.ts +113 -0
  63. package/src/utimes.ts +97 -0
  64. package/src/watch.spec.ts +171 -0
  65. package/src/watchfile.spec.ts +185 -0
  66. package/src/write-stream.ts +5 -8
  67. package/tsconfig.tsbuildinfo +1 -1
@@ -1,63 +1,150 @@
1
- import { warnNotImplemented, notImplemented } from "@gjsify/utils";
1
+ import { createGLibFileError } from "@gjsify/utils";
2
2
  import { ReadStream } from "./read-stream.js";
3
3
  import { WriteStream } from "./write-stream.js";
4
- import { Stats } from "./stats.js";
4
+ import { Stats, BigIntStats, STAT_ATTRIBUTES } from "./stats.js";
5
5
  import { getEncodingFromOptions, encodeUint8Array } from "./encoding.js";
6
+ import { normalizePath } from "./utils.js";
7
+ import { chmodSync, chownSync } from "./sync.js";
6
8
  import GLib from "@girs/glib-2.0";
7
9
  import Gio from "@girs/gio-2.0";
10
+ import { createInterface } from "node:readline";
8
11
  import { Buffer } from "node:buffer";
9
- const GLIB_FILE_ERROR_TO_NODE = {
10
- 0: "EEXIST",
11
- 1: "EISDIR",
12
- 2: "EACCES",
13
- 3: "ENAMETOOLONG",
14
- 4: "ENOENT",
15
- 5: "ENOTDIR",
16
- 6: "ENXIO",
17
- 7: "ENODEV",
18
- 8: "EROFS",
19
- 11: "ELOOP",
20
- 12: "ENOSPC",
21
- 13: "ENOMEM",
22
- 14: "EMFILE",
23
- 15: "ENFILE",
24
- 16: "EBADF",
25
- 17: "EINVAL",
26
- 18: "EPIPE",
27
- 21: "EIO",
28
- 22: "EPERM",
29
- 24: "EIO"
30
- };
12
+ const O_WRONLY = 1;
13
+ const O_RDWR = 2;
14
+ const O_CREAT = 64;
15
+ const O_TRUNC = 512;
16
+ const O_APPEND = 1024;
17
+ function resolveIOMode(flags) {
18
+ if (flags === void 0 || flags === null) return "r";
19
+ if (typeof flags === "number") {
20
+ const rdwr = (flags & O_RDWR) !== 0;
21
+ const wronly = (flags & O_WRONLY) !== 0;
22
+ const append = (flags & O_APPEND) !== 0;
23
+ const trunc = (flags & O_TRUNC) !== 0;
24
+ if (rdwr) return trunc ? "w+" : "r+";
25
+ if (wronly) return append ? "a" : "w";
26
+ return "r";
27
+ }
28
+ switch (flags) {
29
+ case "ax":
30
+ case "wx":
31
+ return "w";
32
+ case "ax+":
33
+ case "wx+":
34
+ return "w+";
35
+ case "as":
36
+ case "rs+":
37
+ return "r+";
38
+ case "as+":
39
+ return "a+";
40
+ default:
41
+ return flags;
42
+ }
43
+ }
44
+ function openIOChannel(path, mode, creat) {
45
+ try {
46
+ return GLib.IOChannel.new_file(path, mode);
47
+ } catch (err) {
48
+ const gErr = err;
49
+ if (creat && mode === "r+" && gErr?.code === GLib.FileError.NOENT) {
50
+ GLib.file_set_contents(path, new Uint8Array(0));
51
+ return GLib.IOChannel.new_file(path, mode);
52
+ }
53
+ throw err;
54
+ }
55
+ }
31
56
  function mapOpenError(err, path) {
32
- const gErr = err;
33
- const msg = gErr?.message ?? "";
34
- const code = GLIB_FILE_ERROR_TO_NODE[gErr?.code ?? -1] ?? "EIO";
35
- const error = new Error(`${code}: ${msg || "unknown error"}, open '${path}'`);
36
- error.code = code;
37
- error.syscall = "open";
38
- error.path = path;
39
- return error;
57
+ return createGLibFileError(err, "open", { path });
40
58
  }
41
59
  class FileHandle {
42
60
  constructor(options) {
43
61
  this.options = options;
44
62
  this.options.flags ||= "r";
45
63
  this.options.mode ||= 438;
64
+ const pathStr = normalizePath(options.path);
65
+ const creat = typeof options.flags === "number" && (options.flags & O_CREAT) !== 0;
66
+ const ioMode = resolveIOMode(options.flags);
46
67
  try {
47
- this._file = GLib.IOChannel.new_file(options.path.toString(), this.options.flags);
68
+ this._file = openIOChannel(pathStr, ioMode, creat);
48
69
  } catch (err) {
49
- throw mapOpenError(err, options.path.toString());
70
+ throw mapOpenError(err, pathStr);
50
71
  }
51
72
  this._file.set_encoding(null);
52
73
  this.fd = this._file.unix_get_fd();
74
+ this._gFile = Gio.File.new_for_path(pathStr);
75
+ this._ioMode = ioMode;
53
76
  FileHandle.instances[this.fd] = this;
54
77
  return FileHandle.getInstance(this.fd);
55
78
  }
56
79
  options;
57
80
  /** Not part of the default implementation, used internal by gjsify */
58
81
  _file;
82
+ /**
83
+ * Lazily-opened Gio streams for positional read() / write() so each call does
84
+ * not re-load the entire file via Gio.File.load_contents(). The IOStream is
85
+ * used when the handle was opened with write capability (r+, w, w+, a, a+) —
86
+ * it shares seek state between input and output so writes are visible to
87
+ * subsequent reads without a flush. For read-only handles we only open a
88
+ * FileInputStream; trying to open_readwrite on a read-only file can fall
89
+ * back to create_readwrite(REPLACE_DESTINATION) which would truncate it.
90
+ */
91
+ _ioStream = null;
92
+ _readStream = null;
93
+ _gFile;
94
+ _ioMode;
95
+ // Serialize async I/O on the shared FileIOStream. Concurrent write_bytes_async
96
+ // calls hit Gio.IOErrorEnum.PENDING ("Datenstrom hat noch einen ausstehenden
97
+ // Vorgang"); overlapping seek()s on the shared cursor also corrupt positions.
98
+ // random-access-file (used by fs-chunk-store / webtorrent) issues many
99
+ // concurrent positional writes, so every async op on this handle chains
100
+ // through _ioLock.
101
+ _ioLock = Promise.resolve();
59
102
  /** Not part of the default implementation, used internal by gjsify */
60
103
  static instances = {};
104
+ /**
105
+ * Lazy-open the read-capable stream and return both the input stream and
106
+ * its seekable view. Both FileInputStream (read-only handle) and
107
+ * FileIOStream (read/write handle) implement Gio.Seekable, but we return
108
+ * both to avoid callers needing to know which concrete type they got.
109
+ */
110
+ _getReadStream() {
111
+ if (this._ioStream) {
112
+ return {
113
+ input: this._ioStream.get_input_stream(),
114
+ seekable: this._ioStream
115
+ };
116
+ }
117
+ if (this._ioMode === "r") {
118
+ if (!this._readStream) this._readStream = this._gFile.read(null);
119
+ return {
120
+ input: this._readStream,
121
+ seekable: this._readStream
122
+ };
123
+ }
124
+ this._ioStream = this._gFile.open_readwrite(null);
125
+ return {
126
+ input: this._ioStream.get_input_stream(),
127
+ seekable: this._ioStream
128
+ };
129
+ }
130
+ /** Lazy-open the write-capable stream (IOStream) for this handle. Only valid
131
+ * when the handle was opened with a write-capable mode. */
132
+ _getWriteStream() {
133
+ if (this._ioStream) return this._ioStream;
134
+ if (this._ioMode === "r") {
135
+ throw new Error("FileHandle opened read-only; cannot write");
136
+ }
137
+ this._ioStream = this._gFile.open_readwrite(null);
138
+ return this._ioStream;
139
+ }
140
+ /** Serialize an async operation on the shared FileIOStream. */
141
+ _serialize(op) {
142
+ const prev = this._ioLock;
143
+ const next = prev.catch(() => {
144
+ }).then(op);
145
+ this._ioLock = next;
146
+ return next;
147
+ }
61
148
  /**
62
149
  * The numeric file descriptor managed by the {FileHandle} object.
63
150
  * @since v10.0.0
@@ -98,7 +185,7 @@ class FileHandle {
98
185
  * @return Fulfills with `undefined` upon success.
99
186
  */
100
187
  async chown(uid, gid) {
101
- warnNotImplemented("fs.FileHandle.chown");
188
+ chownSync(normalizePath(this.options.path), uid, gid);
102
189
  }
103
190
  /**
104
191
  * Modifies the permissions on the file. See [`chmod(2)`](http://man7.org/linux/man-pages/man2/chmod.2.html).
@@ -107,7 +194,7 @@ class FileHandle {
107
194
  * @return Fulfills with `undefined` upon success.
108
195
  */
109
196
  async chmod(mode) {
110
- warnNotImplemented("fs.FileHandle.chmod");
197
+ chmodSync(normalizePath(this.options.path), mode);
111
198
  }
112
199
  /**
113
200
  * Unlike the 16 kb default `highWaterMark` for a `stream.Readable`, the stream
@@ -193,7 +280,7 @@ class FileHandle {
193
280
  * @return Fulfills with `undefined` upon success.
194
281
  */
195
282
  async datasync() {
196
- warnNotImplemented("fs.FileHandle.datasync");
283
+ this._file.flush();
197
284
  }
198
285
  /**
199
286
  * Request that all data for the open file descriptor is flushed to the storage
@@ -203,7 +290,7 @@ class FileHandle {
203
290
  * @return Fufills with `undefined` upon success.
204
291
  */
205
292
  async sync() {
206
- warnNotImplemented("fs.FileHandle.sync");
293
+ this._file.flush();
207
294
  }
208
295
  async read(...args) {
209
296
  let buffer;
@@ -225,19 +312,18 @@ class FileHandle {
225
312
  const bufView = buffer;
226
313
  const bufOffset = offset ?? 0;
227
314
  const readLength = length ?? bufView?.byteLength ?? 65536;
228
- const gFile = Gio.File.new_for_path(this.options.path.toString());
229
- const [, fileContents] = gFile.load_contents(null);
230
- const fileData = fileContents;
231
315
  const startPos = position ?? 0;
232
- const readData = fileData.slice(startPos, startPos + readLength);
233
- const bytesRead = readData.length;
234
- if (bufView && bytesRead > 0) {
235
- bufView.set(readData, bufOffset);
236
- }
237
- return {
238
- bytesRead,
239
- buffer
240
- };
316
+ return this._serialize(async () => {
317
+ const { input, seekable } = this._getReadStream();
318
+ seekable.seek(BigInt(startPos), GLib.SeekType.SET, null);
319
+ const bytes = input.read_bytes(readLength, null);
320
+ const data = bytes.get_data();
321
+ const bytesRead = data?.length ?? 0;
322
+ if (bufView && data && bytesRead > 0) {
323
+ bufView.set(data, bufOffset);
324
+ }
325
+ return { bytesRead, buffer };
326
+ });
241
327
  }
242
328
  /**
243
329
  * Returns a `ReadableStream` that may be used to read the files data.
@@ -303,11 +389,20 @@ class FileHandle {
303
389
  * @param options See `filehandle.createReadStream()` for the options.
304
390
  */
305
391
  readLines(options) {
306
- notImplemented("fs.FileHandle.readLines");
392
+ return createInterface({ input: this.createReadStream(options), crlfDelay: Infinity });
307
393
  }
308
394
  async stat(opts) {
309
- warnNotImplemented("fs.FileHandle.stat");
310
- return new Stats(this.options.path.toString());
395
+ const info = await new Promise((resolve, reject) => {
396
+ this._gFile.query_info_async(STAT_ATTRIBUTES, Gio.FileQueryInfoFlags.NONE, GLib.PRIORITY_DEFAULT, null, (_s, res) => {
397
+ try {
398
+ resolve(this._gFile.query_info_finish(res));
399
+ } catch (e) {
400
+ reject(e);
401
+ }
402
+ });
403
+ });
404
+ const pathStr = normalizePath(this.options.path);
405
+ return opts?.bigint ? new BigIntStats(info, pathStr) : new Stats(info, pathStr);
311
406
  }
312
407
  /**
313
408
  * Truncates the file.
@@ -340,18 +435,16 @@ class FileHandle {
340
435
  async truncate(len = 0) {
341
436
  const effectiveLen = Math.max(0, len);
342
437
  this._file.flush();
343
- const gFile = Gio.File.new_for_path(this.options.path.toString());
344
- const [, currentContent] = gFile.load_contents(null);
345
- const newContent = new Uint8Array(effectiveLen);
346
- newContent.set(currentContent.slice(0, Math.min(effectiveLen, currentContent.length)));
347
- gFile.replace_contents(newContent, null, false, Gio.FileCreateFlags.NONE, null);
438
+ const out = this._getWriteStream().get_output_stream();
439
+ out.truncate(effectiveLen, null);
348
440
  }
349
441
  /**
350
442
  * Change the file system timestamps of the object referenced by the `FileHandle` then resolves the promise with no arguments upon success.
351
443
  * @since v10.0.0
352
444
  */
353
445
  async utimes(atime, mtime) {
354
- warnNotImplemented("fs.FileHandle.utimes");
446
+ const { utimesSync } = await import("./utimes.js");
447
+ utimesSync(normalizePath(this.options.path), atime, mtime);
355
448
  }
356
449
  /**
357
450
  * Asynchronously writes data to a file, replacing the file if it already exists.`data` can be a string, a buffer, an
@@ -410,21 +503,33 @@ class FileHandle {
410
503
  const writeLength = length ?? writeBuf.byteLength - bufOffset;
411
504
  const writeSlice = writeBuf.slice(bufOffset, bufOffset + writeLength);
412
505
  const writePos = position ?? 0;
413
- const gFile = Gio.File.new_for_path(this.options.path.toString());
414
- let existingData;
415
- try {
416
- const [, existing] = gFile.load_contents(null);
417
- existingData = existing;
418
- } catch {
419
- existingData = new Uint8Array(0);
420
- }
421
- const newSize = Math.max(existingData.length, writePos + writeSlice.length);
422
- const newContent = new Uint8Array(newSize);
423
- newContent.set(existingData);
424
- newContent.set(writeSlice, writePos);
425
- gFile.replace_contents(newContent, null, false, Gio.FileCreateFlags.NONE, null);
506
+ const bytesWritten = await this._serialize(async () => {
507
+ const stream = this._getWriteStream();
508
+ stream.seek(BigInt(writePos), GLib.SeekType.SET, null);
509
+ const output = stream.get_output_stream();
510
+ const written = await new Promise((resolve, reject) => {
511
+ output.write_bytes_async(new GLib.Bytes(writeSlice), GLib.PRIORITY_DEFAULT, null, (_source, asyncResult) => {
512
+ try {
513
+ resolve(output.write_bytes_finish(asyncResult));
514
+ } catch (err) {
515
+ reject(err);
516
+ }
517
+ });
518
+ });
519
+ await new Promise((resolve, reject) => {
520
+ output.flush_async(GLib.PRIORITY_DEFAULT, null, (_source, asyncResult) => {
521
+ try {
522
+ output.flush_finish(asyncResult);
523
+ resolve();
524
+ } catch (err) {
525
+ reject(err);
526
+ }
527
+ });
528
+ });
529
+ return written;
530
+ });
426
531
  return {
427
- bytesWritten: writeSlice.length,
532
+ bytesWritten,
428
533
  buffer: data
429
534
  };
430
535
  }
@@ -444,11 +549,18 @@ class FileHandle {
444
549
  * position.
445
550
  */
446
551
  async writev(buffers, position) {
447
- warnNotImplemented("fs.FileHandle.writev");
448
- return {
449
- bytesWritten: 0,
450
- buffers
451
- };
552
+ let bytesWritten = 0;
553
+ for (const buf of buffers) {
554
+ const b = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
555
+ const res = await this.write(
556
+ b,
557
+ 0,
558
+ b.byteLength,
559
+ position != null ? position + bytesWritten : null
560
+ );
561
+ bytesWritten += res.bytesWritten;
562
+ }
563
+ return { bytesWritten, buffers };
452
564
  }
453
565
  /**
454
566
  * Read from a file and write to an array of [ArrayBufferView](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) s
@@ -457,11 +569,65 @@ class FileHandle {
457
569
  * @return Fulfills upon success an object containing two properties:
458
570
  */
459
571
  async readv(buffers, position) {
460
- warnNotImplemented("fs.FileHandle.readv");
461
- return {
462
- bytesRead: 0,
463
- buffers
464
- };
572
+ let bytesRead = 0;
573
+ for (const buf of buffers) {
574
+ const res = await this.read({
575
+ buffer: Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength),
576
+ position: position != null ? position + bytesRead : null
577
+ });
578
+ bytesRead += res.bytesRead;
579
+ if (res.bytesRead < buf.byteLength) break;
580
+ }
581
+ return { bytesRead, buffers };
582
+ }
583
+ /** @internal */
584
+ _flushSync() {
585
+ this._file.flush();
586
+ }
587
+ /** @internal */
588
+ _closeSync() {
589
+ try {
590
+ this._ioStream?.close(null);
591
+ } catch {
592
+ }
593
+ try {
594
+ this._readStream?.close(null);
595
+ } catch {
596
+ }
597
+ this._ioStream = null;
598
+ this._readStream = null;
599
+ try {
600
+ this._file.shutdown(true);
601
+ } catch {
602
+ }
603
+ delete FileHandle.instances[this.fd];
604
+ }
605
+ /** @internal */
606
+ _readSync(buffer, offset, length, position) {
607
+ const stream = this._gFile.read(null);
608
+ try {
609
+ if (position !== null && position >= 0) {
610
+ stream.seek(position, GLib.SeekType.SET, null);
611
+ }
612
+ const bytes = stream.read_bytes(length, null);
613
+ const arr = bytes.get_data();
614
+ new Uint8Array(buffer.buffer, buffer.byteOffset + offset).set(arr.subarray(0, arr.length));
615
+ return arr.length;
616
+ } finally {
617
+ stream.close(null);
618
+ }
619
+ }
620
+ /** @internal */
621
+ _writeSync(data, position) {
622
+ const stream = this._gFile.open_readwrite(null);
623
+ try {
624
+ if (position !== null && position >= 0) {
625
+ stream.seek(position, GLib.SeekType.SET, null);
626
+ }
627
+ return stream.get_output_stream().write_bytes(GLib.Bytes.new(data), null);
628
+ } finally {
629
+ stream.close(null);
630
+ }
465
631
  }
466
632
  /**
467
633
  * Closes the file handle after waiting for any pending operation on the handle to
@@ -481,7 +647,20 @@ class FileHandle {
481
647
  * @return Fulfills with `undefined` upon success.
482
648
  */
483
649
  async close() {
484
- this._file.shutdown(true);
650
+ try {
651
+ this._ioStream?.close(null);
652
+ } catch {
653
+ }
654
+ try {
655
+ this._readStream?.close(null);
656
+ } catch {
657
+ }
658
+ this._ioStream = null;
659
+ this._readStream = null;
660
+ try {
661
+ this._file.shutdown(true);
662
+ } catch {
663
+ }
485
664
  }
486
665
  async [Symbol.asyncDispose]() {
487
666
  await this.close();
@@ -1,6 +1,7 @@
1
1
  import GLib from "@girs/glib-2.0";
2
2
  import Gio from "@girs/gio-2.0";
3
3
  import { EventEmitter } from "node:events";
4
+ import { normalizePath } from "./utils.js";
4
5
  const privates = /* @__PURE__ */ new WeakMap();
5
6
  class FSWatcher extends EventEmitter {
6
7
  constructor(filename, options, listener) {
@@ -9,12 +10,17 @@ class FSWatcher extends EventEmitter {
9
10
  options = { persistent: true };
10
11
  const persistent = options.persistent !== false;
11
12
  const cancellable = Gio.Cancellable.new();
12
- const file = Gio.File.new_for_path(filename);
13
+ const pathStr = normalizePath(filename);
14
+ const file = Gio.File.new_for_path(pathStr);
13
15
  const watcher = file.monitor(Gio.FileMonitorFlags.NONE, cancellable);
14
16
  watcher.connect("changed", changed.bind(this));
15
17
  let sourceId = null;
16
18
  if (persistent) {
17
- sourceId = GLib.timeout_add(GLib.PRIORITY_LOW, 2147483647, () => GLib.SOURCE_CONTINUE);
19
+ sourceId = GLib.timeout_add(
20
+ GLib.PRIORITY_LOW,
21
+ 2147483647,
22
+ () => GLib.SOURCE_CONTINUE
23
+ );
18
24
  }
19
25
  privates.set(this, {
20
26
  persistent,
@@ -45,7 +51,11 @@ class FSWatcher extends EventEmitter {
45
51
  const priv = privates.get(this);
46
52
  if (!priv.persistent && !priv.cancellable.is_cancelled()) {
47
53
  priv.persistent = true;
48
- priv.sourceId = GLib.timeout_add(GLib.PRIORITY_LOW, 2147483647, () => GLib.SOURCE_CONTINUE);
54
+ priv.sourceId = GLib.timeout_add(
55
+ GLib.PRIORITY_LOW,
56
+ 2147483647,
57
+ () => GLib.SOURCE_CONTINUE
58
+ );
49
59
  }
50
60
  return this;
51
61
  }
@@ -81,7 +91,81 @@ function changed(watcher, file, otherFile, eventType) {
81
91
  }
82
92
  }
83
93
  var fs_watcher_default = FSWatcher;
94
+ function gioEventToNodeType(eventType) {
95
+ switch (eventType) {
96
+ case Gio.FileMonitorEvent.CHANGES_DONE_HINT:
97
+ return "change";
98
+ case Gio.FileMonitorEvent.DELETED:
99
+ case Gio.FileMonitorEvent.CREATED:
100
+ case Gio.FileMonitorEvent.RENAMED:
101
+ case Gio.FileMonitorEvent.MOVED_IN:
102
+ case Gio.FileMonitorEvent.MOVED_OUT:
103
+ return "rename";
104
+ default:
105
+ return null;
106
+ }
107
+ }
108
+ async function* watchAsync(filename, options) {
109
+ const signal = options?.signal;
110
+ if (signal?.aborted) return;
111
+ const pathStr = normalizePath(filename);
112
+ const file = Gio.File.new_for_path(pathStr);
113
+ const cancellable = Gio.Cancellable.new();
114
+ let watcher;
115
+ try {
116
+ watcher = file.monitor(Gio.FileMonitorFlags.NONE, cancellable);
117
+ } catch {
118
+ return;
119
+ }
120
+ const eventQueue = [];
121
+ const waiterQueue = [];
122
+ let finished = false;
123
+ function enqueue(event) {
124
+ if (finished) return;
125
+ if (waiterQueue.length > 0) {
126
+ waiterQueue.shift().resolve({ value: event, done: false });
127
+ } else {
128
+ eventQueue.push(event);
129
+ }
130
+ }
131
+ function terminate() {
132
+ if (finished) return;
133
+ finished = true;
134
+ if (!cancellable.is_cancelled()) cancellable.cancel();
135
+ while (waiterQueue.length > 0) {
136
+ waiterQueue.shift().resolve({ value: void 0, done: true });
137
+ }
138
+ }
139
+ const signalId = watcher.connect("changed", (_mon, changedFile, _otherFile, eventType) => {
140
+ const type = gioEventToNodeType(eventType);
141
+ if (type === null) return;
142
+ enqueue({ eventType: type, filename: changedFile?.get_basename() ?? null });
143
+ });
144
+ const abortHandler = () => terminate();
145
+ signal?.addEventListener("abort", abortHandler);
146
+ try {
147
+ while (!finished) {
148
+ if (eventQueue.length > 0) {
149
+ yield eventQueue.shift();
150
+ continue;
151
+ }
152
+ const result = await new Promise((resolve) => {
153
+ waiterQueue.push({ resolve });
154
+ });
155
+ if (result.done) break;
156
+ yield result.value;
157
+ }
158
+ } finally {
159
+ signal?.removeEventListener("abort", abortHandler);
160
+ try {
161
+ watcher.disconnect(signalId);
162
+ } catch {
163
+ }
164
+ if (!cancellable.is_cancelled()) cancellable.cancel();
165
+ }
166
+ }
84
167
  export {
85
168
  FSWatcher,
86
- fs_watcher_default as default
169
+ fs_watcher_default as default,
170
+ watchAsync
87
171
  };