opfs-worker 2.0.0 → 2.1.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 (135) hide show
  1. package/README.md +62 -439
  2. package/dist/BaseOPFS-BPw9AIQ-.cjs +4 -0
  3. package/dist/BaseOPFS-BPw9AIQ-.cjs.map +1 -0
  4. package/dist/BaseOPFS-Cylz6ijk.js +2036 -0
  5. package/dist/BaseOPFS-Cylz6ijk.js.map +1 -0
  6. package/dist/OPFSAsync-BKSAJJeh.js +124 -0
  7. package/dist/OPFSAsync-BKSAJJeh.js.map +1 -0
  8. package/dist/OPFSAsync-s2P7y3sw.cjs +2 -0
  9. package/dist/OPFSAsync-s2P7y3sw.cjs.map +1 -0
  10. package/dist/OPFSFacade-DePnNRgt.cjs +2 -0
  11. package/dist/OPFSFacade-DePnNRgt.cjs.map +1 -0
  12. package/dist/OPFSFacade-XohWlHuu.js +281 -0
  13. package/dist/OPFSFacade-XohWlHuu.js.map +1 -0
  14. package/dist/OPFSSync-C7KBepe_.cjs +2 -0
  15. package/dist/OPFSSync-C7KBepe_.cjs.map +1 -0
  16. package/dist/OPFSSync-CsSQkpk5.js +208 -0
  17. package/dist/OPFSSync-CsSQkpk5.js.map +1 -0
  18. package/dist/assets/dedicated.worker-nY3tK4zi.js.map +1 -0
  19. package/dist/{OPFSWorker.d.ts → core/BaseOPFS.d.ts} +34 -304
  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-BwKDmHsy.cjs +2 -0
  26. package/dist/createOPFSAsync-BwKDmHsy.cjs.map +1 -0
  27. package/dist/createOPFSAsync-Y4Mg6qte.js +10 -0
  28. package/dist/createOPFSAsync-Y4Mg6qte.js.map +1 -0
  29. package/dist/createOPFSDedicated-CsnV6MLP.js +2333 -0
  30. package/dist/createOPFSDedicated-CsnV6MLP.js.map +1 -0
  31. package/dist/createOPFSDedicated-pKTRWLdz.cjs +2283 -0
  32. package/dist/createOPFSDedicated-pKTRWLdz.cjs.map +1 -0
  33. package/dist/createOPFSShared-CGdd__PH.js +23 -0
  34. package/dist/createOPFSShared-CGdd__PH.js.map +1 -0
  35. package/dist/createOPFSShared-CskROt7U.cjs +2 -0
  36. package/dist/createOPFSShared-CskROt7U.cjs.map +1 -0
  37. package/dist/dedicated.worker.js +2281 -0
  38. package/dist/dedicated.worker.js.map +1 -0
  39. package/dist/{OPFSFacade.d.ts → facade/OPFSFacade.d.ts} +31 -23
  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 +2166 -0
  78. package/dist/shared.worker.js.map +1 -0
  79. package/dist/types.d.ts +9 -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/README.md +30 -0
  90. package/docs/api/backend.md +36 -0
  91. package/docs/api/create.md +82 -0
  92. package/docs/api/facade.md +52 -0
  93. package/docs/choosing-a-mode.md +41 -0
  94. package/docs/file-descriptors.md +8 -9
  95. package/docs/guides/async.md +36 -0
  96. package/docs/guides/dedicated.md +76 -0
  97. package/docs/guides/hashing.md +34 -0
  98. package/docs/guides/pure.md +46 -0
  99. package/docs/guides/sharedworker.md +57 -0
  100. package/docs/guides/streaming.md +51 -0
  101. package/docs/guides/watching.md +35 -0
  102. package/docs/migration.md +41 -0
  103. package/docs/types.md +20 -6
  104. package/package.json +42 -3
  105. package/src/{OPFSWorker.ts → core/BaseOPFS.ts} +54 -647
  106. package/src/core/OPFSAsync.ts +248 -0
  107. package/src/core/OPFSSync.ts +460 -0
  108. package/src/{OPFSFacade.ts → facade/OPFSFacade.ts} +83 -54
  109. package/src/facade/createOPFSAsync.ts +22 -0
  110. package/src/facade/createOPFSDedicated.ts +23 -0
  111. package/src/facade/createOPFSShared.ts +30 -0
  112. package/src/index.async.ts +11 -0
  113. package/src/index.pure.ts +3 -2
  114. package/src/index.sharedworker.ts +12 -0
  115. package/src/index.sync.ts +12 -0
  116. package/src/index.ts +37 -8
  117. package/src/types.ts +8 -4
  118. package/src/worker/createDedicatedWorker.ts +74 -0
  119. package/src/worker/createSharedWorker.ts +67 -0
  120. package/src/worker/dedicated.worker.ts +13 -0
  121. package/src/worker/shared.worker.ts +17 -0
  122. package/dist/OPFSFacade.d.ts.map +0 -1
  123. package/dist/OPFSWorker.d.ts.map +0 -1
  124. package/dist/assets/worker.entry-DUlEoroc.js.map +0 -1
  125. package/dist/createOPFSWorker.d.ts +0 -17
  126. package/dist/createOPFSWorker.d.ts.map +0 -1
  127. package/dist/helpers-DNj8ZoMu.cjs +0 -4
  128. package/dist/helpers-DNj8ZoMu.cjs.map +0 -1
  129. package/dist/helpers-WY2jfbOT.js +0 -1427
  130. package/dist/helpers-WY2jfbOT.js.map +0 -1
  131. package/dist/worker.entry.d.ts +0 -2
  132. package/dist/worker.entry.d.ts.map +0 -1
  133. package/docs/api-reference.md +0 -815
  134. package/src/createOPFSWorker.ts +0 -57
  135. package/src/worker.entry.ts +0 -6
