opfs-worker 1.3.2 → 2.0.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 (65) hide show
  1. package/README.md +89 -106
  2. package/dist/{facade.d.ts → OPFSFacade.d.ts} +22 -5
  3. package/dist/OPFSFacade.d.ts.map +1 -0
  4. package/dist/{worker.d.ts → OPFSWorker.d.ts} +10 -6
  5. package/dist/OPFSWorker.d.ts.map +1 -0
  6. package/dist/assets/worker.entry-DUlEoroc.js.map +1 -0
  7. package/dist/createOPFSWorker.d.ts +17 -0
  8. package/dist/createOPFSWorker.d.ts.map +1 -0
  9. package/dist/helpers-DNj8ZoMu.cjs +4 -0
  10. package/dist/helpers-DNj8ZoMu.cjs.map +1 -0
  11. package/dist/{helpers-Ca2c767K.js → helpers-WY2jfbOT.js} +221 -221
  12. package/dist/helpers-WY2jfbOT.js.map +1 -0
  13. package/dist/index.cjs +296 -285
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.ts +3 -9
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +369 -340
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.pure.cjs +2 -0
  20. package/dist/index.pure.cjs.map +1 -0
  21. package/dist/index.pure.d.ts +6 -0
  22. package/dist/index.pure.d.ts.map +1 -0
  23. package/dist/{raw.js → index.pure.js} +241 -186
  24. package/dist/index.pure.js.map +1 -0
  25. package/dist/types.d.ts +1 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/utils/helpers.d.ts +11 -1
  28. package/dist/utils/helpers.d.ts.map +1 -1
  29. package/dist/worker.entry.d.ts +2 -0
  30. package/dist/worker.entry.d.ts.map +1 -0
  31. package/docs/api-reference.md +815 -0
  32. package/docs/file-descriptors.md +696 -0
  33. package/docs/types.md +232 -0
  34. package/package.json +14 -8
  35. package/src/OPFSFacade.ts +460 -0
  36. package/src/OPFSWorker.ts +1544 -0
  37. package/src/createOPFSWorker.ts +57 -0
  38. package/src/index.pure.ts +7 -0
  39. package/src/index.ts +15 -0
  40. package/src/types.ts +99 -0
  41. package/src/utils/encoder.ts +205 -0
  42. package/src/utils/errors.ts +297 -0
  43. package/src/utils/helpers.ts +465 -0
  44. package/src/worker.entry.ts +6 -0
  45. package/dist/assets/worker-1Wh1cr7P.js.map +0 -1
  46. package/dist/assets/worker-BeJaVyBV.js.map +0 -1
  47. package/dist/assets/worker-DYYLzR1Y.js.map +0 -1
  48. package/dist/facade.d.ts.map +0 -1
  49. package/dist/helpers-BuGfPAg0.js +0 -1439
  50. package/dist/helpers-BuGfPAg0.js.map +0 -1
  51. package/dist/helpers-CF7A2WQG.cjs +0 -4
  52. package/dist/helpers-CF7A2WQG.cjs.map +0 -1
  53. package/dist/helpers-CIiblZ8d.cjs +0 -4
  54. package/dist/helpers-CIiblZ8d.cjs.map +0 -1
  55. package/dist/helpers-CKOebsbw.js +0 -1423
  56. package/dist/helpers-CKOebsbw.js.map +0 -1
  57. package/dist/helpers-Ca2c767K.js.map +0 -1
  58. package/dist/helpers-Dwc92hv9.cjs +0 -4
  59. package/dist/helpers-Dwc92hv9.cjs.map +0 -1
  60. package/dist/raw.cjs +0 -2
  61. package/dist/raw.cjs.map +0 -1
  62. package/dist/raw.d.ts +0 -13
  63. package/dist/raw.d.ts.map +0 -1
  64. package/dist/raw.js.map +0 -1
  65. package/dist/worker.d.ts.map +0 -1
