opfs-worker 2.0.0 → 2.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 (134) hide show
  1. package/README.md +64 -428
  2. package/dist/BaseOPFS-BusBkyJX.js +2114 -0
  3. package/dist/BaseOPFS-BusBkyJX.js.map +1 -0
  4. package/dist/BaseOPFS-CYwrD7-q.cjs +4 -0
  5. package/dist/BaseOPFS-CYwrD7-q.cjs.map +1 -0
  6. package/dist/OPFSAsync-B2dR-2VR.cjs +2 -0
  7. package/dist/OPFSAsync-B2dR-2VR.cjs.map +1 -0
  8. package/dist/OPFSAsync-CO44W23e.js +124 -0
  9. package/dist/OPFSAsync-CO44W23e.js.map +1 -0
  10. package/dist/OPFSFacade-CEaSqTut.js +308 -0
  11. package/dist/OPFSFacade-CEaSqTut.js.map +1 -0
  12. package/dist/OPFSFacade-hrNXCn6w.cjs +2 -0
  13. package/dist/OPFSFacade-hrNXCn6w.cjs.map +1 -0
  14. package/dist/OPFSSync-BPrNtvFS.cjs +2 -0
  15. package/dist/OPFSSync-BPrNtvFS.cjs.map +1 -0
  16. package/dist/OPFSSync-D7obTsl3.js +208 -0
  17. package/dist/OPFSSync-D7obTsl3.js.map +1 -0
  18. package/dist/assets/dedicated.worker-DW6UqFo0.js.map +1 -0
  19. package/dist/{OPFSWorker.d.ts → core/BaseOPFS.d.ts} +53 -292
  20. package/dist/core/BaseOPFS.d.ts.map +1 -0
  21. package/dist/core/OPFSAsync.d.ts +55 -0
  22. package/dist/core/OPFSAsync.d.ts.map +1 -0
  23. package/dist/core/OPFSSync.d.ts +54 -0
  24. package/dist/core/OPFSSync.d.ts.map +1 -0
  25. package/dist/createOPFSAsync-DdhJYoC0.js +10 -0
  26. package/dist/createOPFSAsync-DdhJYoC0.js.map +1 -0
  27. package/dist/createOPFSAsync-DyWpvmL-.cjs +2 -0
  28. package/dist/createOPFSAsync-DyWpvmL-.cjs.map +1 -0
  29. package/dist/createOPFSDedicated-BwTc0NV5.js +2407 -0
  30. package/dist/createOPFSDedicated-BwTc0NV5.js.map +1 -0
  31. package/dist/createOPFSDedicated-CEeRexom.cjs +2357 -0
  32. package/dist/createOPFSDedicated-CEeRexom.cjs.map +1 -0
  33. package/dist/createOPFSShared-BSFWsoLe.js +23 -0
  34. package/dist/createOPFSShared-BSFWsoLe.js.map +1 -0
  35. package/dist/createOPFSShared-BoCft_BS.cjs +2 -0
  36. package/dist/createOPFSShared-BoCft_BS.cjs.map +1 -0
  37. package/dist/dedicated.worker.js +2355 -0
  38. package/dist/dedicated.worker.js.map +1 -0
  39. package/dist/{OPFSFacade.d.ts → facade/OPFSFacade.d.ts} +51 -25
  40. package/dist/facade/OPFSFacade.d.ts.map +1 -0
  41. package/dist/facade/createOPFSAsync.d.ts +16 -0
  42. package/dist/facade/createOPFSAsync.d.ts.map +1 -0
  43. package/dist/facade/createOPFSDedicated.d.ts +17 -0
  44. package/dist/facade/createOPFSDedicated.d.ts.map +1 -0
  45. package/dist/facade/createOPFSShared.d.ts +24 -0
  46. package/dist/facade/createOPFSShared.d.ts.map +1 -0
  47. package/dist/index.async.cjs +2 -0
  48. package/dist/index.async.cjs.map +1 -0
  49. package/dist/index.async.d.ts +10 -0
  50. package/dist/index.async.d.ts.map +1 -0
  51. package/dist/index.async.js +55 -0
  52. package/dist/index.async.js.map +1 -0
  53. package/dist/index.cjs +1 -2477
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.ts +17 -3
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +61 -2828
  58. package/dist/index.js.map +1 -1
  59. package/dist/index.pure.cjs +1 -1
  60. package/dist/index.pure.cjs.map +1 -1
  61. package/dist/index.pure.d.ts +3 -1
  62. package/dist/index.pure.d.ts.map +1 -1
  63. package/dist/index.pure.js +50 -1053
  64. package/dist/index.pure.js.map +1 -1
  65. package/dist/index.sharedworker.cjs +2 -0
  66. package/dist/index.sharedworker.cjs.map +1 -0
  67. package/dist/index.sharedworker.d.ts +11 -0
  68. package/dist/index.sharedworker.d.ts.map +1 -0
  69. package/dist/index.sharedworker.js +55 -0
  70. package/dist/index.sharedworker.js.map +1 -0
  71. package/dist/index.sync.cjs +2 -0
  72. package/dist/index.sync.cjs.map +1 -0
  73. package/dist/index.sync.d.ts +11 -0
  74. package/dist/index.sync.d.ts.map +1 -0
  75. package/dist/index.sync.js +55 -0
  76. package/dist/index.sync.js.map +1 -0
  77. package/dist/shared.worker.js +2240 -0
  78. package/dist/shared.worker.js.map +1 -0
  79. package/dist/types.d.ts +42 -4
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/worker/createDedicatedWorker.d.ts +30 -0
  82. package/dist/worker/createDedicatedWorker.d.ts.map +1 -0
  83. package/dist/worker/createSharedWorker.d.ts +37 -0
  84. package/dist/worker/createSharedWorker.d.ts.map +1 -0
  85. package/dist/worker/dedicated.worker.d.ts +2 -0
  86. package/dist/worker/dedicated.worker.d.ts.map +1 -0
  87. package/dist/worker/shared.worker.d.ts +2 -0
  88. package/dist/worker/shared.worker.d.ts.map +1 -0
  89. package/docs/api/backend.md +36 -0
  90. package/docs/api/create.md +82 -0
  91. package/docs/api/facade.md +53 -0
  92. package/docs/choosing-a-mode.md +41 -0
  93. package/docs/file-descriptors.md +46 -605
  94. package/docs/guides/async.md +36 -0
  95. package/docs/guides/dedicated.md +76 -0
  96. package/docs/guides/hashing.md +34 -0
  97. package/docs/guides/pure.md +46 -0
  98. package/docs/guides/sharedworker.md +57 -0
  99. package/docs/guides/streaming.md +185 -0
  100. package/docs/guides/watching.md +35 -0
  101. package/docs/migration.md +41 -0
  102. package/docs/types.md +105 -153
  103. package/package.json +53 -4
  104. package/src/{OPFSWorker.ts → core/BaseOPFS.ts} +159 -629
  105. package/src/core/OPFSAsync.ts +248 -0
  106. package/src/core/OPFSSync.ts +460 -0
  107. package/src/{OPFSFacade.ts → facade/OPFSFacade.ts} +134 -58
  108. package/src/facade/createOPFSAsync.ts +22 -0
  109. package/src/facade/createOPFSDedicated.ts +23 -0
  110. package/src/facade/createOPFSShared.ts +30 -0
  111. package/src/index.async.ts +11 -0
  112. package/src/index.pure.ts +3 -2
  113. package/src/index.sharedworker.ts +12 -0
  114. package/src/index.sync.ts +12 -0
  115. package/src/index.ts +37 -8
  116. package/src/types.ts +45 -4
  117. package/src/worker/createDedicatedWorker.ts +74 -0
  118. package/src/worker/createSharedWorker.ts +67 -0
  119. package/src/worker/dedicated.worker.ts +13 -0
  120. package/src/worker/shared.worker.ts +17 -0
  121. package/dist/OPFSFacade.d.ts.map +0 -1
  122. package/dist/OPFSWorker.d.ts.map +0 -1
  123. package/dist/assets/worker.entry-DUlEoroc.js.map +0 -1
  124. package/dist/createOPFSWorker.d.ts +0 -17
  125. package/dist/createOPFSWorker.d.ts.map +0 -1
  126. package/dist/helpers-DNj8ZoMu.cjs +0 -4
  127. package/dist/helpers-DNj8ZoMu.cjs.map +0 -1
  128. package/dist/helpers-WY2jfbOT.js +0 -1427
  129. package/dist/helpers-WY2jfbOT.js.map +0 -1
  130. package/dist/worker.entry.d.ts +0 -2
  131. package/dist/worker.entry.d.ts.map +0 -1
  132. package/docs/api-reference.md +0 -815
  133. package/src/createOPFSWorker.ts +0 -57
  134. package/src/worker.entry.ts +0 -6
