@gjsify/fs 0.0.4 → 0.1.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.
Files changed (91) hide show
  1. package/README.md +31 -2
  2. package/lib/esm/callback.js +251 -15
  3. package/lib/esm/dirent.js +47 -6
  4. package/lib/esm/encoding.js +2 -3
  5. package/lib/esm/errors.js +13 -0
  6. package/lib/esm/file-handle.js +108 -66
  7. package/lib/esm/fs-watcher.js +44 -7
  8. package/lib/esm/index.js +140 -5
  9. package/lib/esm/promises.js +290 -69
  10. package/lib/esm/read-stream.js +82 -57
  11. package/lib/esm/stats.js +138 -18
  12. package/lib/esm/sync.js +293 -44
  13. package/lib/esm/write-stream.js +4 -4
  14. package/lib/types/callback.d.ts +233 -0
  15. package/lib/types/dirent.d.ts +77 -0
  16. package/lib/types/encoding.d.ts +6 -0
  17. package/lib/types/errors.d.ts +7 -0
  18. package/lib/types/file-handle.d.ts +367 -0
  19. package/lib/types/fs-watcher.d.ts +17 -0
  20. package/lib/types/index.d.ts +149 -0
  21. package/lib/types/promises.d.ts +158 -0
  22. package/lib/types/read-stream.d.ts +21 -0
  23. package/lib/types/stats.d.ts +67 -0
  24. package/lib/types/sync.d.ts +109 -0
  25. package/lib/types/types/encoding-option.d.ts +2 -0
  26. package/lib/types/types/file-read-options.d.ts +15 -0
  27. package/lib/types/types/file-read-result.d.ts +4 -0
  28. package/lib/types/types/flag-and-open-mode.d.ts +5 -0
  29. package/lib/types/types/index.d.ts +6 -0
  30. package/lib/types/types/open-flags.d.ts +1 -0
  31. package/lib/types/types/read-options.d.ts +5 -0
  32. package/lib/types/utils.d.ts +2 -0
  33. package/lib/types/write-stream.d.ts +45 -0
  34. package/package.json +22 -35
  35. package/src/callback.spec.ts +284 -30
  36. package/src/callback.ts +352 -39
  37. package/src/dirent.ts +56 -8
  38. package/src/encoding.ts +7 -2
  39. package/src/errors.spec.ts +389 -0
  40. package/src/errors.ts +19 -0
  41. package/src/extended.spec.ts +706 -0
  42. package/src/file-handle.spec.ts +104 -23
  43. package/src/file-handle.ts +147 -79
  44. package/src/fs-watcher.ts +55 -8
  45. package/src/index.ts +146 -2
  46. package/src/new-apis.spec.ts +505 -0
  47. package/src/promises.spec.ts +651 -11
  48. package/src/promises.ts +353 -81
  49. package/src/read-stream.ts +98 -74
  50. package/src/stat.spec.ts +22 -14
  51. package/src/stats.ts +176 -75
  52. package/src/streams.spec.ts +455 -0
  53. package/src/symlink.spec.ts +176 -26
  54. package/src/sync.spec.ts +204 -32
  55. package/src/sync.ts +363 -58
  56. package/src/test.mts +7 -2
  57. package/src/types/encoding-option.ts +1 -1
  58. package/src/types/flag-and-open-mode.ts +1 -1
  59. package/src/types/read-options.ts +2 -2
  60. package/src/utils.ts +2 -0
  61. package/src/write-stream.ts +9 -7
  62. package/tsconfig.json +23 -10
  63. package/tsconfig.tsbuildinfo +1 -0
  64. package/lib/cjs/callback.js +0 -112
  65. package/lib/cjs/dirent.js +0 -98
  66. package/lib/cjs/encoding.js +0 -34
  67. package/lib/cjs/file-handle.js +0 -444
  68. package/lib/cjs/fs-watcher.js +0 -50
  69. package/lib/cjs/index.js +0 -95
  70. package/lib/cjs/promises.js +0 -160
  71. package/lib/cjs/read-stream.js +0 -78
  72. package/lib/cjs/stats.js +0 -45
  73. package/lib/cjs/sync.js +0 -126
  74. package/lib/cjs/types/encoding-option.js +0 -0
  75. package/lib/cjs/types/file-read-options.js +0 -0
  76. package/lib/cjs/types/file-read-result.js +0 -0
  77. package/lib/cjs/types/flag-and-open-mode.js +0 -0
  78. package/lib/cjs/types/index.js +0 -6
  79. package/lib/cjs/types/open-flags.js +0 -0
  80. package/lib/cjs/types/read-options.js +0 -0
  81. package/lib/cjs/utils.js +0 -18
  82. package/lib/cjs/write-stream.js +0 -116
  83. package/test/watch.js +0 -1
  84. package/test.gjs.js +0 -35359
  85. package/test.gjs.js.map +0 -7
  86. package/test.gjs.mjs +0 -40534
  87. package/test.gjs.mjs.meta.json +0 -1
  88. package/test.node.js +0 -1479
  89. package/test.node.js.map +0 -7
  90. package/test.node.mjs +0 -710
  91. package/tsconfig.types.json +0 -8
