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,465 @@
1
+ import { minimatch } from 'minimatch';
2
+
3
+ import { encodeString } from './encoder';
4
+ import {
5
+ OPFSNotSupportedError,
6
+ ValidationError,
7
+ mapDomError
8
+ } from './errors';
9
+
10
+ import type { Encoding } from '../types';
11
+
12
+
13
+ /**
14
+ * Check if the browser supports the OPFS API
15
+ *
16
+ * @throws {OPFSNotSupportedError} If the browser does not support the OPFS API
17
+ */
18
+ export function checkOPFSSupport(): void {
19
+ if (!('storage' in navigator) || !('getDirectory' in (navigator.storage as any))) {
20
+ throw new OPFSNotSupportedError();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Run a callback while holding an exclusive lock on a path
26
+ *
27
+ * Locks are always exclusive: OPFS permits a single sync access handle per file,
28
+ * so readers can't share access either.
29
+ *
30
+ * @param path - The path to lock
31
+ * @param fn - The callback to run while holding the lock
32
+ * @returns The value returned by the callback
33
+ */
34
+ export async function withLock<T>(
35
+ path: string,
36
+ fn: () => Promise<T>
37
+ ): Promise<T> {
38
+ if (typeof navigator !== 'undefined' && navigator.locks?.request) {
39
+ return navigator.locks.request(`opfs:${ path.replace(/\/+/g, '/') }`, { mode: 'exclusive' }, fn);
40
+ }
41
+
42
+ return fn();
43
+ }
44
+
45
+ /**
46
+ * Split a path into an array of segments
47
+ *
48
+ * @param path - The path to split
49
+ * @returns The array of segments
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * splitPath('/path/to/file'); // ['path', 'to', 'file']
54
+ * splitPath('~/path/to/file'); // ['path', 'to', 'file'] (home dir handled)
55
+ * splitPath('relative/path'); // ['relative', 'path']
56
+ * ```
57
+ */
58
+ export function splitPath(path: string | string[]): string[] {
59
+ if (Array.isArray(path)) {
60
+ return path;
61
+ }
62
+
63
+ const normalizedPath = path.startsWith('~/') ? path.slice(2) : path;
64
+
65
+ return normalizedPath.split('/').filter(Boolean);
66
+ }
67
+
68
+
69
+ /**
70
+ * Join an array of path segments into a single path
71
+ *
72
+ * @param segments - The array of path segments
73
+ * @returns The joined path
74
+ */
75
+ export function joinPath(segments: string[] | string): string {
76
+ return typeof segments === 'string'
77
+ ? (segments ?? '/')
78
+ : `/${ segments.join('/') }`;
79
+ }
80
+
81
+ /**
82
+ * Extract the filename from a path
83
+ *
84
+ * @param path - The file path
85
+ * @returns The filename without the directory path
86
+ *
87
+ * @example
88
+ * ```typescript
89
+ * basename('/path/to/file.txt'); // 'file.txt'
90
+ * basename('/path/to/directory/'); // ''
91
+ * basename('file.txt'); // 'file.txt'
92
+ * ```
93
+ */
94
+ export function basename(path: string): string {
95
+ const segments = splitPath(path);
96
+
97
+ return segments[segments.length - 1] || '';
98
+ }
99
+
100
+ /**
101
+ * Extract the directory path from a file path
102
+ *
103
+ * @param path - The file path
104
+ * @returns The directory path without the filename
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * dirname('/path/to/file.txt'); // '/path/to'
109
+ * dirname('/path/to/directory/'); // '/path/to/directory'
110
+ * dirname('file.txt'); // '/'
111
+ * ```
112
+ */
113
+ export function dirname(path: string): string {
114
+ const segments = splitPath(path);
115
+
116
+ segments.pop();
117
+
118
+ return joinPath(segments);
119
+ }
120
+
121
+ /**
122
+ * Normalize a path to ensure it starts with '/'
123
+ *
124
+ * @param path - The path to normalize
125
+ * @returns The normalized path
126
+ *
127
+ * @example
128
+ * ```typescript
129
+ * normalizePath('path/to/file'); // '/path/to/file'
130
+ * normalizePath('/path/to/file'); // '/path/to/file'
131
+ * normalizePath('~/path/to/file'); // '/path/to/file' (home dir normalized to root)
132
+ * normalizePath(''); // '/'
133
+ * ```
134
+ */
135
+ export function normalizePath(path: string): string {
136
+ if (!path || path === '/') {
137
+ return '/';
138
+ }
139
+
140
+ if (path.startsWith('~/')) {
141
+ return `/${ path.slice(2) }`;
142
+ }
143
+
144
+ return path.startsWith('/') ? path : `/${ path }`;
145
+ }
146
+
147
+ export function normalizeMinimatch(path: string, recursive: boolean = false): string {
148
+ path = path.replace(/\/$/, '');
149
+ if (recursive && !path.includes('*')) {
150
+ return `${ path }/**`;
151
+ }
152
+
153
+ return path;
154
+ }
155
+
156
+ export function matchMinimatch(path: string, pattern: string): boolean {
157
+ return minimatch(path, pattern, {
158
+ dot: true,
159
+ matchBase: true,
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Check if a path matches any of the provided exclude patterns (minimatch syntax)
165
+ *
166
+ * @param path - Absolute or relative path
167
+ * @param patterns - Glob pattern(s) to match against
168
+ * @returns true if excluded, false otherwise
169
+ */
170
+ export function isPathExcluded(path: string, patterns?: string | string[]): boolean {
171
+ if (!patterns || (Array.isArray(patterns) && patterns.length === 0)) {
172
+ return false;
173
+ }
174
+
175
+ const normalized = normalizePath(path);
176
+ const list = Array.isArray(patterns) ? patterns : [patterns];
177
+
178
+ return list.some(pattern => minimatch(normalized, pattern, { dot: true }));
179
+ }
180
+
181
+ /**
182
+ * Resolve a path to an absolute path, handling relative segments
183
+ *
184
+ * @param path - The path to resolve
185
+ * @returns The resolved absolute path
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * resolvePath('./config/../data/file.txt'); // '/data/file.txt'
190
+ * resolvePath('/path/to/../file.txt'); // '/path/file.txt'
191
+ * resolvePath('../../file.txt'); // '/file.txt' (truncated to root)
192
+ * resolvePath('~/config/../data/file.txt'); // '/data/file.txt' (home dir normalized to root)
193
+ * ```
194
+ */
195
+ export function resolvePath(path: string): string {
196
+ // First normalize the path to handle home directory references
197
+ const normalizedPath = normalizePath(path);
198
+ const segments = splitPath(normalizedPath);
199
+ const normalizedSegments: string[] = [];
200
+
201
+ for (const segment of segments) {
202
+ if (segment === '.' || segment === '') {
203
+ // Skip current directory references and empty segments
204
+ continue;
205
+ }
206
+ else if (segment === '..') {
207
+ if (normalizedSegments.length === 0) {
208
+ // Path escapes root, keep at root level
209
+ continue;
210
+ }
211
+
212
+ // Go up one directory
213
+ normalizedSegments.pop();
214
+ }
215
+ else {
216
+ normalizedSegments.push(segment);
217
+ }
218
+ }
219
+
220
+ return joinPath(normalizedSegments);
221
+ }
222
+
223
+ /**
224
+ * Get the file extension from a path
225
+ *
226
+ * @param path - The file path
227
+ * @returns The file extension including the dot, or empty string if no extension
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * extname('/path/to/file.txt'); // '.txt'
232
+ * extname('/path/to/file'); // ''
233
+ * extname('/path/to/file.name.ext'); // '.ext'
234
+ * extname('/path/to/.hidden'); // ''
235
+ * ```
236
+ */
237
+ export function extname(path: string): string {
238
+ const filename = basename(path);
239
+ const lastDotIndex = filename.lastIndexOf('.');
240
+
241
+ if (lastDotIndex <= 0 || lastDotIndex === filename.length - 1) {
242
+ return '';
243
+ }
244
+
245
+ return filename.slice(lastDotIndex);
246
+ }
247
+
248
+ export function createBuffer(data: string | Uint8Array | ArrayBuffer, encoding: Encoding = 'utf-8'): Uint8Array {
249
+ if (typeof data === 'string') {
250
+ return encodeString(data, encoding);
251
+ }
252
+
253
+ return data instanceof Uint8Array ? data : new Uint8Array(data);
254
+ }
255
+
256
+ /**
257
+ * Calculate file hash using Web Crypto API
258
+ *
259
+ * @param buffer - The file content as File, ArrayBuffer, or Uint8Array
260
+ * @param algorithm - Hash algorithm to use (default: 'SHA-1')
261
+ * @param maxSize - Maximum file size in bytes. If file is larger, throws error (default: 50MB)
262
+ * @returns Promise that resolves to the hash string
263
+ * @throws Error if file size exceeds maxSize
264
+ */
265
+ export async function calculateFileHash(
266
+ buffer: File | ArrayBuffer | Uint8Array,
267
+ algorithm: string = 'SHA-1',
268
+ maxSize: number = 50 * 1024 * 1024 // 50MB default
269
+ ): Promise<string> {
270
+ if (buffer instanceof File) {
271
+ buffer = await buffer.arrayBuffer();
272
+ }
273
+
274
+ // Check file size before processing
275
+ if (buffer.byteLength > maxSize) {
276
+ throw new Error(`File size ${ buffer.byteLength } bytes exceeds maximum allowed size ${ maxSize } bytes`);
277
+ }
278
+
279
+ const bufferSource = new Uint8Array(buffer);
280
+ const hashBuffer = await crypto.subtle.digest(algorithm, bufferSource);
281
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
282
+
283
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
284
+ }
285
+
286
+ /**
287
+ * Compare two Uint8Array buffers for equality
288
+ *
289
+ * @param a - First buffer
290
+ * @param b - Second buffer
291
+ * @returns true if buffers are equal, false otherwise
292
+ */
293
+ export function buffersEqual(a: Uint8Array, b: Uint8Array): boolean {
294
+ if (a.length !== b.length) {
295
+ return false;
296
+ }
297
+
298
+ for (let i = 0; i < a.length; i++) {
299
+ if (a[i] !== b[i]) {
300
+ return false;
301
+ }
302
+ }
303
+
304
+ return true;
305
+ }
306
+
307
+ /**
308
+ * Convert a Blob to Uint8Array
309
+ *
310
+ * This function converts a Blob object to a Uint8Array for use with file operations.
311
+ * It's useful when working with file uploads or other Blob data sources.
312
+ *
313
+ * @param blob - The Blob to convert
314
+ * @returns Promise that resolves to the Uint8Array representation of the Blob
315
+ *
316
+ * @example
317
+ * ```typescript
318
+ * const fileInput = document.getElementById('file') as HTMLInputElement;
319
+ * const file = fileInput.files?.[0];
320
+ * if (file) {
321
+ * const data = await convertBlobToUint8Array(file);
322
+ * await fs.writeFile('/uploaded-file', data);
323
+ * }
324
+ * }
325
+ * ```
326
+ */
327
+ export async function convertBlobToUint8Array(blob: Blob): Promise<Uint8Array> {
328
+ const arrayBuffer = await blob.arrayBuffer();
329
+
330
+ return new Uint8Array(arrayBuffer);
331
+ }
332
+
333
+ /**
334
+ * Remove a file or directory entry using a directory handle
335
+ *
336
+ * @param parentHandle - The parent directory handle
337
+ * @param path - The full path of the entry to remove
338
+ * @param options - Remove options
339
+ * @param options.recursive - Whether to remove directories and their contents recursively
340
+ * @param options.force - Whether to ignore errors if the path doesn't exist
341
+ * @param options.useTrash - Placeholder for trash support (currently unused)
342
+ */
343
+ export async function removeEntry(
344
+ parentHandle: FileSystemDirectoryHandle,
345
+ path: string,
346
+ options: { recursive?: boolean; force?: boolean; useTrash?: boolean } = {}
347
+ ): Promise<void> {
348
+ const name = basename(path);
349
+
350
+ return withLock(path, async() => {
351
+ const recursive = options.recursive ?? false;
352
+ const force = options.force ?? false;
353
+
354
+ try {
355
+ await parentHandle.removeEntry(name, { recursive });
356
+ }
357
+ catch (e: any) {
358
+ if (e.name === 'NotFoundError' && force) {
359
+ return;
360
+ }
361
+
362
+ const isDirectory = e.name === 'TypeMismatchError' && !recursive;
363
+
364
+ throw mapDomError(e, {
365
+ path,
366
+ operation: 'remove',
367
+ isDirectory,
368
+ });
369
+ }
370
+ });
371
+ }
372
+
373
+ /**
374
+ * Validate read/write arguments for file descriptor operations
375
+ *
376
+ * @param bufferLen - Length of the buffer
377
+ * @param offset - Offset in the buffer
378
+ * @param length - Number of bytes to read/write
379
+ * @param position - Position in the file (null for current position)
380
+ * @throws {OPFSError} If arguments are invalid
381
+ */
382
+ export function validateReadWriteArgs(
383
+ bufferLen: number,
384
+ offset: number,
385
+ length: number,
386
+ position: number | null | undefined
387
+ ): void {
388
+ if (!Number.isInteger(offset) || !Number.isInteger(length)) {
389
+ throw new ValidationError('argument', 'Invalid offset or length');
390
+ }
391
+
392
+ if (offset < 0 || length < 0) {
393
+ throw new ValidationError('argument', 'Negative offset or length not allowed');
394
+ }
395
+
396
+ if (offset + length > bufferLen) {
397
+ throw new ValidationError('overflow', 'Operation would overflow buffer');
398
+ }
399
+
400
+ if (position != null && (!Number.isInteger(position) || position < 0)) {
401
+ throw new ValidationError('argument', 'Invalid position');
402
+ }
403
+ }
404
+
405
+ /**
406
+ * Safely close a file descriptor's sync handle
407
+ *
408
+ * @param fd - The file descriptor number (for logging)
409
+ * @param syncHandle - The sync handle to close
410
+ * @param path - The file path (for logging)
411
+ */
412
+ export function safeCloseSyncHandle(fd: number, syncHandle: any, path: string): void {
413
+ try {
414
+ syncHandle.flush();
415
+ syncHandle.close();
416
+ }
417
+ catch (error) {
418
+ // Log warning but don't throw, as the fd should still be removed
419
+ console.warn(`Warning: Failed to properly close file descriptor ${ fd } (${ path }):`, error);
420
+ }
421
+ }
422
+
423
+ /**
424
+ * Check if position is at or beyond end of file and calculate actual read length
425
+ *
426
+ * @param position - The position to read from
427
+ * @param requestedLength - The requested length to read
428
+ * @param fileSize - The current file size
429
+ * @returns Object with isEOF flag and actual length to read
430
+ */
431
+ export function calculateReadLength(position: number, requestedLength: number, fileSize: number): { isEOF: boolean; actualLength: number } {
432
+ // Check if we're at or beyond end of file
433
+ if (position >= fileSize) {
434
+ return { isEOF: true, actualLength: 0 };
435
+ }
436
+
437
+ // Calculate actual length to read (don't read beyond file end)
438
+ const actualLength = Math.min(requestedLength, fileSize - position);
439
+
440
+ if (actualLength <= 0) {
441
+ return { isEOF: true, actualLength: 0 };
442
+ }
443
+
444
+ return { isEOF: false, actualLength };
445
+ }
446
+
447
+ /**
448
+ * Safely create a sync access handle with proper error mapping
449
+ *
450
+ * @param fileHandle - The file handle to create sync access handle from
451
+ * @param path - The file path for error context
452
+ * @returns Promise that resolves to FileSystemSyncAccessHandle
453
+ * @throws {OPFSError} If creation fails
454
+ */
455
+ export async function createSyncHandleSafe(
456
+ fileHandle: FileSystemFileHandle,
457
+ path: string
458
+ ): Promise<FileSystemSyncAccessHandle> {
459
+ try {
460
+ return await fileHandle.createSyncAccessHandle();
461
+ }
462
+ catch (error: any) {
463
+ throw mapDomError(error, { path, isDirectory: false });
464
+ }
465
+ }
@@ -0,0 +1,6 @@
1
+ import { expose } from 'comlink';
2
+
3
+ import { OPFSWorker } from './OPFSWorker';
4
+
5
+ /** Worker entry — bundled via `?worker&inline`, not a public package entry. */
6
+ expose(new OPFSWorker());