@@ -1,191 +1,66 @@
1
1
  # File Descriptors
2
2
 
3
- This document covers working with file descriptors in OPFS Worker, which provides low-level file I/O operations similar to POSIX file descriptors.
3
+ Positional I/O on the **dedicated / sync** backend (`createOPFSDedicated` / `createOPFS`, or `OPFSSync` from `/pure`). Async and SharedWorker throw `ENOTSUP`.
4
4
 
5
- ## Requirements
6
-
7
- **Important**: When using file descriptors from the main window (browser), you must import and use `Comlink` for proper buffer handling:
5
+ If you call FD `read`/`write` on **`fs.backend`** from the main thread, transfer buffers with Comlink. The [facade](./api/facade.md) does that for you.
8
6
 
9
7
  ```typescript
10
8
  import { transfer } from 'comlink';
11
9
  ```
12
10
 
13
- ## Overview
14
-
15
- File descriptors provide efficient, low-level access to files for reading, writing, and manipulating file data. They're particularly useful for:
16
-
17
- - **Large file operations**: Reading/writing files in chunks without loading entire content into memory
18
- - **Streaming operations**: Processing files sequentially or in specific positions
19
- - **Performance-critical applications**: Direct file access without the overhead of high-level methods
20
- - **Binary data manipulation**: Working with raw bytes at specific file positions
21
-
22
- ## Table of Contents
23
-
24
- - [Requirements](#requirements)
25
- - [File Descriptor Basics](#file-descriptor-basics)
26
- - [Opening Files](#opening-files)
27
- - [Reading Data](#reading-data)
28
- - [Writing Data](#writing-data)
29
- - [File Positioning](#file-positioning)
30
- - [File Operations](#file-operations)
31
- - [Closing Files](#closing-files)
32
- - [Error Handling](#error-handling)
33
- - [Performance Considerations](#performance-considerations)
34
- - [Examples](#examples)
35
-
36
- ## File Descriptor Basics
37
-
38
- A file descriptor is a small integer that represents an open file. In OPFS Worker, file descriptors provide:
39
-
40
- - **Sequential access**: Read/write operations that advance the file position
41
- - **Random access**: Read/write at specific file positions
42
- - **Efficient I/O**: Direct access to file data without intermediate buffers
43
- - **Resource management**: Automatic cleanup when closed
44
-
45
- ### File Descriptor Lifecycle
46
-
47
- 1. **Open**: Create a file descriptor with `open()`
48
- 2. **Use**: Perform read/write operations
49
- 3. **Close**: Release the file descriptor with `close()`
50
-
51
- ## Opening Files
52
-
53
- ### `open(path: string, options?: FileOpenOptions): Promise<number>`
54
-
55
- Opens a file and returns a file descriptor number.
11
+ ## `open(path, options?): Promise<number>`
56
12
 
57
13
  ```typescript
58
- // Basic file opening
59
14
  const fd = await fs.open('/data/config.json');
60
-
61
- // Create new file if it doesn't exist
62
15
  const fd = await fs.open('/data/new.txt', { create: true });
63
-
64
- // Create file exclusively (fails if exists)
65
16
  const fd = await fs.open('/data/unique.txt', { create: true, exclusive: true });
66
-
67
- // Open and truncate file to zero length
68
17
  const fd = await fs.open('/data/log.txt', { create: true, truncate: true });
69
18
  ```
70
19
 
71
- **Parameters:**
72
-
73
- - `path`: The path to the file to open
74
- - `options.create` (optional): Whether to create the file if it doesn't exist (default: `false`)
75
- - `options.exclusive` (optional): If `true` and `create` is `true`, fails if file already exists (default: `false`)
76
- - `options.truncate` (optional): Whether to truncate the file to zero length (default: `false`)
77
-
78
- **Returns:** `Promise<number>` - File descriptor number
79
-
80
- **Throws:**
20
+ | Option | Default | |
21
+ | ------ | ------- | --- |
22
+ | `create` | `false` | Create the file if missing |
23
+ | `exclusive` | `false` | With `create`, fail if the file already exists |
24
+ | `truncate` | `false` | Truncate to zero length |
81
25
 
82
- - `AlreadyExistsError` if file exists and `exclusive: true`
83
- - `FileTypeError` if path is a directory
84
- - `FileSystemOperationError` for other failures
26
+ Throws `AlreadyExistsError` (`exclusive`), `FileTypeError` (path is a directory), or `FileSystemOperationError`.
85
27
 
86
- **⚠️ Race Condition Warning**: The `exclusive` option provides **best-effort atomicity only**. **Race conditions ARE possible** if two workers try to create the same file simultaneously. OPFS does not provide true file locking, so don't rely on `exclusive` for critical synchronization. If you need real atomicity, implement your own locking mechanism.
28
+ `exclusive` is best-effort only two workers can still race. OPFS has no real file locking.
87
29
 
88
- ## Reading Data
89
-
90
- ### `read(fd: number, buffer: Uint8Array, offset: number, length: number, position?: number | null): Promise<{bytesRead: number, buffer: Uint8Array}>`
91
-
92
- Reads data from a file descriptor into a buffer.
30
+ ## `read(fd, buffer, offset, length, position?): Promise<{ bytesRead, buffer }>`
93
31
 
94
32
  ```typescript
95
- const fd = await fs.open('/data/file.txt');
96
-
97
- // Read 1024 bytes starting at current position
98
33
  const buffer = new Uint8Array(1024);
99
- const result = await fs.read(fd, buffer, 0, 1024, null);
100
- console.log(`Read ${result.bytesRead} bytes`);
101
-
102
- // Read 512 bytes at specific position
103
- const buffer2 = new Uint8Array(512);
104
- const result2 = await fs.read(fd, buffer2, 0, 512, 1000);
105
- console.log(`Read ${result2.bytesRead} bytes starting at position 1000`);
106
-
107
- await fs.close(fd);
34
+ const { bytesRead, buffer: out } = await fs.read(fd, buffer, 0, 1024, null);
108
35
  ```
109
36
 
110
- **Parameters:**
37
+ - `position` `null` / `undefined` — current cursor, then advance
38
+ - `position` number — that offset, cursor unchanged
39
+ - Returns `bytesRead === 0` at EOF
111
40
 
112
- - `fd`: File descriptor to read from
113
- - `buffer`: The buffer to read data into
114
- - `offset`: Offset in the buffer to start writing at
115
- - `length`: Number of bytes to read
116
- - `position`: Position in file to read from (`null` for current position, `undefined` for current position)
117
-
118
- **Returns:** `Promise<{bytesRead: number, buffer: Uint8Array}>` - Object containing bytes read and modified buffer
119
-
120
- **⚠️ Node.js Compatibility Note**: While similar to Node.js `fs.read()`, the returned `buffer` may be a **different object** due to `Comlink.transfer()`. Unlike Node.js where the same buffer object is modified in-place, here you must use the returned buffer for safety.
121
-
122
- **⚠️ CRITICAL WARNING**: The `read()` method uses `Comlink.transfer()` for efficient buffer handling across Web Worker boundaries. This requires **bidirectional transfer**:
123
-
124
- 1. **From main window to worker**: You must transfer buffer ownership using `transfer(buffer, [buffer.buffer])`
125
- 2. **From worker to main window**: The worker transfers the modified buffer back to you
126
-
127
- **🚨 NEVER USE THE ORIGINAL BUFFER AFTER TRANSFER** - it becomes detached and unusable. **ALWAYS use the buffer returned from the method**. Using the original buffer will result in silent data corruption or errors.
128
-
129
- This ensures zero-copy performance but requires careful buffer management. See the usage examples below for proper implementation.
130
-
131
- **Behavior:**
132
-
133
- - Returns `0` when reaching end of file
134
- - Automatically advances file position if `position` is `null` or `undefined`
135
- - Reads up to `length` bytes, but may read fewer if end of file is reached
136
- - Throws error if reading beyond file bounds
137
-
138
- ### Buffer Transfer Usage
139
-
140
- **Important**: When using `read()` from the main window, you must transfer buffer ownership to the worker. The worker then transfers the modified buffer back to you.
141
-
142
- #### From Main Window (Browser)
41
+ With a Comlink proxy, buffers are transferred both ways. Use the **returned** buffer; the original is detached after transfer.
143
42
 
144
43
  ```typescript
145
44
  import { transfer } from 'comlink';
146
45
 
147
- // Create buffer in main window
148
46
  const buffer = new Uint8Array(64);
149
-
150
- // Transfer buffer ownership to worker when calling read()
151
- const { bytesRead, buffer: modifiedBuffer } = await fs.read(
47
+ const { bytesRead, buffer: out } = await fs.read(
152
48
  fd,
153
- transfer(buffer, [buffer.buffer]), // Transfer ownership to worker
49
+ transfer(buffer, [buffer.buffer]),
154
50
  0,
155
51
  buffer.length,
156
52
  null
157
53
  );
158
-
159
- // Now modifiedBuffer is "alive" again in main window
160
- console.log('bytesRead =', bytesRead);
161
-
162
- // Work directly with the returned buffer
163
- const text = new TextDecoder().decode(modifiedBuffer.subarray(0, bytesRead));
164
- console.log('read:', text);
165
-
166
- // Note: The original 'buffer' is now detached and cannot be used
167
- // Always use 'modifiedBuffer' from the result
168
- ```
169
-
170
- #### From Worker (Direct Usage)
171
-
172
- ```typescript
173
- // When using OPFSWorker directly in a worker
174
- const buffer = new Uint8Array(64);
175
- const { bytesRead, buffer: modifiedBuffer } = await fs.read(fd, buffer, 0, buffer.length, null);
176
-
177
- // modifiedBuffer contains the read data
178
- const data = modifiedBuffer.subarray(0, bytesRead);
179
54
  ```
180
55
 
181
- #### Common Patterns
56
+ Inside a worker with `OPFSSync` directly, plain buffers are fine — no `transfer`.
182
57
 
183
- **Reading in chunks:**
58
+ Chunked read from the main thread:
184
59
 
185
60
  ```typescript
186
61
  const fd = await fs.open('/data/large-file.txt');
187
62
  const chunkSize = 1024;
188
- let buffer = new Uint8Array(chunkSize); // Use 'let' for reassignment
63
+ let buffer = new Uint8Array(chunkSize);
189
64
 
190
65
  try {
191
66
  while (true) {
@@ -196,14 +71,8 @@ try {
196
71
  chunkSize,
197
72
  null
198
73
  );
199
-
200
- if (result.bytesRead === 0) break; // EOF
201
-
202
- // Process the chunk
203
- const chunk = result.buffer.subarray(0, result.bytesRead);
204
- processChunk(chunk);
205
-
206
- // Create new buffer for next iteration (previous one was transferred)
74
+ if (result.bytesRead === 0) break;
75
+ processChunk(result.buffer.subarray(0, result.bytesRead));
207
76
  buffer = new Uint8Array(chunkSize);
208
77
  }
209
78
  } finally {
@@ -211,486 +80,58 @@ try {
211
80
  }
212
81
  ```
213
82
 
214
- **Reading at specific positions:**
215
-
216
- ```typescript
217
- const fd = await fs.open('/data/file.txt');
218
- const buffer = new Uint8Array(100);
219
-
220
- // Read 100 bytes starting at position 500
221
- const result = await fs.read(
222
- fd,
223
- transfer(buffer, [buffer.buffer]),
224
- 0,
225
- 100,
226
- 500
227
- );
228
-
229
- console.log(`Read ${result.bytesRead} bytes from position 500`);
230
- ```
231
-
232
- ## Writing Data
233
-
234
- ### `write(fd: number, buffer: Uint8Array, offset?: number, length?: number, position?: number | null): Promise<number>`
83
+ Prefer small chunks over allocating a buffer the size of the whole file.
235
84
 
236
- Writes data from a buffer to a file descriptor.
85
+ ## `write(fd, buffer, offset?, length?, position?): Promise<number>`
237
86
 
238
87
  ```typescript
239
- const fd = await fs.open('/data/output.txt', { create: true });
240
-
241
- // Write entire buffer at current position
242
88
  const data = new TextEncoder().encode('Hello, World!');
243
89
  const bytesWritten = await fs.write(fd, data);
244
- console.log(`Wrote ${bytesWritten} bytes`);
245
-
246
- // Write portion of buffer at specific position
247
- const data2 = new TextEncoder().encode('Additional text');
248
90
  const bytesWritten2 = await fs.write(fd, data2, 0, 10, 100);
249
- console.log(`Wrote ${bytesWritten2} bytes at position 100`);
250
-
251
- await fs.close(fd);
252
91
  ```
253
92
 
254
- **Parameters:**
255
-
256
- - `fd`: File descriptor to write to
257
- - `buffer`: Buffer containing data to write
258
- - `offset` (optional): Offset in buffer to start reading from (default: `0`)
259
- - `length` (optional): Number of bytes to write (default: entire buffer from offset)
260
- - `position` (optional): Position in file to write to (`null`/`undefined` for current position)
261
-
262
- **Returns:** `Promise<number>` - Number of bytes actually written
263
-
264
- **Behavior:**
93
+ Same `position` rules as `read`. Extends the file if you write past EOF. Triggers watch events.
265
94
 
266
- - Automatically advances file position if `position` is `null` or `undefined`
267
- - Extends file size if writing beyond current end
268
- - Triggers file change notifications for watching systems
269
-
270
- ## File Positioning
271
-
272
- File descriptors maintain a current position that advances with read/write operations. You can control positioning through the `position` parameter:
95
+ ## Positioning
273
96
 
274
97
  ```typescript
275
- const fd = await fs.open('/data/file.txt');
276
-
277
- // Read at current position (starts at 0)
278
- const buffer1 = new Uint8Array(10);
279
- const result1 = await fs.read(fd, buffer1, 0, 10, null); // Position advances to 10
280
-
281
- // Read at current position (continues from 10)
282
- const buffer2 = new Uint8Array(10);
283
- const result2 = await fs.read(fd, buffer2, 0, 10, null); // Position advances to 20
284
-
285
- // Read at specific position (doesn't change current position)
286
- const buffer3 = new Uint8Array(10);
287
- const result3 = await fs.read(fd, buffer3, 0, 10, 0); // Reads from beginning, position stays at 20
288
-
289
- await fs.close(fd);
98
+ await fs.read(fd, buf, 0, 10, null); // cursor 0 → 10
99
+ await fs.read(fd, buf, 0, 10, null); // cursor 10 → 20
100
+ await fs.read(fd, buf, 0, 10, 0); // reads at 0, cursor stays 20
290
101
  ```
291
102
 
292
- **Position Behavior:**
293
-
294
- - **`null`**: Use current position, then advance it
295
- - **`undefined`**: Use current position, then advance it
296
- - **`number`**: Use specified position, don't change current position
297
-
298
- ## File Operations
299
-
300
- ### `fstat(fd: number): Promise<FileStat>`
301
-
302
- Get file status information by file descriptor.
103
+ ## `fstat` / `ftruncate` / `fsync`
303
104
 
304
105
  ```typescript
305
- const fd = await fs.open('/data/file.txt');
306
106
  const stats = await fs.fstat(fd);
307
-
308
- console.log(`File size: ${stats.size} bytes`);
309
- console.log(`Last modified: ${stats.mtime}`);
310
- console.log(`Is file: ${stats.isFile}`);
311
-
312
- // If hashing is enabled, hash will be included
313
- if (stats.hash) {
314
- console.log(`Hash: ${stats.hash}`);
315
- }
316
-
317
- await fs.close(fd);
318
- ```
319
-
320
- **Returns:** `Promise<FileStat>` - File statistics object
321
-
322
- ### `ftruncate(fd: number, size: number = 0): Promise<void>`
323
-
324
- Truncate file to specified size.
325
-
326
- ```typescript
327
- const fd = await fs.open('/data/file.txt', { create: true });
328
-
329
- // Write some data
330
- const data = new TextEncoder().encode('Hello, World!');
331
- await fs.write(fd, data);
332
-
333
- // Truncate to 5 bytes
334
107
  await fs.ftruncate(fd, 5);
335
-
336
- // File now contains only "Hello"
337
- const buffer = new Uint8Array(10);
338
- const result = await fs.read(fd, buffer, 0, 10, 0);
339
- console.log(new TextDecoder().decode(result.buffer.subarray(0, result.bytesRead))); // "Hello"
340
-
341
- await fs.close(fd);
342
- ```
343
-
344
- **Parameters:**
345
-
346
- - `fd`: File descriptor
347
- - `size`: New file size in bytes (default: `0`)
348
-
349
- **Behavior:**
350
-
351
- - Adjusts current position if it's beyond the new file size
352
- - Triggers file change notifications
353
- - Automatically flushes changes to storage
354
-
355
- ### `fsync(fd: number): Promise<void>`
356
-
357
- Synchronize file data to storage (equivalent to POSIX `fsync`).
358
-
359
- ```typescript
360
- const fd = await fs.open('/data/critical.txt', { create: true });
361
-
362
- // Write critical data
363
- const data = new TextEncoder().encode('Important information');
364
- await fs.write(fd, data);
365
-
366
- // Ensure data is written to storage
367
108
  await fs.fsync(fd);
368
-
369
- await fs.close(fd);
370
109
  ```
371
110
 
372
- **⚠️ Power-Loss Safety Disclaimer**: Unlike traditional filesystems, OPFS `fsync()` is **best-effort only**. **There are NO guarantees of power-loss safety** - the browser decides when/if to actually write data to disk. This is fundamentally different from POSIX `fsync()` which provides durability guarantees.
373
-
374
- **Use Cases:**
111
+ `ftruncate` clamps the cursor if it sits past the new size, emits a watch event, and flushes.
375
112
 
376
- - Ensuring critical data is persisted before closing
377
- - Database transaction commits
378
- - Log file integrity
379
- - Critical file operations
113
+ `fsync` is best-effort. Unlike POSIX, the browser does not guarantee durability on power loss.
380
114
 
381
- ## Closing Files
115
+ ## `close(fd)`
382
116
 
383
- ### `close(fd: number): Promise<void>`
384
-
385
- Closes a file descriptor and releases associated resources.
117
+ Always close in a `finally`. Pending writes are flushed; open FDs are also closed when the worker is disposed.
386
118
 
387
119
  ```typescript
388
120
  const fd = await fs.open('/data/file.txt');
389
-
390
- // Use the file descriptor
391
- const buffer = new Uint8Array(1024);
392
- await fs.read(fd, buffer, 0, 1024, null);
393
-
394
- // Always close when done
395
- await fs.close(fd);
396
- ```
397
-
398
- **Important Notes:**
399
-
400
- - **Always close file descriptors** when you're done with them
401
- - Unclosed file descriptors consume system resources
402
- - The file system automatically flushes pending writes before closing
403
- - File descriptors are automatically closed when the worker is disposed
404
-
405
- ## Error Handling
406
-
407
- File descriptor operations can fail for various reasons. Always handle errors appropriately:
408
-
409
- ```typescript
410
121
  try {
411
- const fd = await fs.open('/data/file.txt');
412
-
413
- try {
414
- const buffer = new Uint8Array(1024);
415
- const result = await fs.read(fd, buffer, 0, 1024, null);
416
- console.log(`Read ${result.bytesRead} bytes`);
417
- } finally {
418
- // Always close, even if read fails
419
- await fs.close(fd);
420
- }
421
- } catch (error) {
422
- if (error instanceof OPFSError) {
423
- switch (error.code) {
424
- case 'EEXIST':
425
- console.error('File already exists');
426
- break;
427
- case 'EISDIR':
428
- console.error('Path is a directory');
429
- break;
430
- default:
431
- console.error('File operation failed:', error.message);
432
- }
433
- } else {
434
- console.error('Unexpected error:', error);
435
- }
436
- }
437
- ```
438
-
439
- **Common Error Types:**
440
-
441
- - `AlreadyExistsError`: File already exists (with `exclusive: true`)
442
- - `ExistenceError`: File not found
443
- - `FileTypeError`: Path is directory when file expected (or vice versa)
444
- - `ValidationError`: Invalid arguments (offset, length, etc.)
445
- - `FileSystemOperationError`: General file system operation failures
446
- - `IOError`: I/O operation failures
447
- - `PermissionError`: Permission denied
448
-
449
- ## Performance Considerations
450
-
451
- ### 🚨 **CRITICAL MEMORY WARNING**
452
-
453
- **⚠️ FOR LARGE FILES - ALWAYS READ IN CHUNKS, NEVER READ THE ENTIRE FILE AT ONCE!**
454
-
455
- **Reading large files in one operation can crash the browser tab due to memory exhaustion.** Browsers have limited memory per tab, and creating massive buffers (hundreds of MB or GB) will cause:
456
-
457
- - **Tab crashes**
458
- - **Browser freezes**
459
- - **Out of memory errors**
460
- - **Poor user experience**
461
-
462
- **✅ GOOD**: `const buffer = new Uint8Array(8192); // 8KB chunks`
463
- **❌ BAD**: `const buffer = new Uint8Array(fileSize); // Entire file!`
464
-
465
- ### Buffer Management
466
-
467
- Reuse buffers when possible to reduce memory allocation:
468
-
469
- ```typescript
470
- const fd = await fs.open('/data/large-file.txt');
471
- const buffer = new Uint8Array(8192); // 8KB buffer
472
-
473
- let totalBytes = 0;
474
- let bytesRead;
475
-
476
- // Read file in chunks using the same buffer
477
- while (true) {
478
- const result = await fs.read(fd, buffer, 0, buffer.length, null);
479
- if (result.bytesRead === 0) break;
480
-
481
- totalBytes += result.bytesRead;
482
- // Process result.buffer.subarray(0, result.bytesRead)
483
- }
484
-
485
- await fs.close(fd);
486
- ```
487
-
488
- ### Batch Operations
489
-
490
- Group related operations together:
491
-
492
- ```typescript
493
- const fd = await fs.open('/data/log.txt', { create: true });
494
-
495
- // Batch multiple writes
496
- const entries = [
497
- 'User login',
498
- 'Data processed',
499
- 'Operation completed'
500
- ];
501
-
502
- for (const entry of entries) {
503
- const data = new TextEncoder().encode(entry + '\n');
504
- await fs.write(fd, data);
505
- }
506
-
507
- // Single sync at the end
508
- await fs.fsync(fd);
509
- await fs.close(fd);
510
- ```
511
-
512
- ### File Descriptor Limits
513
-
514
- Monitor the number of open file descriptors:
515
-
516
- ```typescript
517
- // Check how many files are open
518
- console.log(`Open files: ${fs.openFiles.size}`);
519
-
520
- // Close unused file descriptors promptly
521
- const unusedFds = [];
522
- // ... track unused file descriptors ...
523
- for (const fd of unusedFds) {
122
+ //
123
+ } finally {
524
124
  await fs.close(fd);
525
125
  }
526
126
  ```
527
127
 
528
- ## Examples
529
-
530
- ### Reading Large Files in Chunks
531
-
532
- ```typescript
533
- async function readLargeFile(filePath: string, chunkSize: number = 8192) {
534
- const fd = await fs.open(filePath);
535
- const chunks: Uint8Array[] = [];
536
-
537
- try {
538
- while (true) {
539
- // Create new buffer for each iteration (previous one was transferred)
540
- const buffer = new Uint8Array(chunkSize);
541
-
542
- const result = await fs.read(fd, buffer, 0, chunkSize, null);
543
- if (result.bytesRead === 0) break;
544
-
545
- // Copy the chunk data from the returned buffer
546
- const chunk = new Uint8Array(result.bytesRead);
547
- chunk.set(result.buffer.subarray(0, result.bytesRead));
548
- chunks.push(chunk);
549
- }
550
-
551
- return chunks;
552
- } finally {
553
- await fs.close(fd);
554
- }
555
- }
556
-
557
- // Usage
558
- const chunks = await readLargeFile('/data/large-file.dat', 16384);
559
- console.log(`Read ${chunks.length} chunks`);
560
- ```
561
-
562
- ### Binary File Processing
563
-
564
- ```typescript
565
- async function processBinaryFile(filePath: string) {
566
- const fd = await fs.open(filePath);
567
-
568
- try {
569
- // Read header (first 16 bytes)
570
- const headerBuffer = new Uint8Array(16);
571
- const headerResult = await fs.read(fd, headerBuffer, 0, 16, 0);
572
-
573
- // Process header
574
- const header = new DataView(headerResult.buffer.buffer);
575
- const fileType = header.getUint32(0, true);
576
- const dataSize = header.getUint32(4, true);
577
-
578
- // Read data section
579
- const dataBuffer = new Uint8Array(dataSize);
580
- const dataResult = await fs.read(fd, dataBuffer, 0, dataSize, 16);
581
-
582
- return { fileType, dataSize, data: dataBuffer };
583
- } finally {
584
- await fs.close(fd);
585
- }
586
- }
587
- ```
588
-
589
- ### Log File Management
590
-
591
- ```typescript
592
- class LogManager {
593
- private fd: number | null = null;
594
- private logPath: string;
595
-
596
- constructor(logPath: string) {
597
- this.logPath = logPath;
598
- }
599
-
600
- async open() {
601
- this.fd = await fs.open(this.logPath, { create: true });
602
- }
603
-
604
- async writeLog(level: string, message: string) {
605
- if (!this.fd) throw new Error('Log file not open');
606
-
607
- const timestamp = new Date().toISOString();
608
- const logEntry = `[${timestamp}] [${level}] ${message}\n`;
609
- const data = new TextEncoder().encode(logEntry);
610
-
611
- await fs.write(this.fd, data);
612
- }
613
-
614
- async flush() {
615
- if (this.fd) {
616
- await fs.fsync(this.fd);
617
- }
618
- }
619
-
620
- async close() {
621
- if (this.fd) {
622
- await fs.close(this.fd);
623
- this.fd = null;
624
- }
625
- }
626
- }
627
-
628
- // Usage
629
- const logger = new LogManager('/logs/app.log');
630
- await logger.open();
631
- await logger.writeLog('INFO', 'Application started');
632
- await logger.writeLog('ERROR', 'Something went wrong');
633
- await logger.flush();
634
- await logger.close();
635
- ```
636
-
637
- ### File Copy with Progress
638
-
639
- ```typescript
640
- async function copyFileWithProgress(sourcePath: string, destPath: string, onProgress?: (bytes: number) => void) {
641
- const sourceFd = await fs.open(sourcePath);
642
- const destFd = await fs.open(destPath, { create: true, truncate: true });
643
-
644
- try {
645
- const buffer = new Uint8Array(8192);
646
- let totalBytes = 0;
647
-
648
- while (true) {
649
- const result = await fs.read(sourceFd, buffer, 0, buffer.length, null);
650
- if (result.bytesRead === 0) break;
651
-
652
- await fs.write(destFd, result.buffer, 0, result.bytesRead);
653
- totalBytes += result.bytesRead;
654
-
655
- if (onProgress) {
656
- onProgress(totalBytes);
657
- }
658
- }
659
-
660
- await fs.fsync(destFd);
661
- } finally {
662
- await fs.close(sourceFd);
663
- await fs.close(destFd);
664
- }
665
-
666
- return totalBytes;
667
- }
668
-
669
- // Usage
670
- const totalBytes = await copyFileWithProgress(
671
- '/source/large-file.dat',
672
- '/dest/large-file.dat',
673
- (bytes) => console.log(`Copied ${bytes} bytes`)
674
- );
675
- console.log(`Copy complete: ${totalBytes} bytes`);
676
- ```
128
+ ## Errors
677
129
 
678
- ## Best Practices
679
-
680
- 1. **🚨 NEVER read entire large files at once** - use chunk-based reading to prevent browser crashes
681
- 2. **Always close file descriptors** in a `finally` block or use try-with-resources pattern
682
- 3. **Reuse buffers** for multiple read/write operations to reduce memory allocation
683
- 4. **Use appropriate buffer sizes** - too small causes many operations, too large wastes memory (8KB-64KB recommended)
684
- 5. **Flush critical data** with `fsync()` before closing important files
685
- 6. **Handle errors gracefully** and provide meaningful error messages
686
- 7. **Monitor resource usage** to avoid hitting file descriptor limits
687
- 8. **Use positioned I/O** when you need random access without changing current position
688
- 9. **Batch operations** when possible to reduce overhead
689
-
690
- ## Limitations
691
-
692
- - **Browser-specific**: File descriptor behavior may vary between browsers
693
- - **OPFS constraints**: Subject to browser storage limits and OPFS implementation details
694
- - **No file locking**: Concurrent access to the same file from multiple workers may cause issues
695
- - **Position limits**: File positions are limited by JavaScript number precision (2^53 - 1)
696
- - **No direct memory mapping**: Files must be read into buffers rather than memory-mapped
130
+ | Error | When |
131
+ | ----- | ---- |
132
+ | `AlreadyExistsError` | `exclusive: true` and file exists |
133
+ | `ExistenceError` | Missing path |
134
+ | `FileTypeError` | File vs directory mismatch |
135
+ | `ValidationError` | Bad offset / length / args |
136
+ | `FileSystemOperationError` / `IOError` | Underlying FS / I/O failure |
137
+ | `PermissionError` | Access denied |