@@ -1,34 +1,115 @@
1
+ // Ported from refs/node-test/parallel/test-fs-promises-file-handle-*.js
2
+ // Original: MIT license, Node.js contributors
1
3
  import { describe, it, expect } from '@gjsify/unit';
2
- import { promises } from 'fs';
3
- import { Buffer } from 'buffer';
4
+ import { promises, mkdtempSync, rmdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+ import { tmpdir } from 'node:os';
7
+ import { Buffer } from 'node:buffer';
4
8
 
5
9
  export default async () => {
6
- await describe('FileHandle', async () => {
7
- await it(`should open a file for writing`, async () => {
8
- const path = './test/openP.txt';
9
- const fileHandle = await promises.open(path, 'w+', 0o666);
10
+ await describe('FileHandle', async () => {
11
+ await it('should open a file for writing and return correct bytesWritten', async () => {
12
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-'));
13
+ const path = join(dir, 'write.txt');
14
+ const fh = await promises.open(path, 'w+', 0o666);
15
+ const buf = Buffer.from('Hello World', 'utf8');
16
+ const res = await fh.write(buf, 0, buf.length, 0);
17
+ expect(res.bytesWritten).toBe(buf.length);
18
+ expect(res.buffer).toBe(buf);
19
+ await fh.close();
20
+ await promises.rm(path);
21
+ rmdirSync(dir);
22
+ });
10
23
 
11
- console.log('FileHandle: file open');
24
+ await it('should read back written content via read()', async () => {
25
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-read-'));
26
+ const path = join(dir, 'rw.txt');
27
+ const fh = await promises.open(path, 'w+');
28
+ const written = Buffer.from('Read back test');
29
+ await fh.write(written, 0, written.length, 0);
12
30
 
13
- let buffWrite = Buffer.from('Hello World', 'utf8'),
14
- buffStart = 0,
15
- buffLength = buffWrite.length,
16
- filePos = 0;
17
- const res = await fileHandle.write(buffWrite, buffStart, buffLength, filePos);
18
- console.log('FileHandle: file written');
31
+ const readBuf = Buffer.alloc(written.length);
32
+ const { bytesRead } = await fh.read(readBuf, 0, written.length, 0);
33
+ expect(bytesRead).toBe(written.length);
34
+ expect(readBuf.toString()).toBe('Read back test');
19
35
 
20
- // console.log('written', res.bytesWritten);
36
+ await fh.close();
37
+ await promises.rm(path);
38
+ rmdirSync(dir);
39
+ });
21
40
 
22
- expect(res.bytesWritten).toBe(buffWrite.length);
41
+ await it('should read entire file via readFile()', async () => {
42
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-rf-'));
43
+ const path = join(dir, 'rf.txt');
44
+ await promises.writeFile(path, 'FileHandle readFile content');
45
+ const fh = await promises.open(path, 'r');
46
+ const content = await fh.readFile({ encoding: 'utf8' });
47
+ expect(content).toBe('FileHandle readFile content');
48
+ await fh.close();
49
+ await promises.rm(path);
50
+ rmdirSync(dir);
51
+ });
23
52
 
24
- expect(res.buffer).toBe(buffWrite);
53
+ await it('should stat the opened file', async () => {
54
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-stat-'));
55
+ const path = join(dir, 'stat.txt');
56
+ await promises.writeFile(path, 'stat content');
57
+ const fh = await promises.open(path, 'r');
58
+ const s = await fh.stat();
59
+ expect(s.isFile()).toBe(true);
60
+ expect(s.size).toBeGreaterThan(0);
61
+ await fh.close();
62
+ await promises.rm(path);
63
+ rmdirSync(dir);
64
+ });
25
65
 
26
- await fileHandle.close();
27
- console.log('FileHandle: file closed');
66
+ await it('should truncate the file via truncate()', async () => {
67
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-trunc-'));
68
+ const path = join(dir, 'trunc.txt');
69
+ await promises.writeFile(path, 'Hello World Truncate');
70
+ const fh = await promises.open(path, 'r+');
71
+ await fh.truncate(5);
72
+ await fh.close();
73
+ const content = await promises.readFile(path, { encoding: 'utf8' });
74
+ expect(content).toBe('Hello');
75
+ await promises.rm(path);
76
+ rmdirSync(dir);
77
+ });
28
78
 
29
- await promises.rm(path);
30
- console.log('FileHandle: file removed');
79
+ await it('should append content via appendFile()', async () => {
80
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-app-'));
81
+ const path = join(dir, 'append.txt');
82
+ await promises.writeFile(path, 'start');
83
+ const fh = await promises.open(path, 'a');
84
+ await fh.appendFile(' appended');
85
+ await fh.close();
86
+ const content = await promises.readFile(path, { encoding: 'utf8' });
87
+ expect(content).toBe('start appended');
88
+ await promises.rm(path);
89
+ rmdirSync(dir);
90
+ });
31
91
 
32
- });
33
- });
34
- }
92
+ await it('should throw when opening a non-existent file for reading', async () => {
93
+ let threw = false;
94
+ try {
95
+ await promises.open('/nonexistent/path/abc123.txt', 'r');
96
+ } catch (e: unknown) {
97
+ threw = true;
98
+ expect((e as NodeJS.ErrnoException).code).toBe('ENOENT');
99
+ }
100
+ expect(threw).toBe(true);
101
+ });
102
+
103
+ await it('should writeFile via FileHandle', async () => {
104
+ const dir = mkdtempSync(join(tmpdir(), 'fs-fh-wf-'));
105
+ const path = join(dir, 'wf.txt');
106
+ const fh = await promises.open(path, 'w');
107
+ await fh.writeFile('written via FileHandle.writeFile');
108
+ await fh.close();
109
+ const content = await promises.readFile(path, { encoding: 'utf8' });
110
+ expect(content).toBe('written via FileHandle.writeFile');
111
+ await promises.rm(path);
112
+ rmdirSync(dir);
113
+ });
114
+ });
115
+ };
@@ -1,20 +1,24 @@
1
+ // Reference: Node.js lib/internal/fs/promises.js (FileHandle)
2
+ // Reimplemented for GJS using Gio.File
3
+
1
4
  import { warnNotImplemented, notImplemented } from '@gjsify/utils';
