@zenfs/core 2.5.8 → 2.6.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 (63) hide show
  1. package/dist/backends/cow.d.ts +13 -0
  2. package/dist/backends/cow.js +48 -11
  3. package/dist/backends/single_buffer.d.ts +2 -2
  4. package/dist/backends/store/fs.d.ts +6 -6
  5. package/dist/backends/store/fs.js +10 -2
  6. package/dist/internal/devices.d.ts +6 -6
  7. package/dist/internal/index_fs.d.ts +4 -4
  8. package/dist/node/async.js +1 -1
  9. package/dist/node/dir.d.ts +3 -1
  10. package/dist/node/dir.js +23 -5
  11. package/dist/node/promises.d.ts +3 -1
  12. package/dist/node/promises.js +128 -89
  13. package/dist/node/streams.d.ts +4 -0
  14. package/dist/node/streams.js +9 -10
  15. package/dist/node/sync.js +71 -40
  16. package/dist/utils.d.ts +8 -1
  17. package/dist/utils.js +26 -7
  18. package/dist/vfs/async.js +224 -85
  19. package/dist/vfs/file.d.ts +13 -7
  20. package/dist/vfs/file.js +661 -357
  21. package/dist/vfs/shared.d.ts +1 -0
  22. package/dist/vfs/shared.js +19 -3
  23. package/dist/vfs/sync.js +214 -85
  24. package/dist/vfs/vcache.d.ts +66 -0
  25. package/dist/vfs/vcache.js +249 -0
  26. package/dist/vfs/vnode.d.ts +66 -0
  27. package/dist/vfs/vnode.js +200 -0
  28. package/dist/vfs/watchers.js +4 -2
  29. package/dist/vfs/xattr.js +2 -2
  30. package/eslint.shared.js +1 -1
  31. package/package.json +3 -3
  32. package/scripts/make-index.js +7 -1
  33. package/scripts/test.js +28 -21
  34. package/tests/backend/port.test.ts +3 -3
  35. package/tests/backend/store.test.ts +49 -0
  36. package/tests/common.ts +62 -0
  37. package/tests/fetch/fetch.ts +6 -0
  38. package/tests/fetch/run.sh +2 -2
  39. package/tests/fetch/server.js +3 -3
  40. package/tests/fs/append.test.ts +5 -5
  41. package/tests/fs/concurrency.test.ts +52 -0
  42. package/tests/fs/dir.test.ts +13 -15
  43. package/tests/fs/directory.test.ts +32 -31
  44. package/tests/fs/errors.test.ts +151 -31
  45. package/tests/fs/exists.test.ts +4 -4
  46. package/tests/fs/links.test.ts +4 -11
  47. package/tests/fs/open.test.ts +4 -4
  48. package/tests/fs/permissions.test.ts +15 -15
  49. package/tests/fs/read.test.ts +9 -9
  50. package/tests/fs/readFile.test.ts +9 -9
  51. package/tests/fs/rename.test.ts +8 -8
  52. package/tests/fs/scaling.test.ts +4 -4
  53. package/tests/fs/stat.test.ts +20 -20
  54. package/tests/fs/streams.test.ts +9 -6
  55. package/tests/fs/tempdir.test.ts +33 -10
  56. package/tests/fs/times.test.ts +11 -11
  57. package/tests/fs/truncate.test.ts +6 -6
  58. package/tests/fs/watch.test.ts +107 -53
  59. package/tests/fs/write.test.ts +10 -10
  60. package/tests/fs/xattr.test.ts +3 -3
  61. package/tests/native/run.sh +3 -0
  62. package/tests/native/setup.ts +230 -0
  63. package/types/readable-stream.d.ts +2 -2
@@ -55,14 +55,15 @@ import { Exception, rethrow, UV } from 'kerium';
55
55
  import { encodeUTF8 } from 'utilium';
56
56
  import * as constants from '../constants.js';
57
57
  import { hasAccess, InodeFlags, isDirectory } from '../internal/inode.js';