@@ -0,0 +1,57 @@
1
+ import { wrap } from 'comlink';
2
+
3
+ import WorkerCtor from './worker.entry?worker&inline';
4
+
5
+ import type { OPFSOptions, RemoteOPFSWorker } from './types';
6
+
7
+ const InlineWorker = WorkerCtor as new (options?: WorkerOptions) => Worker;
8
+
9
+ export interface RawWorker {
10
+ /** Comlink proxy to `OPFSWorker` (bytes in / bytes out) */
11
+ fs: RemoteOPFSWorker;
12
+ /** Underlying browser Worker */
13
+ worker: Worker;
14
+ /** Calls worker `dispose()` then `terminate()` */
15
+ dispose: () => void;
16
+ }
17
+
18
+ /** A `BroadcastChannel` instance can't cross the wire — send its name instead. */
19
+ function applyWorkerOptions(fs: Pick<RemoteOPFSWorker, 'setOptions'>, options?: OPFSOptions): void {
20
+ if (!options) {
21
+ return;
22
+ }
23
+
24
+ if (options.broadcastChannel instanceof BroadcastChannel) {
25
+ options.broadcastChannel = options.broadcastChannel.name;
26
+ }
27
+
28
+ void fs.setOptions(options);
29
+ }
30
+
31
+ /**
32
+ * Mode 2: spawn the inlined worker and get a Comlink proxy to `OPFSWorker`
33
+ * (bytes in / bytes out), without the facade.
34
+ *
35
+ * `OPFSFacade` is built on top of this — copy it if you want your own facade.
36
+ */
37
+ export function createOPFSWorker(options?: OPFSOptions): RawWorker {
38
+ const worker = new InlineWorker();
39
+ const fs = wrap<RemoteOPFSWorker>(worker);
40
+
41
+ applyWorkerOptions(fs, options);
42
+
43
+ return {
44
+ fs,
45
+ worker,
46
+ dispose() {
47
+ void (async() => {
48
+ try {
49
+ await fs.dispose();
50
+ }
51
+ finally {
52
+ worker.terminate();
53
+ }
54
+ })();
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,7 @@
1
+ export * from './types';
2
+ export * from './utils/errors';
3
+ export * from './utils/helpers';
4
+ export * from './utils/encoder';
5
+
6
+ // Explicit re-export shadows the type-only `OPFSWorker` coming from './types'
7
+ export { OPFSWorker } from './OPFSWorker';
package/src/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export * from './types';
2
+ export * from './utils/errors';
3
+ export * from './utils/helpers';
4
+ export * from './utils/encoder';
5
+
6
+ export {
7
+ OPFSFacade,
8
+ OPFSFileSystem,
9
+ createOPFS,
10
+ // 1.x aliases
11
+ createWorker,
12
+ } from './OPFSFacade';
13
+
14
+ export { createOPFSWorker } from './createOPFSWorker';
15
+ export type { RawWorker } from './createOPFSWorker';
package/src/types.ts ADDED
@@ -0,0 +1,99 @@
1
+ import type { OPFSWorker } from './OPFSWorker';
2
+ import type { Remote } from 'comlink';
3
+
4
+ /**
5
+ * Type for paths that can be either a string or URI
6
+ */
7
+ export type PathLike = string | URL;
8
+
9
+ export type Kind = 'file' | 'directory';
10
+
11
+ export type StringEncoding = 'ascii'
12
+ | 'utf8'
13
+ | 'utf-8'
14
+ | 'utf16le'
15
+ | 'utf-16le'
16
+ | 'ucs2'
17
+ | 'ucs-2'
18
+ | 'base64'
19
+ | 'latin1'
20
+ | 'hex';
21
+
22
+ export type BinaryEncoding = 'binary';
23
+
24
+ export type Encoding = StringEncoding | BinaryEncoding;
25
+
26
+ export interface FileStat {
27
+ kind: Kind;
28
+ size: number;
29
+ mtime: string; // ISO string
30
+ ctime: string; // ISO string
31
+ isFile: boolean;
32
+ isDirectory: boolean;
33
+ /** Hash of file content (only for files, undefined for directories) */
34
+ hash?: string;
35
+ }
36
+
37
+ export interface DirentData {
38
+ name: string;
39
+ kind: 'file' | 'directory';
40
+ isFile: boolean;
41
+ isDirectory: boolean;
42
+ }
43
+
44
+ export enum WatchEventType {
45
+ Added = 'added',
46
+ Changed = 'changed',
47
+ Removed = 'removed'
48
+ }
49
+
50
+ export interface WatchEvent {
51
+ namespace: string;
52
+ path: string;
53
+ type: WatchEventType;
54
+ isDirectory: boolean;
55
+ timestamp: string;
56
+ hash?: string;
57
+ }
58
+
59
+ export type { OPFSWorker };
60
+ export type RemoteOPFSWorker = Remote<OPFSWorker>;
61
+
62
+ export interface OPFSOptions {
63
+ /** Root path for the file system (default: '/') */
64
+ root?: string;
65
+ /** Namespace for the events (default: 'opfs-worker:${root}') */
66
+ namespace?: string;
67
+ /** Hash algorithm for file hashing, or false/null to disable (default: 'etag') */
68
+ hashAlgorithm?: null | false | 'etag' | 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
69
+ /** Maximum file size in bytes for hashing (default: 50MB) */
70
+ maxFileSize?: number;
71
+ /** Custom name for the broadcast channel (default: 'opfs-worker') */
72
+ broadcastChannel?: string | BroadcastChannel | null;
73
+ }
74
+
75
+ export interface RenameOptions {
76
+ /** Whether to overwrite existing files (default: false) */
77
+ overwrite?: boolean;
78
+ }
79
+
80
+ export interface WatchOptions {
81
+ /** Whether to watch recursively (default: true) */
82
+ recursive?: boolean;
83
+ /** Glob patterns to include in watching (minimatch syntax, default: ['**']) */
84
+ include?: string | string[];
85
+ /** Glob patterns to exclude from watching (minimatch syntax, default: []) */
86
+ exclude?: string | string[];
87
+ }
88
+
89
+ export interface FileOpenOptions {
90
+ create?: boolean;
91
+ exclusive?: boolean;
92
+ truncate?: boolean;
93
+ }
94
+
95
+ export interface WatchSnapshot {
96
+ pattern: string;
97
+ include: string[];
98
+ exclude: string[];
99
+ }
@@ -0,0 +1,205 @@
1
+ import { ValidationError } from './errors';
2
+
3
+ import type { Encoding } from '../types';
4
+
5
+
6
+ /**
7
+ * Common binary file extensions
8
+ */
9
+ export const BINARY_FILE_EXTENSIONS = [
10
+ // Images
11
+ '.jpg',
12
+ '.jpeg',
13
+ '.png',
14
+ '.gif',
15
+ '.bmp',
16
+ '.webp',
17
+ '.ico',
18
+ '.tiff',
19
+ '.tga',
20
+ // Audio
21
+ '.mp3',
22
+ '.wav',
23
+ '.ogg',
24
+ '.flac',
25
+ '.aac',
26
+ '.wma',
27
+ '.m4a',
28
+ // Video
29
+ '.mp4',
30
+ '.avi',
31
+ '.mov',
32
+ '.wmv',
33
+ '.flv',
34
+ '.webm',
35
+ '.mkv',
36
+ '.m4v',
37
+ // Documents
38
+ '.pdf',
39
+ '.doc',
40
+ '.docx',
41
+ '.xls',
42
+ '.xlsx',
43
+ '.ppt',
44
+ '.pptx',
45
+ // Archives
46
+ '.zip',
47
+ '.rar',
48
+ '.7z',
49
+ '.tar',
50
+ '.gz',
51
+ '.bz2',
52
+ // Executables
53
+ '.exe',
54
+ '.dll',
55
+ '.so',
56
+ '.dylib',
57
+ // Other binary formats
58
+ '.dat',
59
+ '.db',
60
+ '.sqlite',
61
+ '.bin',
62
+ '.obj',
63
+ '.fbx',
64
+ '.3ds',
65
+ ] as const;
66
+
67
+ /**
68
+ * Check if a file extension indicates a binary file
69
+ *
70
+ * @param path - The file path or filename
71
+ * @returns True if the file extension suggests binary content
72
+ *
73
+ * @example
74
+ * ```typescript
75
+ * isBinaryFileExtension('/path/to/image.jpg'); // true
76
+ * isBinaryFileExtension('/path/to/document.txt'); // false
77
+ * isBinaryFileExtension('data.bin'); // true
78
+ * isBinaryFileExtension('data'); // true
79
+ * ```
80
+ */
81
+ export function isBinaryFileExtension(path: string): boolean {
82
+ const i = path.lastIndexOf('.');
83
+
84
+ if (i <= 0) {
85
+ return true;
86
+ }
87
+
88
+ const ext = path.slice(i).toLowerCase();
89
+
90
+ return BINARY_FILE_EXTENSIONS.includes(ext as any);
91
+ }
92
+
93
+ export function encodeString(data: string, encoding: Encoding = 'utf-8'): Uint8Array {
94
+ switch (encoding) {
95
+ case 'utf8':
96
+ case 'utf-8':
97
+ return new TextEncoder().encode(data);
98
+
99
+ case 'utf16le':
100
+ case 'utf-16le':
101
+ case 'ucs2':
102
+ case 'ucs-2':
103
+ return encodeUtf16LE(data);
104
+
105
+ case 'ascii':
106
+ return encodeAscii(data);
107
+
108
+ case 'latin1':
109
+ return encodeLatin1(data);
110
+
111
+ case 'binary':
112
+ return Uint8Array.from(data, char => char.charCodeAt(0));
113
+
114
+ case 'base64':
115
+ return Uint8Array.from(atob(data), c => c.charCodeAt(0));
116
+
117
+ case 'hex':
118
+ if (!/^[\da-f]+$/i.test(data) || data.length % 2 !== 0) {
119
+ throw new ValidationError('format', 'Invalid hex string');
120
+ }
121
+
122
+ return Uint8Array.from(data.match(/.{1,2}/g)!.map(b => parseInt(b, 16)));
123
+
124
+ default:
125
+ console.warn('Encoding not supported, falling back to UTF-8');
126
+
127
+ return new TextEncoder().encode(data);
128
+ }
129
+ }
130
+
131
+ export function decodeBuffer(buffer: Uint8Array, encoding: Encoding = 'utf-8'): string {
132
+ // eslint-disable-next-line ts/switch-exhaustiveness-check
133
+ switch (encoding) {
134
+ case 'utf8':
135
+ case 'utf-8':
136
+ return new TextDecoder().decode(buffer);
137
+
138
+ case 'utf16le':
139
+ case 'utf-16le':
140
+ case 'ucs2':
141
+ case 'ucs-2':
142
+ return decodeUtf16LE(buffer);
143
+
144
+ case 'latin1':
145
+ return String.fromCharCode(...buffer);
146
+
147
+ case 'ascii':
148
+ return String.fromCharCode(...buffer.map(b => b & 0x7F));
149
+
150
+ case 'base64':
151
+ return btoa(String.fromCharCode(...buffer));
152
+
153
+ case 'hex':
154
+ return Array.from(buffer).map(b => b.toString(16).padStart(2, '0')).join('');
155
+
156
+ default:
157
+ console.warn('Unsupported encoding, falling back to UTF-8');
158
+
159
+ return new TextDecoder().decode(buffer);
160
+ }
161
+ }
162
+
163
+ function encodeUtf16LE(str: string): Uint8Array {
164
+ const buf = new Uint8Array(str.length * 2);
165
+
166
+ for (let i = 0; i < str.length; i++) {
167
+ const code = str.charCodeAt(i);
168
+
169
+ buf[(i * 2)] = code & 0xFF;
170
+ buf[(i * 2) + 1] = code >> 8;
171
+ }
172
+
173
+ return buf;
174
+ }
175
+
176
+ function decodeUtf16LE(buf: Uint8Array): string {
177
+ if (buf.length % 2 !== 0) {
178
+ console.warn('Invalid UTF-16LE buffer length, truncating last byte');
179
+ buf = buf.slice(0, buf.length - 1);
180
+ }
181
+
182
+ const codeUnits = new Uint16Array(buf.buffer, buf.byteOffset, buf.byteLength / 2);
183
+
184
+ return String.fromCharCode(...codeUnits);
185
+ }
186
+
187
+ function encodeLatin1(str: string): Uint8Array {
188
+ const buf = new Uint8Array(str.length);
189
+
190
+ for (let i = 0; i < str.length; i++) {
191
+ buf[i] = str.charCodeAt(i) & 0xFF;
192
+ }
193
+
194
+ return buf;
195
+ }
196
+
197
+ function encodeAscii(str: string): Uint8Array {
198
+ const buf = new Uint8Array(str.length);
199
+
200
+ for (let i = 0; i < str.length; i++) {
201
+ buf[i] = str.charCodeAt(i) & 0x7F;
202
+ }
203
+
204
+ return buf;
205
+ }
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Error code to numeric errno mapping (Node.js compatible)
3
+ */
4
+ const ERROR_CODE_TO_ERRNO: Record<string, number> = {
5
+ ENOENT: -2, // No such file or directory
6
+ EISDIR: -21, // Is a directory
7
+ ENOTDIR: -20, // Not a directory
8
+ EACCES: -13, // Permission denied
9
+ EEXIST: -17, // File exists
10
+ ENOTEMPTY: -39, // Directory not empty
11
+ EINVAL: -22, // Invalid argument
12
+ EIO: -5, // I/O error
13
+ ENOSPC: -28, // No space left on device
14
+ EBUSY: -16, // Device or resource busy
15
+ EINTR: -4, // Interrupted system call
16
+ ENOTSUP: -95, // Operation not supported
17
+ ERANGE: -34, // Result too large
18
+ EBADF: -9, // Bad file descriptor
19
+ EROOT: -1, // Custom: Cannot remove root directory
20
+ };
21
+
22
+ /**
23
+ * Base error class for all OPFS-related errors (Node.js SystemError compatible)
24
+ */
25
+ export class OPFSError extends Error {
26
+ public readonly errno: number;
27
+ public readonly syscall?: string;
28
+ public readonly path?: string;
29
+
30
+ constructor(
31
+ message: string,
32
+ code: string,
33
+ path?: string,
34
+ syscall?: string,
35
+ cause?: any
36
+ ) {
37
+ super(message, { cause });
38
+ this.name = code;
39
+ this.errno = ERROR_CODE_TO_ERRNO[code] || -1;
40
+ this.path = path;
41
+ this.syscall = syscall;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Error thrown when OPFS is not supported in the current browser
47
+ */
48
+ export class OPFSNotSupportedError extends OPFSError {
49
+ constructor(cause?: unknown) {
50
+ super('OPFS is not supported in this browser', 'ENOTSUP', undefined, undefined, cause);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Error thrown for invalid paths or path traversal attempts
56
+ */
57
+ export class PathError extends OPFSError {
58
+ constructor(message: string, path: string, cause?: unknown) {
59
+ super(message, 'INVALID_PATH', path, 'access', cause);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Error thrown when files or directories don't exist
65
+ */
66
+ export class ExistenceError extends OPFSError {
67
+ constructor(type: 'file' | 'directory' | 'source', path: string, cause?: unknown) {
68
+ const messages = {
69
+ file: `File not found: ${ path }`,
70
+ directory: `Directory not found: ${ path }`,
71
+ source: `Source does not exist: ${ path }`,
72
+ };
73
+
74
+ super(messages[type], 'ENOENT', path, 'access', cause);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Error thrown when permission is denied for an operation
80
+ */
81
+ export class PermissionError extends OPFSError {
82
+ constructor(path: string, operation: string, cause?: unknown) {
83
+ super(`Permission denied for ${ operation } on: ${ path }`, 'EACCES', path, operation, cause);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Error thrown when an operation fails due to insufficient storage
89
+ */
90
+ export class StorageError extends OPFSError {
91
+ constructor(message: string, path?: string, cause?: unknown) {
92
+ super(message, 'ENOSPC', path, 'write', cause);
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Error thrown when a file is busy (locked by another operation)
98
+ */
99
+ export class FileBusyError extends OPFSError {
100
+ constructor(path: string, cause?: unknown) {
101
+ super(`File is busy: ${ path }`, 'EBUSY', path, 'open', cause);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Error thrown when file/directory type expectations don't match
107
+ */
108
+ export class FileTypeError extends OPFSError {
109
+ constructor(actualType: 'file' | 'directory', path: string, cause?: unknown) {
110
+ const message = actualType === 'directory'
111
+ ? `Is a directory: ${ path }`
112
+ : `Not a directory: ${ path }`;
113
+ const code = actualType === 'directory' ? 'EISDIR' : 'ENOTDIR';
114
+
115
+ super(message, code, path, 'access', cause);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Error thrown for validation failures (invalid arguments, formats, etc.)
121
+ */
122
+ export class ValidationError extends OPFSError {
123
+ constructor(type: 'argument' | 'format' | 'descriptor' | 'overflow', message: string, path?: string, cause?: unknown) {
124
+ const codes = {
125
+ argument: 'EINVAL',
126
+ format: 'INVALID_FORMAT',
127
+ descriptor: 'EBADF',
128
+ overflow: 'ERANGE',
129
+ };
130
+
131
+ super(message, codes[type], path, 'validate', cause);
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Error thrown when an operation is aborted
137
+ */
138
+ export class OperationAbortedError extends OPFSError {
139
+ constructor(path: string, cause?: unknown) {
140
+ super(`Operation aborted: ${ path }`, 'EINTR', path, 'interrupt', cause);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Error thrown for I/O operation failures
146
+ */
147
+ export class IOError extends OPFSError {
148
+ constructor(message: string, path?: string, cause?: unknown) {
149
+ super(message, 'EIO', path, 'io', cause);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Error thrown when an operation is not supported
155
+ */
156
+ export class OperationNotSupportedError extends OPFSError {
157
+ constructor(path: string, cause?: unknown) {
158
+ super(`Operation not supported: ${ path }`, 'ENOTSUP', path, 'operation', cause);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Error thrown when directory operations fail
164
+ */
165
+ export class DirectoryOperationError extends OPFSError {
166
+ constructor(code: string, path: string, cause?: unknown) {
167
+ const messages = {
168
+ RM_FAILED: `Failed to remove entry: ${ path }`,
169
+ ENOTEMPTY: `Directory not empty: ${ path }. Use recursive option to force removal.`,
170
+ EROOT: 'Cannot remove root directory',
171
+ };
172
+
173
+ super(messages[code as keyof typeof messages] || `Directory operation failed: ${ path }`, code, path, 'unlink', cause);
174
+ }
175
+ }
176
+
177
+
178
+ /**
179
+ * Error thrown when OPFS initialization fails
180
+ */
181
+ export class InitializationFailedError extends OPFSError {
182
+ constructor(path: string, cause?: unknown) {
183
+ super('Failed to initialize OPFS', 'INIT_FAILED', path, 'init', cause);
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Error thrown when file system operations fail
189
+ */
190
+ export class FileSystemOperationError extends OPFSError {
191
+ constructor(operation: string, path: string, cause?: unknown) {
192
+ super(`Failed to ${ operation }: ${ path }`, `${ operation.toUpperCase() }_FAILED`, path, operation, cause);
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Error thrown when a file or directory already exists
198
+ */
199
+ export class AlreadyExistsError extends OPFSError {
200
+ constructor(path: string, cause?: unknown) {
201
+ super(`Destination already exists: ${ path }`, 'EEXIST', path, 'open', cause);
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Create an OPFSError with file descriptor context
207
+ *
208
+ * @param operation - The operation that failed (e.g., 'read', 'write', 'close')
209
+ * @param fd - The file descriptor number
210
+ * @param path - The file path
211
+ * @param error - The underlying error (optional)
212
+ * @returns OPFSError with appropriate context
213
+ */
214
+ export function createFDError(operation: string, fd: number, path: string, error?: any): OPFSError {
215
+ const errorCode = `${ operation.toUpperCase() }_FAILED` as 'READ_FAILED' | 'WRITE_FAILED' | 'CLOSE_FAILED';
216
+
217
+ return new OPFSError(`Failed to ${ operation } file descriptor: ${ fd }`, errorCode, path, operation, error);
218
+ }
219
+
220
+ /**
221
+ * Context for mapping DOM errors to OPFS errors
222
+ */
223
+ export interface MapDomErrorContext {
224
+ /** File path for context-specific errors */
225
+ path?: string;
226
+ /** Whether the operation involves a directory (for TypeMismatchError) */
227
+ isDirectory?: boolean;
228
+ /** Type of existence for NotFoundError (default: 'file') */
229
+ existenceType?: 'file' | 'directory';
230
+ /** When 'remove', maps InvalidModificationError to ENOTEMPTY and default to RM_FAILED */
231
+ operation?: 'remove';
232
+ }
233
+
234
+ /**
235
+ * Map DOM exceptions to OPFS error codes
236
+ *
237
+ * @param error - The DOM exception to map
238
+ * @param context - Context information for better error mapping
239
+ * @returns OPFSError with appropriate error code
240
+ */
241
+ export function mapDomError(error: any, context?: MapDomErrorContext): OPFSError {
242
+ const path = context?.path;
243
+ const isDirectory = context?.isDirectory;
244
+ const existenceType = context?.existenceType ?? 'file';
245
+ const operation = context?.operation;
246
+
247
+ switch (error.name) {
248
+ case 'InvalidStateError':
249
+ return new FileBusyError(path || 'unknown', error);
250
+
251
+ case 'QuotaExceededError':
252
+ return new StorageError(`No space left on device: ${ path || 'unknown' }`, path, error);
253
+
254
+ case 'NotFoundError':
255
+ return new ExistenceError(existenceType, path!, error);
256
+
257
+ case 'TypeMismatchError':
258
+ if (isDirectory !== undefined) {
259
+ if (isDirectory) {
260
+ return new FileTypeError('directory', path || 'unknown', error);
261
+ }
262
+ else {
263
+ return new FileTypeError('file', path || 'unknown', error);
264
+ }
265
+ }
266
+
267
+ // Fall through to default for ambiguous cases
268
+ return new ValidationError('argument', `Type mismatch: ${ path || 'unknown' }`, path, error);
269
+
270
+ case 'NotAllowedError':
271
+ case 'SecurityError':
272
+ return new PermissionError(path!, 'unknown', error);
273
+
274
+ case 'InvalidModificationError':
275
+ if (operation === 'remove') {
276
+ return new DirectoryOperationError('ENOTEMPTY', path!, error);
277
+ }
278
+
279
+ return new ValidationError('argument', `Invalid modification: ${ path || 'unknown' }`, path, error);
280
+
281
+ case 'AbortError':
282
+ return new OperationAbortedError(path || 'unknown', error);
283
+
284
+ case 'OperationError':
285
+ return new IOError(`Operation failed: ${ path || 'unknown' }`, path, error);
286
+
287
+ case 'TypeError':
288
+ return new OperationNotSupportedError(path || 'unknown', error);
289
+
290
+ default:
291
+ if (operation === 'remove') {
292
+ return new DirectoryOperationError('RM_FAILED', path!, error);
293
+ }
294
+
295
+ return new IOError(`I/O error: ${ path || 'unknown' }`, path, error);
296
+ }
297
+ }