2
5
  import { ReadStream } from "./read-stream.js";
3
6
  import { WriteStream } from "./write-stream.js";
4
7
  import { Stats } from "./stats.js";
5
8
  import { getEncodingFromOptions, encodeUint8Array } from './encoding.js';
6
9
  import GLib from '@girs/glib-2.0';
7
- import { ReadableStream } from "stream/web";
8
- import { Buffer } from "buffer";
10
+ import Gio from '@girs/gio-2.0';
11
+ import { ReadableStream } from "node:stream/web";
12
+ import { Buffer } from "node:buffer";
9
13
 
10
- import type { Abortable } from 'events';
14
+ import type { Abortable } from 'node:events';
11
15
  import type {
12
16
  FlagAndOpenMode,
13
17
  FileReadResult,
14
18
  FileReadOptions,
15
19
  OpenFlags,
16
20
  } from './types/index.js';
17
- import type { FileHandle as IFileHandle, CreateReadStreamOptions, CreateWriteStreamOptions } from 'fs/promises';
21
+ import type { FileHandle as IFileHandle, CreateReadStreamOptions, CreateWriteStreamOptions } from 'node:fs/promises';
18
22
  import type {
19
23
  ObjectEncodingOptions,
20
24
  Mode,
@@ -25,9 +29,49 @@ import type {
25
29
  WriteVResult,
26
30
  ReadVResult,
27
31
  ReadPosition,
28
- } from 'fs';
32
+ } from 'node:fs';
29
33
  import type { Interface as ReadlineInterface } from 'node:readline';