@@ -0,0 +1,248 @@
1
+ import { transfer } from 'comlink';
2
+
3
+ import { BaseOPFS } from './BaseOPFS';
4
+ import { WatchEventType } from '../types';
5
+ import { OPFSError, OperationNotSupportedError, mapDomError } from '../utils/errors';
6
+ import { withLock } from '../utils/helpers';
7
+
8
+ import type { FileOpenOptions, FileStat } from '../types';
9
+
10
+ const FD_UNSUPPORTED = 'file descriptors are not available in the async backend, use the dedicated worker backend (`createOPFSDedicated`) for positional I/O';
11
+
12
+ /**
13
+ * Async OPFS backend using the promise-based File System API
14
+ * (`getFile()` / `createWritable()`).
15
+ *
16
+ * Unlike `OPFSSync`, it does not require a dedicated worker: it works on the
17
+ * main thread and inside any worker type (including SharedWorker).
18
+ *
19
+ * Requirements and limitations:
20
+ * - Writing needs `FileSystemFileHandle.createWritable()` — Chrome, Firefox, Safari 26+.
21
+ * Reading works in older Safari too.
22
+ * - No file descriptors / positional I/O: `open`, `read`, `write`, `close`,
23
+ * `fstat`, `ftruncate` and `fsync` always throw `OperationNotSupportedError`.
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * import { OPFSAsync } from 'opfs-worker/pure';
28
+ * // or: 'opfs-worker' / 'opfs-worker/async'
29
+ *
30
+ * const fs = new OPFSAsync({ root: '/my-app' });
31
+ * await fs.writeFile('/config.json', new TextEncoder().encode('{}'));
32
+ * const config = await fs.readFile('/config.json');
33
+ * ```
34
+ */
35
+ export class OPFSAsync extends BaseOPFS {
36
+ async readFile(path: string): Promise<Uint8Array> {
37
+ await this.mount();
38
+
39
+ try {
40
+ return await withLock(path, async() => {
41
+ const fileHandle = await this.getFileHandle(path, false);
42
+ const file = await fileHandle.getFile();
43
+ const buffer = new Uint8Array(await file.arrayBuffer());
44
+
45
+ return transfer(buffer, [buffer.buffer]);
46
+ });
47
+ }
48
+ catch (error: any) {
49
+ if (error instanceof OPFSError) {
50
+ throw error;
51
+ }
52
+
53
+ throw mapDomError(error, { path, isDirectory: error?.name === 'TypeMismatchError' });
54
+ }
55
+ }
56
+
57
+ async writeFile(
58
+ path: string,
59
+ data: Uint8Array | ArrayBuffer
60
+ ): Promise<void> {
61
+ await this.mount();
62
+
63
+ const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
64
+
65
+ try {
66
+ await withLock(path, async() => {
67
+ const existed = await this.exists(path);
68
+ const fileHandle = await this.getFileHandle(path, true);
69
+
70
+ // Default createWritable() starts from an empty swap file,
71
+ // so close() atomically replaces the content (truncate + write).
72
+ await this._writeStream(fileHandle, async writable => writable.write(buffer as Uint8Array<ArrayBuffer>));
73
+
74
+ await this.notifyChange({ path, type: existed ? WatchEventType.Changed : WatchEventType.Added, isDirectory: false });
75
+ });
76
+ }
77
+ catch (error: any) {
78
+ if (error instanceof OPFSError) {
79
+ throw error;
80
+ }
81
+
82
+ throw mapDomError(error, { path, isDirectory: error?.name === 'TypeMismatchError' });
83
+ }
84
+ }
85
+
86
+ async appendFile(
87
+ path: string,
88
+ data: Uint8Array | ArrayBuffer
89
+ ): Promise<void> {
90
+ await this.mount();
91
+
92
+ const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
93
+
94
+ try {
95
+ await withLock(path, async() => {
96
+ const fileHandle = await this.getFileHandle(path, true);
97
+ const { size } = await fileHandle.getFile();
98
+
99
+ await this._writeStream(
100
+ fileHandle,
101
+ async writable => writable.write({ type: 'write', position: size, data: buffer as Uint8Array<ArrayBuffer> }),
102
+ { keepExistingData: true }
103
+ );
104
+
105
+ await this.notifyChange({ path, type: WatchEventType.Changed, isDirectory: false });
106
+ });
107
+ }
108
+ catch (error: any) {
109
+ if (error instanceof OPFSError) {
110
+ throw error;
111
+ }
112
+
113
+ throw mapDomError(error, { path, isDirectory: error?.name === 'TypeMismatchError' });
114
+ }
115
+ }
116
+
117
+ async writeStream(
118
+ path: string,
119
+ stream: ReadableStream<Uint8Array>,
120
+ onProgress?: (bytesWritten: number) => unknown
121
+ ): Promise<number> {
122
+ await this.mount();
123
+
124
+ try {
125
+ return await withLock(path, async() => {
126
+ const existed = await this.exists(path);
127
+ const fileHandle = await this.getFileHandle(path, true);
128
+ const reader = stream.getReader();
129
+ let totalBytes = 0;
130
+
131
+ try {
132
+ await this._writeStream(fileHandle, async(writable) => {
133
+ while (true) {
134
+ const { done, value: chunk } = await reader.read();
135
+
136
+ if (done) {
137
+ break;
138
+ }
139
+
140
+ await writable.write(chunk as Uint8Array<ArrayBuffer>);
141
+ totalBytes += chunk.byteLength;
142
+ await onProgress?.(totalBytes);
143
+ }
144
+ });
145
+ }
146
+ catch (error) {
147
+ await reader.cancel(error).catch(() => {});
148
+
149
+ throw error;
150
+ }
151
+ finally {
152
+ reader.releaseLock();
153
+ }
154
+
155
+ await this.notifyChange({
156
+ path,
157
+ type: existed ? WatchEventType.Changed : WatchEventType.Added,
158
+ isDirectory: false,
159
+ });
160
+
161
+ return totalBytes;
162
+ });
163
+ }
164
+ catch (error: any) {
165
+ if (error instanceof OPFSError) {
166
+ throw error;
167
+ }
168
+
169
+ throw mapDomError(error, { path, isDirectory: error?.name === 'TypeMismatchError' });
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Open a writable stream, run the write callback and commit via close().
175
+ * Aborts the stream (discarding the swap file) if the callback fails.
176
+ * @private
177
+ */
178
+ private async _writeStream(
179
+ fileHandle: FileSystemFileHandle,
180
+ write: (writable: FileSystemWritableFileStream) => Promise<void>,
181
+ options?: { keepExistingData?: boolean }
182
+ ): Promise<void> {
183
+ if (typeof fileHandle.createWritable !== 'function') {
184
+ throw new OperationNotSupportedError('createWritable() is not available in this browser (Safari supports it since version 26), use the dedicated worker backend (`createOPFSDedicated`) instead');
185
+ }
186
+
187
+ const writable = await fileHandle.createWritable(options);
188
+
189
+ try {
190
+ await write(writable);
191
+ }
192
+ catch (error) {
193
+ await writable.abort().catch(() => {});
194
+
195
+ throw error;
196
+ }
197
+
198
+ await writable.close();
199
+ }
200
+
201
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
202
+ async open(_path: string, _options?: FileOpenOptions): Promise<number> {
203
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
204
+ }
205
+
206
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
207
+ async close(_fd: number): Promise<void> {
208
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
209
+ }
210
+
211
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
212
+ async read(
213
+ _fd: number,
214
+ _buffer: Uint8Array,
215
+ _offset: number,
216
+ _length: number,
217
+ _position: number | null | undefined
218
+ ): Promise<{ bytesRead: number; buffer: Uint8Array }> {
219
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
220
+ }
221
+
222
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
223
+ async write(
224
+ _fd: number,
225
+ _buffer: Uint8Array,
226
+ _offset?: number,
227
+ _length?: number,
228
+ _position?: number | null | undefined,
229
+ _emitEvent?: boolean
230
+ ): Promise<number> {
231
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
232
+ }
233
+
234
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
235
+ async fstat(_fd: number): Promise<FileStat> {
236
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
237
+ }
238
+
239
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
240
+ async ftruncate(_fd: number, _size?: number): Promise<void> {
241
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
242
+ }
243
+
244
+ /** @throws {OperationNotSupportedError} Always — no FD support in the async backend */
245
+ async fsync(_fd: number): Promise<void> {
246
+ throw new OperationNotSupportedError(FD_UNSUPPORTED);
247
+ }
248
+ }
@@ -0,0 +1,460 @@
1
+ import { transfer } from 'comlink';
2
+
3
+ import { BaseOPFS } from './BaseOPFS';
4
+ import { WatchEventType } from '../types';
5
+ import {
6
+ AlreadyExistsError,
7
+ OPFSError,
8
+ ValidationError,
9
+ createFDError,
10
+ mapDomError
11
+ } from '../utils/errors';
12
+
13
+ import {
14
+ calculateReadLength,
15
+ createSyncHandleSafe,
16
+ normalizePath,
17
+ resolvePath,
18
+ safeCloseSyncHandle,
19
+ validateReadWriteArgs,
20
+ withLock
21
+ } from '../utils/helpers';
22
+
23
+ import type { FileOpenOptions, FileStat, OPFSOptions } from '../types';
24
+
25
+ /**
26
+ * Sync OPFS backend using `FileSystemSyncAccessHandle` (dedicated worker only).
27
+ *
28
+ * Provides high-level file I/O and Node-like file descriptors.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * import { OPFSSync } from 'opfs-worker/pure';
33
+ *
34
+ * const fs = new OPFSSync();
35
+ * await fs.writeFile('/data/config.json', new TextEncoder().encode(JSON.stringify({ theme: 'dark' })));
36
+ * const config = await fs.readFile('/data/config.json');
37
+ * ```
38
+ */
39
+ export class OPFSSync extends BaseOPFS {
40
+ /** Shared sync handles per path (OPFS allows only one handle per file) */
41
+ private openHandles = new Map<string, {
42
+ fileHandle: FileSystemFileHandle;
43
+ syncHandle: FileSystemSyncAccessHandle;
44
+ refCount: number;
45
+ }>();
46
+
47
+ /** Map of open file descriptors to their metadata */
48
+ private openFiles = new Map<number, {
49
+ path: string;
50
+ fileHandle: FileSystemFileHandle;
51
+ syncHandle: FileSystemSyncAccessHandle;
52
+ position: number;
53
+ }>();
54
+
55
+ /** Next available file descriptor number */
56
+ private nextFd = 1;
57
+
58
+ constructor(options?: OPFSOptions) {
59
+ super(options);
60
+ }
61
+
62
+ /**
63
+ * Get file info by descriptor with validation
64
+ * @private
65
+ */
66
+ private _getFileDescriptor(fd: number): { path: string; fileHandle: FileSystemFileHandle; syncHandle: FileSystemSyncAccessHandle; position: number } {
67
+ const fileInfo = this.openFiles.get(fd);
68
+
69
+ if (!fileInfo) {
70
+ throw new ValidationError('descriptor', `Invalid file descriptor: ${ fd }`);
71
+ }
72
+
73
+ return fileInfo;
74
+ }
75
+
76
+ async readFile(path: string): Promise<Uint8Array> {
77
+ await this.mount();
78
+
79
+ try {
80
+ return await withLock(path, async() => {
81
+ const fd = await this.open(path);
82
+
83
+ try {
84
+ const { size } = await this.fstat(fd);
85
+ const buffer = new Uint8Array(size);
86
+
87
+ if (size > 0) {
88
+ await this.read(fd, buffer, 0, size, 0);
89
+ }
90
+
91
+ return transfer(buffer, [buffer.buffer]);
92
+ }
93
+ finally {
94
+ await this.close(fd);
95
+ }
96
+ });
97
+ }
98
+ catch (err) {
99
+ if (err instanceof OPFSError) {
100
+ throw err;
101
+ }
102
+
103
+ throw mapDomError(err, { path, isDirectory: false });
104
+ }
105
+ }
106
+
107
+ async writeFile(
108
+ path: string,
109
+ data: Uint8Array | ArrayBuffer
110
+ ): Promise<void> {
111
+ await this.mount();
112
+
113
+ const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
114
+
115
+ await withLock(path, async() => {
116
+ const existed = await this.exists(path);
117
+ const fd = await this.open(path, { create: true, truncate: true });
118
+
119
+ try {
120
+ await this.write(fd, buffer, 0, buffer.length, null, false);
121
+ await this.fsync(fd);
122
+ }
123
+ finally {
124
+ await this.close(fd);
125
+ }
126
+
127
+ await this.notifyChange({ path, type: existed ? WatchEventType.Changed : WatchEventType.Added, isDirectory: false });
128
+ });
129
+ }
130
+
131
+ async appendFile(
132
+ path: string,
133
+ data: Uint8Array | ArrayBuffer
134
+ ): Promise<void> {
135
+ await this.mount();
136
+
137
+ const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
138
+
139
+ await withLock(path, async() => {
140
+ const fd = await this.open(path, { create: true });
141
+
142
+ try {
143
+ const { size } = await this.fstat(fd);
144
+
145
+ await this.write(fd, buffer, 0, buffer.length, size, false);
146
+ await this.fsync(fd);
147
+ }
148
+ finally {
149
+ await this.close(fd);
150
+ }
151
+
152
+ await this.notifyChange({ path, type: WatchEventType.Changed, isDirectory: false });
153
+ });
154
+ }
155
+
156
+ async writeStream(
157
+ path: string,
158
+ stream: ReadableStream<Uint8Array>,
159
+ onProgress?: (bytesWritten: number) => unknown
160
+ ): Promise<number> {
161
+ await this.mount();
162
+
163
+ return withLock(path, async() => {
164
+ const existed = await this.exists(path);
165
+ const fd = await this.open(path, { create: true, truncate: true });
166
+ const reader = stream.getReader();
167
+ let totalBytes = 0;
168
+
169
+ try {
170
+ while (true) {
171
+ const { done, value: chunk } = await reader.read();
172
+
173
+ if (done) {
174
+ break;
175
+ }
176
+
177
+ let offset = 0;
178
+
179
+ while (offset < chunk.byteLength) {
180
+ offset += await this.write(
181
+ fd,
182
+ chunk,
183
+ offset,
184
+ chunk.byteLength - offset,
185
+ null,
186
+ false
187
+ );
188
+ }
189
+
190
+ totalBytes += chunk.byteLength;
191
+ await onProgress?.(totalBytes);
192
+ }
193
+
194
+ await this.fsync(fd);
195
+ }
196
+ catch (error) {
197
+ await reader.cancel(error).catch(() => {});
198
+
199
+ throw error;
200
+ }
201
+ finally {
202
+ reader.releaseLock();
203
+ await this.close(fd);
204
+ }
205
+
206
+ await this.notifyChange({
207
+ path,
208
+ type: existed ? WatchEventType.Changed : WatchEventType.Added,
209
+ isDirectory: false,
210
+ });
211
+
212
+ return totalBytes;
213
+ });
214
+ }
215
+
216
+ async open(path: string, options?: FileOpenOptions): Promise<number> {
217
+ await this.mount();
218
+
219
+ const { create = false, exclusive = false, truncate = false } = options || {};
220
+
221
+ // Normalize path to prevent path-related issues
222
+ const normalizedPath = normalizePath(resolvePath(path));
223
+
224
+ try {
225
+ // Use lock for atomic operations when creating files
226
+ if (create && exclusive) {
227
+ return await withLock(normalizedPath, async() => {
228
+ const exists = await this.exists(normalizedPath);
229
+
230
+ if (exists) {
231
+ throw new AlreadyExistsError(normalizedPath);
232
+ }
233
+
234
+ return this._openFile(normalizedPath, create, truncate);
235
+ });
236
+ }
237
+
238
+ return await this._openFile(normalizedPath, create, truncate);
239
+ }
240
+ catch (error: any) {
241
+ if (error instanceof OPFSError) {
242
+ throw error;
243
+ }
244
+
245
+ // TypeMismatchError here means the path actually refers to a directory
246
+ // so we map it as a directory-type error (EISDIR) for better Node.js parity.
247
+ const isTypeMismatchDirectory = error && error.name === 'TypeMismatchError';
248
+
249
+ throw mapDomError(error, {
250
+ path: normalizedPath,
251
+ isDirectory: !!isTypeMismatchDirectory,
252
+ });
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Internal method to open a file (without locking)
258
+ *
259
+ * Multiple FDs for the same path share one sync access handle (OPFS limit),
260
+ * with independent per-FD positions — similar to Node.js.
261
+ * @private
262
+ */
263
+ private async _openFile(path: string, create: boolean, truncate: boolean): Promise<number> {
264
+ let shared = this.openHandles.get(path);
265
+
266
+ if (!shared) {
267
+ const fileHandle = await this.getFileHandle(path, create);
268
+
269
+ // Verify that we got a file handle, not a directory
270
+ try {
271
+ await fileHandle.getFile();
272
+ }
273
+ catch (error: any) {
274
+ throw mapDomError(error, { path, isDirectory: true });
275
+ }
276
+
277
+ const syncHandle = await createSyncHandleSafe(fileHandle, path);
278
+
279
+ shared = { fileHandle, syncHandle, refCount: 0 };
280
+ this.openHandles.set(path, shared);
281
+ }
282
+
283
+ shared.refCount++;
284
+
285
+ if (truncate) {
286
+ shared.syncHandle.truncate(0);
287
+ shared.syncHandle.flush();
288
+ }
289
+
290
+ const fd = this.nextFd++;
291
+
292
+ this.openFiles.set(fd, {
293
+ path,
294
+ fileHandle: shared.fileHandle,
295
+ syncHandle: shared.syncHandle,
296
+ position: 0,
297
+ });
298
+
299
+ return fd;
300
+ }
301
+
302
+ async close(fd: number): Promise<void> {
303
+ const fileInfo = this._getFileDescriptor(fd);
304
+
305
+ this.openFiles.delete(fd);
306
+
307
+ const shared = this.openHandles.get(fileInfo.path);
308
+
309
+ if (!shared) {
310
+ return;
311
+ }
312
+
313
+ shared.refCount--;
314
+
315
+ // Only close the underlying sync handle when the last FD for this path is gone
316
+ if (shared.refCount <= 0) {
317
+ safeCloseSyncHandle(fd, shared.syncHandle, fileInfo.path);
318
+ this.openHandles.delete(fileInfo.path);
319
+ }
320
+ }
321
+
322
+ async read(
323
+ fd: number,
324
+ buffer: Uint8Array,
325
+ offset: number,
326
+ length: number,
327
+ position: number | null | undefined
328
+ ): Promise<{ bytesRead: number; buffer: Uint8Array }> {
329
+ const fileInfo = this._getFileDescriptor(fd);
330
+
331
+ // Validate arguments
332
+ validateReadWriteArgs(buffer.length, offset, length, position);
333
+
334
+ try {
335
+ const readPosition = position ?? fileInfo.position;
336
+
337
+ // Get file size and calculate read length
338
+ const fileSize = fileInfo.syncHandle.getSize();
339
+ const { isEOF, actualLength } = calculateReadLength(readPosition, length, fileSize);
340
+
341
+ if (isEOF) {
342
+ return transfer({ bytesRead: 0, buffer }, [buffer.buffer]); // End of file
343
+ }
344
+
345
+ // Create a subarray view for the read operation
346
+ const targetBuffer = buffer.subarray(offset, offset + actualLength);
347
+
348
+ // Perform efficient positioned read
349
+ const bytesRead = fileInfo.syncHandle.read(targetBuffer, { at: readPosition });
350
+
351
+ // Update position if position was not explicitly specified (null means use current position)
352
+ if (position == null) {
353
+ fileInfo.position = readPosition + bytesRead;
354
+ }
355
+
356
+ return transfer({ bytesRead, buffer }, [buffer.buffer]);
357
+ }
358
+ catch (error) {
359
+ throw createFDError('read', fd, fileInfo.path, error);
360
+ }
361
+ }
362
+
363
+ async write(
364
+ fd: number,
365
+ buffer: Uint8Array,
366
+ offset: number = 0,
367
+ length?: number,
368
+ position?: number | null | undefined,
369
+ emitEvent: boolean = true
370
+ ): Promise<number> {
371
+ const fileInfo = this._getFileDescriptor(fd);
372
+
373
+ // Calculate actual length to write
374
+ const actualLength = length ?? (buffer.length - offset);
375
+
376
+ // Validate arguments using helper
377
+ validateReadWriteArgs(buffer.length, offset, actualLength, position);
378
+
379
+ try {
380
+ // Determine write position: use specified position, or current position if null/undefined
381
+ const writePosition = position ?? fileInfo.position;
382
+
383
+ // Create a subarray view for the write operation
384
+ const sourceBuffer = buffer.subarray(offset, offset + actualLength);
385
+
386
+ // Perform efficient positioned write
387
+ const bytesWritten = fileInfo.syncHandle.write(sourceBuffer, { at: writePosition });
388
+
389
+ // Update position if position was null or undefined (i.e., use current position)
390
+ // Also update position when writing at current position (position === fileInfo.position)
391
+ if (position == null || position === fileInfo.position) {
392
+ fileInfo.position = writePosition + bytesWritten;
393
+ }
394
+
395
+ if (emitEvent) {
396
+ await this.notifyChange({ path: fileInfo.path, type: WatchEventType.Changed, isDirectory: false });
397
+ }
398
+
399
+ return bytesWritten;
400
+ }
401
+ catch (error) {
402
+ throw createFDError('write', fd, fileInfo.path, error);
403
+ }
404
+ }
405
+
406
+ async fstat(fd: number): Promise<FileStat> {
407
+ const fileInfo = this._getFileDescriptor(fd);
408
+
409
+ // Simply reuse existing stat() method with the file path
410
+ return this.stat(fileInfo.path);
411
+ }
412
+
413
+ async ftruncate(fd: number, size: number = 0): Promise<void> {
414
+ const fileInfo = this._getFileDescriptor(fd);
415
+
416
+ // Validate size parameter
417
+ if (size < 0 || !Number.isInteger(size)) {
418
+ throw new ValidationError('argument', 'Invalid size');
419
+ }
420
+
421
+ try {
422
+ fileInfo.syncHandle.truncate(size);
423
+ fileInfo.syncHandle.flush();
424
+
425
+ // Adjust position if it's beyond the new file size
426
+ if (fileInfo.position > size) {
427
+ fileInfo.position = size;
428
+ }
429
+
430
+ await this.notifyChange({ path: fileInfo.path, type: WatchEventType.Changed, isDirectory: false });
431
+ }
432
+ catch (error) {
433
+ throw createFDError('truncate', fd, fileInfo.path, error);
434
+ }
435
+ }
436
+
437
+ async fsync(fd: number): Promise<void> {
438
+ const fileInfo = this._getFileDescriptor(fd);
439
+
440
+ try {
441
+ fileInfo.syncHandle.flush();
442
+ }
443
+ catch (error) {
444
+ throw createFDError('sync', fd, fileInfo.path, error);
445
+ }
446
+ }
447
+
448
+ override dispose(): void {
449
+ // Close each shared sync handle once (not per FD)
450
+ for (const [path, shared] of this.openHandles) {
451
+ safeCloseSyncHandle(-1, shared.syncHandle, path);
452
+ }
453
+
454
+ this.openHandles.clear();
455
+ this.openFiles.clear();
456
+ this.nextFd = 1;
457
+
458
+ super.dispose();
459
+ }
460
+ }