opfs-worker 2.1.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 (78) hide show
  1. package/README.md +40 -27
  2. package/dist/{BaseOPFS-Cylz6ijk.js → BaseOPFS-BusBkyJX.js} +436 -358
  3. package/dist/BaseOPFS-BusBkyJX.js.map +1 -0
  4. package/dist/{BaseOPFS-BPw9AIQ-.cjs → BaseOPFS-CYwrD7-q.cjs} +4 -4
  5. package/dist/BaseOPFS-CYwrD7-q.cjs.map +1 -0
  6. package/dist/{OPFSAsync-s2P7y3sw.cjs → OPFSAsync-B2dR-2VR.cjs} +2 -2
  7. package/dist/{OPFSAsync-s2P7y3sw.cjs.map → OPFSAsync-B2dR-2VR.cjs.map} +1 -1
  8. package/dist/{OPFSAsync-BKSAJJeh.js → OPFSAsync-CO44W23e.js} +2 -2
  9. package/dist/{OPFSAsync-BKSAJJeh.js.map → OPFSAsync-CO44W23e.js.map} +1 -1
  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-C7KBepe_.cjs → OPFSSync-BPrNtvFS.cjs} +2 -2
  15. package/dist/{OPFSSync-C7KBepe_.cjs.map → OPFSSync-BPrNtvFS.cjs.map} +1 -1
  16. package/dist/{OPFSSync-CsSQkpk5.js → OPFSSync-D7obTsl3.js} +2 -2
  17. package/dist/{OPFSSync-CsSQkpk5.js.map → OPFSSync-D7obTsl3.js.map} +1 -1
  18. package/dist/assets/dedicated.worker-DW6UqFo0.js.map +1 -0
  19. package/dist/core/BaseOPFS.d.ts +38 -7
  20. package/dist/core/BaseOPFS.d.ts.map +1 -1
  21. package/dist/createOPFSAsync-DdhJYoC0.js +10 -0
  22. package/dist/{createOPFSAsync-Y4Mg6qte.js.map → createOPFSAsync-DdhJYoC0.js.map} +1 -1
  23. package/dist/createOPFSAsync-DyWpvmL-.cjs +2 -0
  24. package/dist/{createOPFSAsync-BwKDmHsy.cjs.map → createOPFSAsync-DyWpvmL-.cjs.map} +1 -1
  25. package/dist/{createOPFSDedicated-CsnV6MLP.js → createOPFSDedicated-BwTc0NV5.js} +289 -215
  26. package/dist/{createOPFSDedicated-CsnV6MLP.js.map → createOPFSDedicated-BwTc0NV5.js.map} +1 -1
  27. package/dist/{createOPFSDedicated-pKTRWLdz.cjs → createOPFSDedicated-CEeRexom.cjs} +286 -212
  28. package/dist/{createOPFSDedicated-pKTRWLdz.cjs.map → createOPFSDedicated-CEeRexom.cjs.map} +1 -1
  29. package/dist/{createOPFSShared-CGdd__PH.js → createOPFSShared-BSFWsoLe.js} +2 -2
  30. package/dist/{createOPFSShared-CGdd__PH.js.map → createOPFSShared-BSFWsoLe.js.map} +1 -1
  31. package/dist/{createOPFSShared-CskROt7U.cjs → createOPFSShared-BoCft_BS.cjs} +2 -2
  32. package/dist/{createOPFSShared-CskROt7U.cjs.map → createOPFSShared-BoCft_BS.cjs.map} +1 -1
  33. package/dist/dedicated.worker.js +282 -208
  34. package/dist/dedicated.worker.js.map +1 -1
  35. package/dist/facade/OPFSFacade.d.ts +21 -3
  36. package/dist/facade/OPFSFacade.d.ts.map +1 -1
  37. package/dist/index.async.cjs +1 -1
  38. package/dist/index.async.js +4 -4
  39. package/dist/index.cjs +1 -1
  40. package/dist/index.js +7 -7
  41. package/dist/index.pure.cjs +1 -1
  42. package/dist/index.pure.js +3 -3
  43. package/dist/index.sharedworker.cjs +1 -1
  44. package/dist/index.sharedworker.js +4 -4
  45. package/dist/index.sync.cjs +1 -1
  46. package/dist/index.sync.js +4 -4
  47. package/dist/shared.worker.js +430 -356
  48. package/dist/shared.worker.js.map +1 -1
  49. package/dist/types.d.ts +33 -0
  50. package/dist/types.d.ts.map +1 -1
  51. package/docs/api/backend.md +3 -3
  52. package/docs/api/create.md +1 -1
  53. package/docs/api/facade.md +28 -27
  54. package/docs/choosing-a-mode.md +6 -6
  55. package/docs/file-descriptors.md +46 -604
  56. package/docs/guides/async.md +2 -2
  57. package/docs/guides/dedicated.md +5 -5
  58. package/docs/guides/hashing.md +2 -2
  59. package/docs/guides/pure.md +3 -3
  60. package/docs/guides/sharedworker.md +1 -1
  61. package/docs/guides/streaming.md +145 -11
  62. package/docs/guides/watching.md +3 -3
  63. package/docs/migration.md +3 -3
  64. package/docs/types.md +92 -154
  65. package/package.json +12 -2
  66. package/src/core/BaseOPFS.ts +148 -25
  67. package/src/facade/OPFSFacade.ts +51 -4
  68. package/src/types.ts +37 -0
  69. package/dist/BaseOPFS-BPw9AIQ-.cjs.map +0 -1
  70. package/dist/BaseOPFS-Cylz6ijk.js.map +0 -1
  71. package/dist/OPFSFacade-DePnNRgt.cjs +0 -2
  72. package/dist/OPFSFacade-DePnNRgt.cjs.map +0 -1
  73. package/dist/OPFSFacade-XohWlHuu.js +0 -281
  74. package/dist/OPFSFacade-XohWlHuu.js.map +0 -1
  75. package/dist/assets/dedicated.worker-nY3tK4zi.js.map +0 -1
  76. package/dist/createOPFSAsync-BwKDmHsy.cjs +0 -2
  77. package/dist/createOPFSAsync-Y4Mg6qte.js +0 -10
  78. package/docs/README.md +0 -30
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opfs-worker",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "A robust TypeScript library for working with Origin Private File System (OPFS) through Web Workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -66,7 +66,7 @@
66
66
  "prepublishOnly": "bun run lint && bun run type-check && bun run build",