30
34
 
35
+ /**
36
+ * GLib.FileError enum numeric values → Node.js error code strings.
37
+ * GLib.IOChannel.new_file() throws GLib.FileError (different from Gio.IOErrorEnum).
38
+ */
39
+ const GLIB_FILE_ERROR_TO_NODE: Record<number, string> = {
40
+ 0: 'EEXIST',
41
+ 1: 'EISDIR',
42
+ 2: 'EACCES',
43
+ 3: 'ENAMETOOLONG',
44
+ 4: 'ENOENT',
45
+ 5: 'ENOTDIR',
46
+ 6: 'ENXIO',
47
+ 7: 'ENODEV',
48
+ 8: 'EROFS',
49
+ 11: 'ELOOP',
50
+ 12: 'ENOSPC',
51
+ 13: 'ENOMEM',
52
+ 14: 'EMFILE',
53
+ 15: 'ENFILE',
54
+ 16: 'EBADF',
55
+ 17: 'EINVAL',
56
+ 18: 'EPIPE',
57
+ 21: 'EIO',
58
+ 22: 'EPERM',
59
+ 24: 'EIO',
60
+ };
61
+
62
+ function mapOpenError(err: unknown, path: string): NodeJS.ErrnoException {
63
+ const gErr = err as { code?: number; message?: string } | null | undefined;
64
+ const msg = gErr?.message ?? '';
65
+ // GLib.IOChannel.new_file() always throws GLib.FileError (not Gio.IOErrorEnum).
66
+ // GLIB_FILE_ERROR_TO_NODE maps GLib.FileError numeric values to Node.js codes.
67
+ const code = GLIB_FILE_ERROR_TO_NODE[gErr?.code ?? -1] ?? 'EIO';
68
+ const error = new Error(`${code}: ${msg || 'unknown error'}, open '${path}'`) as NodeJS.ErrnoException;
69
+ error.code = code;
70
+ error.syscall = 'open';
71
+ error.path = path;
72
+ return error;
73
+ }
74
+
31
75
  export class FileHandle implements IFileHandle {
32
76
 
33
77
  /** Not part of the default implementation, used internal by gjsify */
@@ -43,7 +87,13 @@ export class FileHandle implements IFileHandle {
43
87
  }) {
44
88
  this.options.flags ||= "r";
45
89
  this.options.mode ||= 0o666;
46
- this._file = GLib.IOChannel.new_file(options.path.toString(), this.options.flags);
90
+ try {
91
+ this._file = GLib.IOChannel.new_file(options.path.toString(), this.options.flags);
92
+ } catch (err: unknown) {
93
+ throw mapOpenError(err, options.path.toString());
94
+ }
95
+ // Binary mode: prevent GLib from doing any character set conversion.
96
+ this._file.set_encoding(null as unknown as string);
47
97
  this.fd = this._file.unix_get_fd();
48
98
 
49
99
  FileHandle.instances[this.fd] = this;
@@ -128,7 +178,7 @@ export class FileHandle implements IFileHandle {
128
178
  * destroyed. Set the `emitClose` option to `false` to change this behavior.
129
179
  *
130
180
  * ```js
131
- * import { open } from 'fs/promises';
181
+ * import { open } from 'node:fs/promises';
132
182
  *
133
183
  * const fd = await open('/dev/input/event0');
134
184
  * // Create a stream from some character device.
@@ -154,7 +204,7 @@ export class FileHandle implements IFileHandle {
154
204
  * An example to read the last 10 bytes of a file which is 100 bytes long:
155
205
  *
156
206
  * ```js
157
- * import { open } from 'fs/promises';
207
+ * import { open } from 'node:fs/promises';
158
208
  *
159
209
  * const fd = await open('sample.txt');
160
210
  * fd.createReadStream({ start: 90, end: 99 });
@@ -220,13 +270,13 @@ export class FileHandle implements IFileHandle {
220
270
  async read<T extends NodeJS.ArrayBufferView>(buffer: T, offset?: number | null, length?: number | null, position?: ReadPosition | null): Promise<FileReadResult<T>>
221
271
  async read<T extends NodeJS.ArrayBufferView = Buffer>(options?: FileReadOptions<T>): Promise<FileReadResult<T>>
222
272
 
223
- async read<T extends NodeJS.ArrayBufferView = Buffer>(args: any[]): Promise<FileReadResult<T>> {
273
+ async read<T extends NodeJS.ArrayBufferView = Buffer>(...args: any[]): Promise<FileReadResult<T>> {
224
274
  let buffer: T | undefined;
225
275
  let offset: number | null | undefined;
226
276
  let length: number | null | undefined;
227
- let position: number | null | undefined;
277
+ let position: number | null | undefined;
228
278
 
229
- if (typeof args[0] === 'object') {
279
+ if (typeof args[0] === 'object' && !(args[0] instanceof Uint8Array) && !(args[0] instanceof Buffer)) {
230
280
  const options: FileReadOptions<T> = args[0];
231
281
  buffer = options.buffer;
232
282
  offset = options.offset;
@@ -239,31 +289,27 @@ export class FileHandle implements IFileHandle {
239
289
  position = args[3];
240
290
  }
241
291
 
242
- if(offset) {
243
- const status = this._file.seek_position(offset, GLib.SeekType.CUR);
244
- if(status === GLib.IOStatus.ERROR) {
245
- throw new Error("Error on set offset!")
246
- }
247
- }
248
- if(length) this._file.set_buffer_size(length);
249
- if(position) {
250
- const status = this._file.seek_position(position, GLib.SeekType.SET);
251
- if(status === GLib.IOStatus.ERROR) {
252
- throw new Error("Error on set position!")
253
- }
254
- }
255
-
256
- const [status, buf, bytesRead] = this._file.read_chars();
257
- if(status === GLib.IOStatus.ERROR) {
258
- throw new Error("Error on read!")
292
+ const bufView = buffer as unknown as Uint8Array;
293
+ const bufOffset = offset ?? 0;
294
+ const readLength = length ?? bufView?.byteLength ?? 65536;
295
+
296
+ // Use Gio.File.load_contents for reliable position-based reads.
297
+ // GLib.IOChannel.read_chars() is not introspectable in GJS (caller-allocated buffer),
298
+ // and read_to_end() has stdio-buffer visibility issues after mixed write/read.
299
+ const gFile = Gio.File.new_for_path(this.options.path.toString());
300
+ const [, fileContents] = gFile.load_contents(null);
301
+ const fileData = fileContents as Uint8Array;
302
+ const startPos = (position as number | null) ?? 0;
303
+ const readData = fileData.slice(startPos, startPos + readLength);
304
+ const bytesRead = readData.length;
305
+ if (bufView && bytesRead > 0) {
306
+ bufView.set(readData, bufOffset);
259
307
  }
260
308
 
261
- buffer = buf as T;
262
-
263
309
  return {
264
310
  bytesRead,
265
- buffer,
266
- }
311
+ buffer: buffer as T,
312
+ };
267
313
  }
268
314
  /**
269
315
  * Returns a `ReadableStream` that may be used to read the files data.
@@ -272,7 +318,7 @@ export class FileHandle implements IFileHandle {
272
318
  * or closing.
273
319
  *
274
320
  * ```js
275
- * import { open } from 'fs/promises';
321
+ * import { open } from 'node:fs/promises';
276
322
  *
277
323
  * const file = await open('./some/file/to/read');
278
324
  *
@@ -309,7 +355,7 @@ export class FileHandle implements IFileHandle {
309
355
  encoding?: null | undefined;
310
356
  flag?: OpenMode | undefined;
311
357
  } | null
312
- ): Promise<Buffer>
358
+ ): Promise<Buffer<ArrayBuffer>>
313
359
  /**
314
360
  * Asynchronously reads the entire contents of a file. The underlying file will _not_ be closed automatically.
315
361
  * The `FileHandle` must have been opened for reading.
@@ -337,7 +383,7 @@ export class FileHandle implements IFileHandle {
337
383
  })
338
384
  | BufferEncoding
339
385
  | null
340
- ): Promise<string | Buffer> {
386
+ ): Promise<string | Buffer<ArrayBuffer>> {
341
387
  const encoding = getEncodingFromOptions(options, 'buffer');
342
388
  if (encoding) this._file.set_encoding(encoding);
343
389
 
@@ -397,7 +443,7 @@ export class FileHandle implements IFileHandle {
397
443
  * The following example retains only the first four bytes of the file:
398
444
  *
399
445
  * ```js
400
- * import { open } from 'fs/promises';
446
+ * import { open } from 'node:fs/promises';
401
447
  *
402
448
  * let filehandle = null;
403
449
  * try {
@@ -416,8 +462,14 @@ export class FileHandle implements IFileHandle {
416
462
  * @param [len=0]
417
463
  * @return Fulfills with `undefined` upon success.
418
464
  */
419
- async truncate(len?: number): Promise<void> {
420
- warnNotImplemented('fs.FileHandle.truncate');
465
+ async truncate(len: number = 0): Promise<void> {
466
+ const effectiveLen = Math.max(0, len);
467
+ this._file.flush();
468
+ const gFile = Gio.File.new_for_path(this.options.path.toString());
469
+ const [, currentContent] = gFile.load_contents(null);
470
+ const newContent = new Uint8Array(effectiveLen);
471
+ newContent.set(currentContent.slice(0, Math.min(effectiveLen, currentContent.length)));
472
+ gFile.replace_contents(newContent, null, false, Gio.FileCreateFlags.NONE, null);
421
473
  }
422
474
  /**
423
475
  * Change the file system timestamps of the object referenced by the `FileHandle` then resolves the promise with no arguments upon success.
@@ -445,7 +497,19 @@ export class FileHandle implements IFileHandle {
445
497
  * @since v10.0.0
446
498
  */
447
499
  async writeFile(data: string | Uint8Array, options?: (ObjectEncodingOptions & FlagAndOpenMode & Abortable) | BufferEncoding | null): Promise<void> {
448
- warnNotImplemented('fs.FileHandle.writeFile');
500
+ const encoding = getEncodingFromOptions(options);
501
+ let buf: Uint8Array;
502
+ if (typeof data === 'string') {
503
+ buf = Buffer.from(data, (encoding as BufferEncoding) || 'utf8');
504
+ } else {
505
+ buf = data;
506
+ }
507
+ this._file.seek_position(0, GLib.SeekType.SET);
508
+ const [status] = this._file.write_chars(buf, buf.length);
509
+ if (status === GLib.IOStatus.ERROR) {
510
+ throw new Error("Error writing to file!");
511
+ }
512
+ this._file.flush();
449
513
  }
450
514
  /**
451
515
  * Write `buffer` to the file.
@@ -474,6 +538,13 @@ export class FileHandle implements IFileHandle {
474
538
  bytesWritten: number;
475
539
  buffer: TBuffer;
476
540
  }>;
541
+ async write<TBuffer extends Uint8Array>(
542
+ buffer: TBuffer,
543
+ options?: { offset?: number; length?: number; position?: number },
544
+ ): Promise<{
545
+ bytesWritten: number;
546
+ buffer: TBuffer;
547
+ }>;
477
548
  async write(
478
549
  data: string,
479
550
  position?: number | null,
@@ -504,45 +575,38 @@ export class FileHandle implements IFileHandle {
504
575
  }
505
576
 
506
577
  encoding = getEncodingFromOptions(encoding, typeof data === 'string' ? 'utf8' : null);
507
- if (encoding) {
508
- console.log("set_encoding", encoding, this._file.get_encoding(), typeof data);
509
-
510
- this._file.set_encoding(encoding === 'buffer' ? null : encoding);
511
- }
512
578
 
513
- if(offset) {
514
- const status = this._file.seek_position(offset, GLib.SeekType.CUR);
515
- if(status === GLib.IOStatus.ERROR) {
516
- throw new Error("Error on set offset!")
517
- }
579
+ // Convert data to Uint8Array bytes
580
+ let writeBuf: Uint8Array;
581
+ if (typeof data === 'string') {
582
+ writeBuf = new TextEncoder().encode(data);
583
+ } else {
584
+ writeBuf = data as unknown as Uint8Array;
518
585
  }
519
- if(length) this._file.set_buffer_size(length);
520
-
521
- if(position) {
522
- const status = this._file.seek_position(position, GLib.SeekType.SET);
523
- if(status === GLib.IOStatus.ERROR) {
524
- throw new Error("Error on set position!")
525
- }
586
+ const bufOffset = offset ?? 0;
587
+ const writeLength = length ?? (writeBuf.byteLength - bufOffset);
588
+ const writeSlice = writeBuf.slice(bufOffset, bufOffset + writeLength);
589
+ const writePos = position ?? 0;
590
+
591
+ // Use Gio.File for reliable position-based writes.
592
+ // GLib.IOChannel write_chars + flush does not guarantee data is visible to
593
+ // subsequent Gio.File or g_file_get_contents readers (stdio-buffer flushing issue).
594
+ const gFile = Gio.File.new_for_path(this.options.path.toString());
595
+ let existingData: Uint8Array;
596
+ try {
597
+ const [, existing] = gFile.load_contents(null);
598
+ existingData = existing as Uint8Array;
599
+ } catch {
600
+ existingData = new Uint8Array(0);
526
601
  }
602
+ const newSize = Math.max(existingData.length, writePos + writeSlice.length);
603
+ const newContent = new Uint8Array(newSize);
604
+ newContent.set(existingData);
605
+ newContent.set(writeSlice, writePos);
606
+ gFile.replace_contents(newContent, null, false, Gio.FileCreateFlags.NONE, null);
527
607
 
528
- let bytesWritten = 0;
529
- let status: GLib.IOStatus;
530
-
531
- if(typeof data === 'string') {
532
- status = this._file.write_unichar(data);
533
- bytesWritten = data.length;
534
- } else {
535
- const [_status, _bytesWritten] = this._file.write_chars(data as Uint8Array, length);
536
- bytesWritten = _bytesWritten;
537
- status = _status;
538
- }
539
-
540
- if(status === GLib.IOStatus.ERROR) {
541
- throw new Error("Error on write to file!")
542
- }
543
-
544
608
  return {
545
- bytesWritten,
609
+ bytesWritten: writeSlice.length,
546
610
  buffer: data
547
611
  }
548
612
  }
@@ -563,11 +627,11 @@ export class FileHandle implements IFileHandle {
563
627
  * @param position The offset from the beginning of the file where the data from `buffers` should be written. If `position` is not a `number`, the data will be written at the current
564
628
  * position.
565
629
  */
566
- async writev(buffers: ReadonlyArray<NodeJS.ArrayBufferView>, position?: number): Promise<WriteVResult> {
630
+ async writev<TBuffers extends readonly NodeJS.ArrayBufferView[]>(buffers: TBuffers, position?: number): Promise<WriteVResult<TBuffers>> {
567
631
  warnNotImplemented('fs.FileHandle.writev');
568
632
  return {
569
633
  bytesWritten: 0,
570
- buffers: [],
634
+ buffers: buffers as unknown as TBuffers,
571
635
  }
572
636
  }
573
637
  /**
@@ -576,11 +640,11 @@ export class FileHandle implements IFileHandle {
576
640
  * @param position The offset from the beginning of the file where the data should be read from. If `position` is not a `number`, the data will be read from the current position.
577
641
  * @return Fulfills upon success an object containing two properties:
578
642
  */
579
- async readv(buffers: ReadonlyArray<NodeJS.ArrayBufferView>, position?: number): Promise<ReadVResult> {
643
+ async readv<TBuffers extends readonly NodeJS.ArrayBufferView[]>(buffers: TBuffers, position?: number): Promise<ReadVResult<TBuffers>> {
580
644
  warnNotImplemented('fs.FileHandle.readv');
581
645
  return {
582
646
  bytesRead: 0,
583
- buffers: [],
647
+ buffers: buffers as unknown as TBuffers,
584
648
  }
585
649
  }
586
650
  /**
@@ -588,7 +652,7 @@ export class FileHandle implements IFileHandle {
588
652
  * complete.
589
653
  *
590
654
  * ```js
591
- * import { open } from 'fs/promises';
655
+ * import { open } from 'node:fs/promises';
592
656
  *
593
657
  * let filehandle;
594
658
  * try {
@@ -601,6 +665,10 @@ export class FileHandle implements IFileHandle {
601
665
  * @return Fulfills with `undefined` upon success.
602
666
  */
603
667
  async close(): Promise<void> {
604
- this._file.close();
668
+ this._file.shutdown(true);
669
+ }
670
+
671
+ async [Symbol.asyncDispose](): Promise<void> {
672
+ await this.close();
605
673
  }
606
674
  }
package/src/fs-watcher.ts CHANGED
@@ -1,8 +1,12 @@
1
+ // Reference: Node.js lib/internal/fs/watchers.js
2
+ // Reimplemented for GJS using Gio.FileMonitor
3
+
4
+ import GLib from '@girs/glib-2.0';
1
5
  import Gio from '@girs/gio-2.0';
2
- import { EventEmitter } from 'events';
6
+ import { EventEmitter } from 'node:events';
3
7
  const privates = new WeakMap;
4
8
 
5
- import type { FSWatcher as IFSWatcher } from 'fs';
9
+ import type { FSWatcher as IFSWatcher } from 'node:fs';
6
10
 
7
11
  export class FSWatcher extends EventEmitter implements IFSWatcher {
8
12
 
@@ -11,29 +15,72 @@ export class FSWatcher extends EventEmitter implements IFSWatcher {
11
15
  if (!options || typeof options !== 'object')
12
16
  options = {persistent: true};
13
17
 
18
+ const persistent = options.persistent !== false;
14
19
  const cancellable = Gio.Cancellable.new();
15
20
  const file = Gio.File.new_for_path(filename);
16
21
  const watcher = file.monitor(Gio.FileMonitorFlags.NONE, cancellable);
17
22
  watcher.connect('changed', changed.bind(this));
18
23
 
24
+ // When persistent is true, acquire a reference on the default GLib main context
25
+ // so the main loop stays alive while this watcher is active.
26
+ // This mirrors Node.js behavior where persistent watchers keep the event loop alive.
27
+ let sourceId: number | null = null;
28
+ if (persistent) {
29
+ // Add a never-firing timeout source to keep the mainloop alive.
30
+ // This is a lightweight way to hold a ref on the main context.
31
+ sourceId = GLib.timeout_add(GLib.PRIORITY_LOW, 2147483647, () => GLib.SOURCE_CONTINUE);
32
+ }
33
+
19
34
  privates.set(this, {
20
- persistent: options.persistent,
35
+ persistent,
21
36
  cancellable,
37
+ sourceId,
22
38
  // even if never used later on, the monitor needs to be
23
39
  // attached to this instance or GJS reference counter
24
40
  // will ignore it and no watch will ever happen
25
41
  watcher
26
42
  });
27
43
  if (listener) this.on('change', listener);
28
- // TODO: if (options.persistent) mainloop.wait();
29
44
  }
30
45
 
31
46
  close() {
32
- const {cancellable, persistent} = privates.get(this);
33
- if (!cancellable.is_cancelled()) {
34
- cancellable.cancel();
35
- // TODO: if (persistent) mainloop.go();
47
+ const priv = privates.get(this);
48
+ if (!priv.cancellable.is_cancelled()) {
49
+ priv.cancellable.cancel();
50
+ if (priv.sourceId !== null) {
51
+ GLib.source_remove(priv.sourceId);
52
+ priv.sourceId = null;
53
+ }
54
+ }
55
+ }
56
+
57
+ /**
58
+ * When called, requests that the Node.js event loop not exit so long as the
59
+ * FSWatcher is active. Calling ref() multiple times has no effect.
60
+ */
61
+ ref(): this {
62
+ const priv = privates.get(this);
63
+ if (!priv.persistent && !priv.cancellable.is_cancelled()) {
64
+ priv.persistent = true;
65
+ priv.sourceId = GLib.timeout_add(GLib.PRIORITY_LOW, 2147483647, () => GLib.SOURCE_CONTINUE);
66
+ }
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * When called, the active FSWatcher will not require the Node.js event loop
72
+ * to remain active. Calling unref() multiple times has no effect.
73
+ */
74
+ unref(): this {
75
+ const priv = privates.get(this);
76
+ if (priv.persistent) {
77
+ priv.persistent = false;
78
+ if (priv.sourceId !== null) {
79
+ GLib.source_remove(priv.sourceId);
80
+ priv.sourceId = null;
81
+ }
36
82
  }
83
+ return this;
37
84
  }
38
85
 
39
86
  };