58
- import { join, matchesGlob } from '../path.js';
58
+ import { dirname, join, matchesGlob } from '../path.js';
59
59
  import '../polyfills.js';
60
- import { _tempDirName, globToRegex, normalizeMode, normalizeOptions, normalizePath, normalizeTime } from '../utils.js';
60
+ import { _tempDirName, globToRegex, normalizeMode, normalizeOptions, normalizePath, normalizeTime, validateFD } from '../utils.js';
61
61
  import * as _async from '../vfs/async.js';
62
62
  import { checkAccess } from '../vfs/config.js';
63
63
  import { deleteFD, fromFD, toFD } from '../vfs/file.js';
64
64
  import * as flags from '../vfs/flags.js';
65
65
  import { _statfs, resolveMount } from '../vfs/shared.js';
66
+ import { cacheOf, lockPath } from '../vfs/vcache.js';
66
67
  import { emitChange, FSWatcher } from '../vfs/watchers.js';
67
68
  import { Dir, Dirent } from './dir.js';
68
69
  import { createInterface } from './readline.js';
@@ -133,11 +134,15 @@ async function* applyTransform(source, transform, signal) {
133
134
  }
134
135
  export class FileHandle {
135
136
  context;
136
- fd;
137
137
  vfs;
138
+ _fd;
139
+ /** The file descriptor, or -1 once the handle has been closed */
140
+ get fd() {
141
+ return this._fd;
142
+ }
138
143
  constructor(context, fd) {
139
144
  this.context = context;
140
- this.fd = fd;
145
+ this._fd = fd;
141
146
  this.vfs = fromFD(context, fd);
142
147
  }
143
148
  _emitChange() {
@@ -250,7 +255,7 @@ export class FileHandle {
250
255
  readableWebStream(options = {}) {
251
256
  if (this.vfs.isClosed)
252
257
  throw UV('EBADF', 'readableWebStream', this.vfs.path);
253
- return this.vfs.fs.streamRead(this.vfs.internalPath, options);
258
+ return this.vfs.streamRead(options);
254
259
  }
255
260
  /**
256
261
  * Not part of the Node.js API!
@@ -264,7 +269,7 @@ export class FileHandle {
264
269
  throw UV('EBADF', 'writableWebStream', this.vfs.path);
265
270
  if (this.vfs.inode.flags & InodeFlags.Immutable)
266
271
  throw UV('EPERM', 'writableWebStream', this.vfs.path);
267
- return this.vfs.fs.streamWrite(this.vfs.internalPath, options);
272
+ return this.vfs.streamWrite(options);
268
273
  }
269
274
  /**
270
275
  * Creates a readline Interface object that allows reading the file line by line
@@ -340,7 +345,9 @@ export class FileHandle {
340
345
  */
341
346
  async close() {
342
347
  await this.vfs.close();
343
- deleteFD(this.context, this.fd);
348
+ deleteFD(this.context, this._fd);
349
+ // Like Node, the descriptor is invalidated so later use fails validation
350
+ this._fd = -1;
344
351
  }
345
352
  /**
346
353
  * Asynchronous `writev`. Writes from multiple buffers.
@@ -377,6 +384,7 @@ export class FileHandle {
377
384
  * @param options Options for the readable stream
378
385
  */
379
386
  createReadStream(options = {}) {
387
+ validateFD(this._fd);
380
388
  if (this.vfs.isClosed || this.vfs.flag & constants.O_WRONLY)
381
389
  throw UV('EBADF', 'createReadStream', this.vfs.path);
382
390
  return new ReadStream(options, this);
@@ -386,6 +394,7 @@ export class FileHandle {
386
394
  * @param options Options for the writeable stream.
387
395
  */
388
396
  createWriteStream(options = {}) {
397
+ validateFD(this._fd);
389
398
  if (this.vfs.isClosed)
390
399
  throw UV('EBADF', 'createWriteStream', this.vfs.path);
391
400
  if (this.vfs.inode.flags & InodeFlags.Immutable)
@@ -559,14 +568,26 @@ export async function truncate(path, len = 0) {
559
568
  }
560
569
  truncate;
561
570
  export async function unlink(path) {
562
- path = normalizePath(path);
563
- const { fs, path: resolved } = resolveMount(path, this);
564
- const $ex = { syscall: 'unlink', path };
565
- const stats = await fs.stat(resolved).catch(rethrow($ex));
566
- if (checkAccess && !hasAccess(this, stats, constants.W_OK))
567
- throw UV('EACCES', $ex);
568
- await fs.unlink(resolved).catch(rethrow($ex));
569
- emitChange(this, 'rename', path.toString());
571
+ const env_2 = { stack: [], error: void 0, hasError: false };
572
+ try {
573
+ path = normalizePath(path);
574
+ const { fs, path: resolved } = resolveMount(path, this);
575
+ const $ex = { syscall: 'unlink', path };
576
+ const stats = await fs.stat(resolved).catch(rethrow($ex));
577
+ if (checkAccess && !hasAccess(this, stats, constants.W_OK))
578
+ throw UV('EACCES', $ex);
579
+ const _ = __addDisposableResource(env_2, await lockPath(fs, dirname(resolved), 'rw'), false);
580
+ await fs.unlink(resolved).catch(rethrow($ex));
581
+ cacheOf(fs).remove(resolved);
582
+ emitChange(this, 'rename', path.toString());
583
+ }
584
+ catch (e_2) {
585
+ env_2.error = e_2;
586
+ env_2.hasError = true;
587
+ }
588
+ finally {
589
+ __disposeResources(env_2);
590
+ }
570
591
  }
571
592
  unlink;
572
593
  /**
@@ -581,18 +602,18 @@ export async function open(path, flag = 'r', mode = 0o644) {
581
602
  }
582
603
  open;
583
604
  export async function readFile(path, _options) {
584
- const env_2 = { stack: [], error: void 0, hasError: false };
605
+ const env_3 = { stack: [], error: void 0, hasError: false };
585
606
  try {
586
607
  const options = normalizeOptions(_options, null, 'r', 0o444);
587
- const handle = __addDisposableResource(env_2, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true);
608
+ const handle = __addDisposableResource(env_3, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true);
588
609
  return await handle.readFile(options);
589
610
  }
590
- catch (e_2) {
591
- env_2.error = e_2;
592
- env_2.hasError = true;
611
+ catch (e_3) {
612
+ env_3.error = e_3;
613
+ env_3.hasError = true;
593
614
  }
594
615
  finally {
595
- const result_2 = __disposeResources(env_2);
616
+ const result_2 = __disposeResources(env_3);
596
617
  if (result_2)
597
618
  await result_2;
598
619
  }
@@ -607,21 +628,21 @@ readFile;
607
628
  * @option flag Defaults to `'w'`.
608
629
  */
609
630
  export async function writeFile(path, data, _options) {
610
- const env_3 = { stack: [], error: void 0, hasError: false };
631
+ const env_4 = { stack: [], error: void 0, hasError: false };
611
632
  try {
612
633
  const options = normalizeOptions(_options, 'utf8', 'w+', 0o644);
613
- const handle = __addDisposableResource(env_3, path instanceof FileHandle ? path : await open.call(this, path.toString(), options.flag, options.mode), true);
634
+ const handle = __addDisposableResource(env_4, path instanceof FileHandle ? path : await open.call(this, path.toString(), options.flag, options.mode), true);
614
635
  const _data = typeof data == 'string' ? data : data instanceof DataView ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : data;
615
636
  if (typeof _data != 'string' && !(_data instanceof Uint8Array))
616
637
  throw new TypeError('The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ' + typeof data);
617
638
  await handle.writeFile(_data, options);
618
639
  }
619
- catch (e_3) {
620
- env_3.error = e_3;
621
- env_3.hasError = true;
640
+ catch (e_4) {
641
+ env_4.error = e_4;
642
+ env_4.hasError = true;
622
643
  }
623
644
  finally {
624
- const result_3 = __disposeResources(env_3);
645
+ const result_3 = __disposeResources(env_4);
625
646
  if (result_3)
626
647
  await result_3;
627
648
  }
@@ -634,7 +655,7 @@ writeFile;
634
655
  * @option flag Defaults to `'a'`.
635
656
  */
636
657
  export async function appendFile(path, data, _options) {
637
- const env_4 = { stack: [], error: void 0, hasError: false };
658
+ const env_5 = { stack: [], error: void 0, hasError: false };
638
659
  try {
639
660
  const options = normalizeOptions(_options, 'utf8', 'a', 0o644);
640
661
  const flag = flags.parse(options.flag);
@@ -644,33 +665,45 @@ export async function appendFile(path, data, _options) {
644
665
  if (typeof data != 'string' && !ArrayBuffer.isView(data))
645
666
  throw new TypeError('The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received ' + typeof data);
646
667
  const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
647
- const handle = __addDisposableResource(env_4, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true);
668
+ const handle = __addDisposableResource(env_5, typeof path == 'object' && 'fd' in path ? path : await open.call(this, path, options.flag, options.mode), true);
648
669
  await handle.appendFile(encodedData, options);
649
670
  }
650
- catch (e_4) {
651
- env_4.error = e_4;
652
- env_4.hasError = true;
671
+ catch (e_5) {
672
+ env_5.error = e_5;
673
+ env_5.hasError = true;
653
674
  }
654
675
  finally {
655
- const result_4 = __disposeResources(env_4);
676
+ const result_4 = __disposeResources(env_5);
656
677
  if (result_4)
657
678
  await result_4;
658
679
  }
659
680
  }
660
681
  appendFile;
661
682
  export async function rmdir(path) {
662
- path = normalizePath(path);
663
- const { fs, path: resolved } = await _async.resolve(this, path);
664
- const $ex = { syscall: 'rmdir', path };
665
- const stats = await fs.stat(resolved).catch(rethrow($ex));
666
- if (!stats)
667
- throw UV('ENOENT', $ex);
668
- if (!isDirectory(stats))
669
- throw UV('ENOTDIR', $ex);
670
- if (checkAccess && !hasAccess(this, stats, constants.W_OK))
671
- throw UV('EACCES', $ex);
672
- await fs.rmdir(resolved).catch(rethrow($ex));
673
- emitChange(this, 'rename', path.toString());
683
+ const env_6 = { stack: [], error: void 0, hasError: false };
684
+ try {
685
+ path = normalizePath(path);
686
+ const { fs, path: resolved } = await _async.resolve(this, path);
687
+ const $ex = { syscall: 'rmdir', path };
688
+ const stats = await fs.stat(resolved).catch(rethrow($ex));
689
+ if (!stats)
690
+ throw UV('ENOENT', $ex);
691
+ if (!isDirectory(stats))
692
+ throw UV('ENOTDIR', $ex);
693
+ if (checkAccess && !hasAccess(this, stats, constants.W_OK))
694
+ throw UV('EACCES', $ex);
695
+ const _ = __addDisposableResource(env_6, await lockPath(fs, dirname(resolved), 'rw'), false);
696
+ await fs.rmdir(resolved).catch(rethrow($ex));
697
+ cacheOf(fs).remove(resolved);
698
+ emitChange(this, 'rename', path.toString());
699
+ }
700
+ catch (e_6) {
701
+ env_6.error = e_6;
702
+ env_6.hasError = true;
703
+ }
704
+ finally {
705
+ __disposeResources(env_6);
706
+ }
674
707
  }
675
708
  rmdir;
676
709
  export async function mkdir(path, options) {
@@ -686,7 +719,7 @@ export async function readdir(path, options) {
686
719
  const values = [];
687
720
  for (const entry of rawEntries) {
688
721
  if (opt.withFileTypes) {
689
- values.push(Dirent.from(entry, opt.encoding));
722
+ values.push(Dirent.from(entry, opt.encoding, path));
690
723
  }
691
724
  else if (opt.encoding == 'buffer') {
692
725
  values.push(Buffer.from(entry.path));
@@ -709,24 +742,24 @@ link;
709
742
  * @param type can be either `'dir'` or `'file'` (default is `'file'`)
710
743
  */
711
744
  export async function symlink(dest, path, type = 'file') {
712
- const env_5 = { stack: [], error: void 0, hasError: false };
745
+ const env_7 = { stack: [], error: void 0, hasError: false };
713
746
  try {
714
747
  if (!['file', 'dir', 'junction'].includes(type))
715
748
  throw new TypeError('Invalid symlink type: ' + type);
716
749
  path = normalizePath(path);
717
750
  if (await exists.call(this, path))
718
751
  throw UV('EEXIST', 'symlink', path);
719
- const handle = __addDisposableResource(env_5, await _async.open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true);
752
+ const handle = __addDisposableResource(env_7, await _async.open(this, path, { flag: 'w+', mode: 0o644, preserveSymlinks: true }), true);
720
753
  const encoded = encodeUTF8(normalizePath(dest, true));
721
754
  await handle.write(encoded, 0, encoded.length, 0);
722
755
  await handle.chmod(constants.S_IFLNK);
723
756
  }
724
- catch (e_5) {
725
- env_5.error = e_5;
726
- env_5.hasError = true;
757
+ catch (e_7) {
758
+ env_7.error = e_7;
759
+ env_7.hasError = true;
727
760
  }
728
761
  finally {
729
- const result_5 = __disposeResources(env_5);
762
+ const result_5 = __disposeResources(env_7);
730
763
  if (result_5)
731
764
  await result_5;
732
765
  }
@@ -741,26 +774,26 @@ export async function readlink(path, options) {
741
774
  }
742
775
  readlink;
743
776
  export async function chown(path, uid, gid) {
744
- const env_6 = { stack: [], error: void 0, hasError: false };
777
+ const env_8 = { stack: [], error: void 0, hasError: false };
745
778
  try {
746
- const handle = __addDisposableResource(env_6, await open.call(this, path, 'r+'), true);
779
+ const handle = __addDisposableResource(env_8, await open.call(this, path, 'r+'), true);
747
780
  await handle.chown(uid, gid);
748
781
  }
749
- catch (e_6) {
750
- env_6.error = e_6;
751
- env_6.hasError = true;
782
+ catch (e_8) {
783
+ env_8.error = e_8;
784
+ env_8.hasError = true;
752
785
  }
753
786
  finally {
754
- const result_6 = __disposeResources(env_6);
787
+ const result_6 = __disposeResources(env_8);
755
788
  if (result_6)
756
789
  await result_6;
757
790
  }
758
791
  }
759
792
  chown;
760
793
  export async function lchown(path, uid, gid) {
761
- const env_7 = { stack: [], error: void 0, hasError: false };
794
+ const env_9 = { stack: [], error: void 0, hasError: false };
762
795
  try {
763
- const handle = __addDisposableResource(env_7, await _async.open(this, path, {
796
+ const handle = __addDisposableResource(env_9, await _async.open(this, path, {
764
797
  flag: 'r+',
765
798
  mode: 0o644,
766
799
  preserveSymlinks: true,
@@ -768,39 +801,39 @@ export async function lchown(path, uid, gid) {
768
801
  }), true);
769
802
  await handle.chown(uid, gid);
770
803
  }
771
- catch (e_7) {
772
- env_7.error = e_7;
773
- env_7.hasError = true;
804
+ catch (e_9) {
805
+ env_9.error = e_9;
806
+ env_9.hasError = true;
774
807
  }
775
808
  finally {
776
- const result_7 = __disposeResources(env_7);
809
+ const result_7 = __disposeResources(env_9);
777
810
  if (result_7)
778
811
  await result_7;
779
812
  }
780
813
  }
781
814
  lchown;
782
815
  export async function chmod(path, mode) {
783
- const env_8 = { stack: [], error: void 0, hasError: false };
816
+ const env_10 = { stack: [], error: void 0, hasError: false };
784
817
  try {
785
- const handle = __addDisposableResource(env_8, await open.call(this, path, 'r+'), true);
818
+ const handle = __addDisposableResource(env_10, await open.call(this, path, 'r+').catch(rethrow({ syscall: 'chmod', path: normalizePath(path) })), true);
786
819
  await handle.chmod(mode);
787
820
  }
788
- catch (e_8) {
789
- env_8.error = e_8;
790
- env_8.hasError = true;
821
+ catch (e_10) {
822
+ env_10.error = e_10;
823
+ env_10.hasError = true;
791
824
  }
792
825
  finally {
793
- const result_8 = __disposeResources(env_8);
826
+ const result_8 = __disposeResources(env_10);
794
827
  if (result_8)
795
828
  await result_8;
796
829
  }
797
830
  }
798
831
  chmod;
799
832
  export async function lchmod(path, mode) {
800
- const env_9 = { stack: [], error: void 0, hasError: false };
833
+ const env_11 = { stack: [], error: void 0, hasError: false };
801
834
  try {
802
835
  mode = normalizeMode(mode);
803
- const handle = __addDisposableResource(env_9, await _async.open(this, path, {
836
+ const handle = __addDisposableResource(env_11, await _async.open(this, path, {
804
837
  flag: 'r+',
805
838
  mode: 0o644,
806
839
  preserveSymlinks: true,
@@ -808,12 +841,12 @@ export async function lchmod(path, mode) {
808
841
  }), true);
809
842
  await handle.chmod(mode);
810
843
  }
811
- catch (e_9) {
812
- env_9.error = e_9;
813
- env_9.hasError = true;
844
+ catch (e_11) {
845
+ env_11.error = e_11;
846
+ env_11.hasError = true;
814
847
  }
815
848
  finally {
816
- const result_9 = __disposeResources(env_9);
849
+ const result_9 = __disposeResources(env_11);
817
850
  if (result_9)
818
851
  await result_9;
819
852
  }
@@ -823,20 +856,20 @@ lchmod;
823
856
  * Change file timestamps of the file referenced by the supplied path.
824
857
  */
825
858
  export async function utimes(path, atime, mtime) {
826
- const env_10 = { stack: [], error: void 0, hasError: false };
859
+ const env_12 = { stack: [], error: void 0, hasError: false };
827
860
  try {
828
- const handle = __addDisposableResource(env_10, await _async.open(this, path, {
861
+ const handle = __addDisposableResource(env_12, await _async.open(this, path, {
829
862
  flag: 'r+',
830
863
  allowDirectory: true,
831
864
  }), true);
832
865
  await handle.utimes(normalizeTime(atime), normalizeTime(mtime));
833
866
  }
834
- catch (e_10) {
835
- env_10.error = e_10;
836
- env_10.hasError = true;
867
+ catch (e_12) {
868
+ env_12.error = e_12;
869
+ env_12.hasError = true;
837
870
  }
838
871
  finally {
839
- const result_10 = __disposeResources(env_10);
872
+ const result_10 = __disposeResources(env_12);
840
873
  if (result_10)
841
874
  await result_10;
842
875
  }
@@ -846,9 +879,9 @@ utimes;
846
879
  * Change file timestamps of the file referenced by the supplied path.
847
880
  */
848
881
  export async function lutimes(path, atime, mtime) {
849
- const env_11 = { stack: [], error: void 0, hasError: false };
882
+ const env_13 = { stack: [], error: void 0, hasError: false };
850
883
  try {
851
- const handle = __addDisposableResource(env_11, await _async.open(this, path, {
884
+ const handle = __addDisposableResource(env_13, await _async.open(this, path, {
852
885
  flag: 'r+',
853
886
  mode: 0o644,
854
887
  preserveSymlinks: true,
@@ -856,12 +889,12 @@ export async function lutimes(path, atime, mtime) {
856
889
  }), true);
857
890
  await handle.utimes(normalizeTime(atime), normalizeTime(mtime));
858
891
  }
859
- catch (e_11) {
860
- env_11.error = e_11;
861
- env_11.hasError = true;
892
+ catch (e_13) {
893
+ env_13.error = e_13;
894
+ env_13.hasError = true;
862
895
  }
863
896
  finally {
864
- const result_11 = __disposeResources(env_11);
897
+ const result_11 = __disposeResources(env_13);
865
898
  if (result_11)
866
899
  await result_11;
867
900
  }
@@ -880,7 +913,8 @@ export async function realpath(path, options) {
880
913
  }
881
914
  realpath;
882
915
  export function watch(filename, options = {}) {
883
- const watcher = new FSWatcher(this, filename.toString(), typeof options !== 'string' ? options : { encoding: options });
916
+ const opts = typeof options !== 'string' ? options : { encoding: options };
917
+ const watcher = new FSWatcher(this, filename.toString(), opts);
884
918
  // A queue to hold change events, since we need to resolve them in the async iterator
885
919
  const eventQueue = [];
886
920
  let done = false;
@@ -890,12 +924,17 @@ export function watch(filename, options = {}) {
890
924
  function cleanup() {
891
925
  done = true;
892
926
  watcher.close();
927
+ opts.signal?.removeEventListener('abort', cleanup);
893
928
  for (const resolve of eventQueue) {
894
929
  resolve({ value: null, done });
895
930
  }
896
931
  eventQueue.length = 0; // Clear the queue
897
932
  return Promise.resolve({ value: undefined, done: true });
898
933
  }
934
+ if (opts.signal?.aborted)
935
+ void cleanup();
936
+ else
937
+ opts.signal?.addEventListener('abort', cleanup, { once: true });
899
938
  return {
900
939
  async next() {
901
940
  if (done)
@@ -51,6 +51,10 @@ export declare class ReadStream extends Readable implements fs.ReadStream {
51
51
  private ready;
52
52
  constructor(opts: CreateReadStreamOptions | undefined, handleOrPromise: FileHandle | Promise<FileHandle>);
53
53
  _read(): Promise<void>;
54
+ /**
55
+ * Like Node, the callback is invoked once the stream is closed.
56
+ * Closing before the stream has finished reports `ERR_STREAM_PREMATURE_CLOSE`.
57
+ */
54
58
  close(callback?: Callback<[void]>): void;
55
59
  get path(): string;
56
60
  get bytesRead(): number;
@@ -1,6 +1,6 @@
1
1
  import { Errno, Exception, UV } from 'kerium';
2
2
  import { warn } from 'kerium/log';
3
- import { Readable, Writable } from 'readable-stream';
3
+ import { finished, Readable, Writable } from 'readable-stream';
4
4
  /**
5
5
  * A ReadStream implementation that wraps an underlying global ReadableStream.
6
6
  */
@@ -15,7 +15,8 @@ export class ReadStream extends Readable {
15
15
  this.ready = Promise.resolve(handleOrPromise)
16
16
  .then(handle => {
17
17
  this._path = handle['vfs'].path;
18
- const internal = handle.readableWebStream({ start: opts.start, end: opts.end });
18
+ // `end` is inclusive for streams, but the internal range is not
19
+ const internal = handle.readableWebStream({ start: opts.start, end: opts.end === undefined ? undefined : opts.end + 1 });
19
20
  this.reader = internal.getReader();
20
21
  this.pending = false;
21
22
  })
@@ -40,15 +41,13 @@ export class ReadStream extends Readable {
40
41
  this.destroy(new Exception(Errno.EIO, err.toString()));
41
42
  }
42
43
  }
44
+ /**
45
+ * Like Node, the callback is invoked once the stream is closed.
46
+ * Closing before the stream has finished reports `ERR_STREAM_PREMATURE_CLOSE`.
47
+ */
43
48
  close(callback = () => null) {
44
- try {
45
- this.destroy();
46
- this.emit('close');
47
- callback(null);
48
- }
49
- catch (err) {
50
- callback(new Exception(Errno.EIO, err.toString()));
51
- }
49
+ finished(this, error => callback(error ?? null));
50
+ this.destroy();
52
51
  }
53
52
  get path() {
54
53
  return this._path;