67
67
  "changelog": "changeset",
68
68
  "version-packages": "changeset version",
69
- "release": "changeset publish"
69
+ "release": "bun run scripts/release.mjs"
70
70
  },
71
71
  "keywords": [
72
72
  "opfs",
@@ -89,12 +89,21 @@
89
89
  "homepage": "https://github.com/kachurun/opfs-worker#readme",
90
90
  "devDependencies": {
91
91
  "@changesets/cli": "^2.29.5",
92
+ "@codemirror/lang-cpp": "6.0.3",
92
93
  "@codemirror/lang-css": "6.3.1",
94
+ "@codemirror/lang-go": "6.0.1",
93
95
  "@codemirror/lang-html": "6.4.11",
96
+ "@codemirror/lang-java": "6.0.2",
94
97
  "@codemirror/lang-javascript": "6.2.5",
95
98
  "@codemirror/lang-json": "6.0.2",
96
99
  "@codemirror/lang-markdown": "6.5.1",
100
+ "@codemirror/lang-php": "6.0.2",
101
+ "@codemirror/lang-python": "6.2.1",
102
+ "@codemirror/lang-rust": "6.0.2",
103
+ "@codemirror/lang-sql": "6.10.0",
97
104
  "@codemirror/lang-xml": "6.1.0",
105
+ "@codemirror/lang-yaml": "6.1.3",
106
+ "@codemirror/legacy-modes": "6.5.3",
98
107
  "@codemirror/theme-one-dark": "6.1.3",
99
108
  "@flexbe/eslint-config": "^1.0.11",
100
109
  "@tailwindcss/vite": "4.3.3",
@@ -127,6 +136,7 @@
127
136
  "packageManager": "bun@1.2.19",
128
137
  "dependencies": {
129
138
  "comlink": "^4.4.2",
139
+ "fflate": "0.8.3",
130
140
  "minimatch": "^10.0.3"
131
141
  },
132
142
  "publishConfig": {
@@ -16,7 +16,6 @@ import {
16
16
  basename,
17
17
  calculateFileHash,
18
18
  checkOPFSSupport,
19
- convertBlobToUint8Array,
20
19
  dirname,
21
20
  joinPath,
22
21
  matchMinimatch,
@@ -27,7 +26,19 @@ import {
27
26
  splitPath
28
27
  } from '../utils/helpers';
29
28
 
30
- import type { DirentData, FileStat, OPFSOptions, RenameOptions, WatchEvent, WatchOptions, WatchSnapshot } from '../types';
29
+ import type {
30
+ DirentData,
31
+ FileStat,
32
+ ImportFileData,
33
+ ImportFilesEntries,
34
+ ImportFilesProgress,
35
+ ImportFilesResult,
36
+ OPFSOptions,
37
+ RenameOptions,
38
+ WatchEvent,
39
+ WatchOptions,
40
+ WatchSnapshot,
41
+ } from '../types';
31
42
 
32
43
 
33
44
  /**
@@ -891,42 +902,105 @@ export abstract class BaseOPFS {
891
902
  }
892
903
 
893
904
  /**
894
- * Bulk-create files from `[path, data]` entries (strings, bytes, or Blobs).
905
+ * Read a file as a `Blob` without copying its bytes into memory.
895
906
  *
896
- * @param entries - Array of `[path, data]` tuples
897
- * @throws {OPFSError} If a write fails
907
+ * OPFS hands back a disk-backed `File`, so the browser can stream it on
908
+ * demand pass it to `URL.createObjectURL()` for `<video>` / `<audio>` /
909
+ * `<img>` and only the played range is actually read. Structured clone keeps
910
+ * the reference intact when the file system runs in a worker.
911
+ *
912
+ * @param path - Path to the file
913
+ * @returns Lazy `File` handle backed by OPFS storage
914
+ * @throws {OPFSError} If the path is missing or is a directory
898
915
  *
899
916
  * @example
900
917
  * ```typescript
901
- * await fs.createIndex([
902
- * ['/config.json', JSON.stringify({ theme: 'dark' })],
903
- * ['/data/binary.dat', new Uint8Array([1, 2, 3, 4])],
904
- * ['/upload.txt', new Blob(['file content'], { type: 'text/plain' })],
905
- * ]);
918
+ * const blob = await fs.readBlob('/media/clip.mp4');
919
+ * video.src = URL.createObjectURL(blob);
906
920
  * ```
907
921
  */
908
- async createIndex(entries: [string, string | Uint8Array | Blob][]): Promise<void> {
922
+ async readBlob(path: string): Promise<Blob> {
909
923
  await this.mount();
910
924
 
911
925
  try {
912
- for (const [path, data] of entries) {
913
- const normalizedPath = normalizePath(path);
926
+ const fileHandle = await this.getFileHandle(path, false);
914
927
 
915
- let fileData: Uint8Array;
928
+ return await fileHandle.getFile();
929
+ }
930
+ catch (error: any) {
931
+ if (error instanceof OPFSError) {
932
+ throw error;
933
+ }
916
934
 
917
- if (data instanceof Blob) {
918
- fileData = await convertBlobToUint8Array(data);
919
- }
920
- else if (typeof data === 'string') {
921
- // Convert string to Uint8Array using UTF-8 encoding
922
- fileData = new TextEncoder().encode(data);
923
- }
924
- else {
925
- fileData = data;
926
- }
935
+ throw mapDomError(error, { path, isDirectory: error?.name === 'TypeMismatchError' });
936
+ }
937
+ }
938
+
939
+ /**
940
+ * Bulk-import files from `[path, data]` entries (strings, bytes, Blobs, or Files).
941
+ * Each entry is written via {@link writeStream} so large Blobs/Files are not
942
+ * fully buffered in memory.
943
+ *
944
+ * Accepts an array of tuples, a `Map`, or any iterable of `[path, data]` pairs.
945
+ *
946
+ * @param entries - Files to write
947
+ * @param onProgress - Per-chunk progress with path, index, and byte counts
948
+ * @returns Written paths, count, and total bytes
949
+ * @throws {OPFSError} If a write fails
950
+ *
951
+ * @example
952
+ * ```typescript
953
+ * const result = await fs.importFiles([
954
+ * ['/config.json', JSON.stringify({ theme: 'dark' })],
955
+ * ['/data/binary.dat', new Uint8Array([1, 2, 3, 4])],
956
+ * ['/upload.txt', fileFromInput],
957
+ * ], (p) => console.log(`${p.path}: ${p.bytesWritten}/${p.bytesTotal}`));
958
+ *
959
+ * console.log(result.count, result.bytesWritten, result.paths);
960
+ * ```
961
+ */
962
+ async importFiles(
963
+ entries: ImportFilesEntries,
964
+ onProgress?: (progress: ImportFilesProgress) => unknown
965
+ ): Promise<ImportFilesResult> {
966
+ await this.mount();
967
+
968
+ const list = [...entries].map(([path, data]) => [normalizePath(path), data] as [string, ImportFileData]);
969
+ const count = list.length;
970
+ const paths = list.map(([path]) => path);
971
+ const totalBytes = list.reduce((sum, [, data]) => sum + importEntryByteLength(data), 0);
972
+ let totalBytesWritten = 0;
927
973
 
928
- await this.writeFile(normalizedPath, fileData);
974
+ try {
975
+ for (let index = 0; index < count; index++) {
976
+ const [path, data] = list[index]!;
977
+ const { stream, size: bytesTotal } = toImportStream(data);
978
+ const base = totalBytesWritten;
979
+
980
+ const written = await this.writeStream(
981
+ path,
982
+ stream,
983
+ onProgress
984
+ ? bytesWritten => onProgress({
985
+ path,
986
+ index,
987
+ count,
988
+ bytesWritten,
989
+ bytesTotal,
990
+ totalBytesWritten: base + bytesWritten,
991
+ totalBytes,
992
+ })
993
+ : undefined
994
+ );
995
+
996
+ totalBytesWritten += written;
929
997
  }
998
+
999
+ return {
1000
+ paths,
1001
+ count,
1002
+ bytesWritten: totalBytesWritten,
1003
+ };
930
1004
  }
931
1005
  catch (error) {
932
1006
  if (error instanceof OPFSError) {
@@ -937,6 +1011,14 @@ export abstract class BaseOPFS {
937
1011
  }
938
1012
  }
939
1013
 
1014
+ /**
1015
+ * @deprecated Use {@link importFiles} instead.
1016
+ */
1017
+ async createIndex(entries: ImportFilesEntries): Promise<void> {
1018
+ warnCreateIndexDeprecated();
1019
+ await this.importFiles(entries);
1020
+ }
1021
+
940
1022
  /**
941
1023
  * Dispose of resources and clean up the file system instance
942
1024
  */
@@ -949,3 +1031,44 @@ export abstract class BaseOPFS {
949
1031
  this.watchers.clear();
950
1032
  }
951
1033
  }
1034
+
1035
+ function importEntryByteLength(data: ImportFileData): number {
1036
+ if (typeof data === 'string') {
1037
+ return new TextEncoder().encode(data).byteLength;
1038
+ }
1039
+
1040
+ if (data instanceof Blob) {
1041
+ return data.size;
1042
+ }
1043
+
1044
+ return data.byteLength;
1045
+ }
1046
+
1047
+ function toImportStream(data: ImportFileData): { stream: ReadableStream<Uint8Array>; size: number } {
1048
+ if (data instanceof Blob) {
1049
+ return { stream: data.stream(), size: data.size };
1050
+ }
1051
+
1052
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
1053
+
1054
+ return {
1055
+ size: bytes.byteLength,
1056
+ stream: new ReadableStream({
1057
+ start(controller) {
1058
+ controller.enqueue(bytes);
1059
+ controller.close();
1060
+ },
1061
+ }),
1062
+ };
1063
+ }
1064
+
1065
+ let createIndexWarned = false;
1066
+
1067
+ function warnCreateIndexDeprecated(): void {
1068
+ if (createIndexWarned) {
1069
+ return;
1070
+ }
1071
+
1072
+ createIndexWarned = true;
1073
+ console.warn('[opfs-worker] createIndex() is deprecated; use importFiles() instead');
1074
+ }
@@ -8,6 +8,9 @@ import type {
8
8
  Encoding,
9
9
  FileOpenOptions,
10
10
  FileStat,
11
+ ImportFileData,
12
+ ImportFilesProgress,
13
+ ImportFilesResult,
11
14
  OPFSApi,
12
15
  OPFSOptions,
13
16
  PathLike,
@@ -28,6 +31,17 @@ function normalizePath(path: PathLike): string {
28
31
  return path;
29
32
  }
30
33
 
34
+ let createIndexWarned = false;
35
+
36
+ function warnCreateIndexDeprecated(): void {
37
+ if (createIndexWarned) {
38
+ return;
39
+ }
40
+
41
+ createIndexWarned = true;
42
+ console.warn('[opfs-worker] createIndex() is deprecated; use importFiles() instead');
43
+ }
44
+
31
45
  /** Backend the facade talks to: an fs implementation plus its cleanup. */
32
46
  export interface OPFSBackend {
33
47
  fs: OPFSApi;
@@ -442,12 +456,45 @@ export class OPFSFacade {
442
456
  }
443
457
 
444
458
  /**
445
- * Synchronize the file system with external data
459
+ * Read a file as a lazy, disk-backed `Blob` without copying the data to memory.
460
+ *
461
+ * Ideal for `URL.createObjectURL()`: the browser streams `<video>` / `<audio>`
462
+ * on demand instead of loading the whole file first.
446
463
  */
447
- async createIndex(entries: [PathLike, string | Uint8Array | Blob][]): Promise<void> {
448
- const normalizedEntries = entries.map(([path, data]) => [normalizePath(path), data] as [string, string | Uint8Array | Blob]);
464
+ async readBlob(path: PathLike): Promise<Blob> {
465
+ return this.#fs.readBlob(normalizePath(path));
466
+ }
449
467
 
450
- return this.#fs.createIndex(normalizedEntries);
468
+ /**
469
+ * Bulk-import files from `[path, data]` entries (strings, bytes, Blobs, or Files).
470
+ * Each entry is streamed via {@link importStream} / `writeStream` so large
471
+ * Blobs/Files are not fully buffered in memory.
472
+ *
473
+ * Accepts an array of tuples, a `Map`, or any iterable of `[path, data]` pairs.
474
+ * Returns written paths, count, and total bytes.
475
+ */
476
+ async importFiles(
477
+ entries: Iterable<[PathLike, ImportFileData]> | Map<string, ImportFileData>,
478
+ options?: { onProgress?: (progress: ImportFilesProgress) => void }
479
+ ): Promise<ImportFilesResult> {
480
+ const normalizedEntries = [...entries].map(([path, data]) => [
481
+ normalizePath(path),
482
+ data,
483
+ ] as [string, ImportFileData]);
484
+
485
+ const onProgress = options?.onProgress
486
+ ? proxy((progress: ImportFilesProgress) => options.onProgress!(progress))
487
+ : undefined;
488
+
489
+ return this.#fs.importFiles(normalizedEntries, onProgress);
490
+ }
491
+
492
+ /**
493
+ * @deprecated Use {@link importFiles} instead.
494
+ */
495
+ async createIndex(entries: Iterable<[PathLike, ImportFileData]> | Map<string, ImportFileData>): Promise<void> {
496
+ warnCreateIndexDeprecated();
497
+ await this.importFiles(entries);
451
498
  }
452
499
 
453
500
  /**
package/src/types.ts CHANGED
@@ -76,6 +76,43 @@ export interface OPFSOptions {
76
76
  broadcastChannel?: string | BroadcastChannel | null;
77
77
  }
78
78
 
79
+ /** Payload accepted by {@link importFiles} for a single entry. */
80
+ export type ImportFileData = string | Uint8Array | Blob;
81
+
82
+ /**
83
+ * Entries for {@link importFiles}: an array of `[path, data]` tuples, a `Map`,
84
+ * or any iterable that yields the same pairs.
85
+ */
86
+ export type ImportFilesEntries = Iterable<[string, ImportFileData]> | Map<string, ImportFileData>;
87
+
88
+ /** Progress event fired while {@link importFiles} writes entries. */
89
+ export interface ImportFilesProgress {
90
+ /** Path of the file currently being written */
91
+ path: string;
92
+ /** 0-based index of the current file */
93
+ index: number;
94
+ /** Total number of entries in this import */
95
+ count: number;
96
+ /** Bytes written for the current file so far */
97
+ bytesWritten: number;
98
+ /** Size of the current file in bytes */
99
+ bytesTotal: number;
100
+ /** Bytes written across all files so far */
101
+ totalBytesWritten: number;
102
+ /** Sum of all entry sizes in bytes */
103
+ totalBytes: number;
104
+ }
105
+
106
+ /** Result of a finished {@link importFiles} call. */
107
+ export interface ImportFilesResult {
108
+ /** Paths written, in import order */
109
+ paths: string[];
110
+ /** Number of files imported (`paths.length`) */
111
+ count: number;
112
+ /** Total bytes written across all files */
113
+ bytesWritten: number;
114
+ }
115
+
79
116
  export interface RenameOptions {
80
117
  /** Whether to overwrite existing files (default: false) */
81
118
  overwrite?: boolean;