opfs-worker 2.1.0 → 2.1.2

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 (111) hide show
  1. package/README.md +51 -37
  2. package/dist/BaseOPFS-Bu7FZtCb.cjs +2 -0
  3. package/dist/BaseOPFS-Bu7FZtCb.cjs.map +1 -0
  4. package/dist/{BaseOPFS-Cylz6ijk.js → BaseOPFS-C3NE4BKr.js} +804 -484
  5. package/dist/BaseOPFS-C3NE4BKr.js.map +1 -0
  6. package/dist/OPFSAsync-C0RsxCX5.cjs +2 -0
  7. package/dist/OPFSAsync-C0RsxCX5.cjs.map +1 -0
  8. package/dist/{OPFSAsync-BKSAJJeh.js → OPFSAsync-aeLUdIqd.js} +19 -19
  9. package/dist/OPFSAsync-aeLUdIqd.js.map +1 -0
  10. package/dist/OPFSFacade-BJv2yuWU.cjs +2 -0
  11. package/dist/OPFSFacade-BJv2yuWU.cjs.map +1 -0
  12. package/dist/OPFSFacade-DiogtQoq.js +308 -0
  13. package/dist/OPFSFacade-DiogtQoq.js.map +1 -0
  14. package/dist/OPFSSync-3qlSBYmz.cjs +2 -0
  15. package/dist/OPFSSync-3qlSBYmz.cjs.map +1 -0
  16. package/dist/{OPFSSync-CsSQkpk5.js → OPFSSync-CvSOswhW.js} +57 -57
  17. package/dist/OPFSSync-CvSOswhW.js.map +1 -0
  18. package/dist/assets/dedicated.worker-Bqqr9UBA.js.map +1 -0
  19. package/dist/core/BaseOPFS.d.ts +60 -9
  20. package/dist/core/BaseOPFS.d.ts.map +1 -1
  21. package/dist/core/OPFSAsync.d.ts.map +1 -1
  22. package/dist/core/OPFSSync.d.ts.map +1 -1
  23. package/dist/createOPFSAsync-B611B63X.js +10 -0
  24. package/dist/{createOPFSAsync-Y4Mg6qte.js.map → createOPFSAsync-B611B63X.js.map} +1 -1
  25. package/dist/createOPFSAsync-BATjJg8k.cjs +2 -0
  26. package/dist/{createOPFSAsync-BwKDmHsy.cjs.map → createOPFSAsync-BATjJg8k.cjs.map} +1 -1
  27. package/dist/createOPFSDedicated-DgudPTB8.js +2676 -0
  28. package/dist/createOPFSDedicated-DgudPTB8.js.map +1 -0
  29. package/dist/createOPFSDedicated-EmIwluQo.cjs +2597 -0
  30. package/dist/createOPFSDedicated-EmIwluQo.cjs.map +1 -0
  31. package/dist/{createOPFSShared-CGdd__PH.js → createOPFSShared-BWYln8jJ.js} +2 -2
  32. package/dist/{createOPFSShared-CGdd__PH.js.map → createOPFSShared-BWYln8jJ.js.map} +1 -1
  33. package/dist/{createOPFSShared-CskROt7U.cjs → createOPFSShared-DPJpCd7z.cjs} +2 -2
  34. package/dist/{createOPFSShared-CskROt7U.cjs.map → createOPFSShared-DPJpCd7z.cjs.map} +1 -1
  35. package/dist/dedicated.worker.js +1104 -790
  36. package/dist/dedicated.worker.js.map +1 -1
  37. package/dist/facade/OPFSFacade.d.ts +21 -3
  38. package/dist/facade/OPFSFacade.d.ts.map +1 -1
  39. package/dist/facade/createOPFSDedicated.d.ts +4 -2
  40. package/dist/facade/createOPFSDedicated.d.ts.map +1 -1
  41. package/dist/index.async.cjs +1 -1
  42. package/dist/index.async.js +33 -32
  43. package/dist/index.cjs +1 -1
  44. package/dist/index.d.ts +1 -5
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +44 -43
  47. package/dist/index.pure.cjs +1 -1
  48. package/dist/index.pure.js +38 -37
  49. package/dist/index.sharedworker.cjs +1 -1
  50. package/dist/index.sharedworker.js +34 -33
  51. package/dist/index.sync.cjs +1 -1
  52. package/dist/index.sync.d.ts +2 -0
  53. package/dist/index.sync.d.ts.map +1 -1
  54. package/dist/index.sync.js +34 -32
  55. package/dist/shared.worker.js +1101 -791
  56. package/dist/shared.worker.js.map +1 -1
  57. package/dist/types.d.ts +38 -5
  58. package/dist/types.d.ts.map +1 -1
  59. package/dist/utils/helpers.d.ts +13 -3
  60. package/dist/utils/helpers.d.ts.map +1 -1
  61. package/dist/worker/createDedicatedWorker.d.ts +8 -3
  62. package/dist/worker/createDedicatedWorker.d.ts.map +1 -1
  63. package/docs/README.md +13 -25
  64. package/docs/api/README.md +221 -0
  65. package/docs/api/file-descriptors.md +137 -0
  66. package/docs/guides/async.md +4 -4
  67. package/docs/guides/dedicated.md +10 -8
  68. package/docs/guides/hashing.md +3 -3
  69. package/docs/guides/pure.md +4 -4
  70. package/docs/guides/sharedworker.md +2 -2
  71. package/docs/guides/streaming.md +146 -12
  72. package/docs/guides/watching.md +3 -3
  73. package/docs/migration.md +10 -11
  74. package/docs/types.md +93 -155
  75. package/package.json +20 -9
  76. package/src/core/BaseOPFS.ts +194 -29
  77. package/src/core/OPFSAsync.ts +4 -5
  78. package/src/core/OPFSSync.ts +6 -7
  79. package/src/facade/OPFSFacade.ts +51 -4
  80. package/src/facade/createOPFSDedicated.ts +4 -2
  81. package/src/index.sync.ts +2 -0
  82. package/src/index.ts +1 -5
  83. package/src/types.ts +42 -3
  84. package/src/utils/helpers.ts +32 -6
  85. package/src/worker/createDedicatedWorker.ts +85 -7
  86. package/src/worker/dedicated.worker.ts +12 -1
  87. package/dist/BaseOPFS-BPw9AIQ-.cjs +0 -4
  88. package/dist/BaseOPFS-BPw9AIQ-.cjs.map +0 -1
  89. package/dist/BaseOPFS-Cylz6ijk.js.map +0 -1
  90. package/dist/OPFSAsync-BKSAJJeh.js.map +0 -1
  91. package/dist/OPFSAsync-s2P7y3sw.cjs +0 -2
  92. package/dist/OPFSAsync-s2P7y3sw.cjs.map +0 -1
  93. package/dist/OPFSFacade-DePnNRgt.cjs +0 -2
  94. package/dist/OPFSFacade-DePnNRgt.cjs.map +0 -1
  95. package/dist/OPFSFacade-XohWlHuu.js +0 -281
  96. package/dist/OPFSFacade-XohWlHuu.js.map +0 -1
  97. package/dist/OPFSSync-C7KBepe_.cjs +0 -2
  98. package/dist/OPFSSync-C7KBepe_.cjs.map +0 -1
  99. package/dist/OPFSSync-CsSQkpk5.js.map +0 -1
  100. package/dist/assets/dedicated.worker-nY3tK4zi.js.map +0 -1
  101. package/dist/createOPFSAsync-BwKDmHsy.cjs +0 -2
  102. package/dist/createOPFSAsync-Y4Mg6qte.js +0 -10
  103. package/dist/createOPFSDedicated-CsnV6MLP.js +0 -2333
  104. package/dist/createOPFSDedicated-CsnV6MLP.js.map +0 -1
  105. package/dist/createOPFSDedicated-pKTRWLdz.cjs +0 -2283
  106. package/dist/createOPFSDedicated-pKTRWLdz.cjs.map +0 -1
  107. package/docs/api/backend.md +0 -36
  108. package/docs/api/create.md +0 -82
  109. package/docs/api/facade.md +0 -52
  110. package/docs/choosing-a-mode.md +0 -41
  111. package/docs/file-descriptors.md +0 -695
@@ -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,
@@ -24,16 +23,31 @@ import {
24
23
  normalizePath,
25
24
  removeEntry,
26
25
  resolvePath,
27
- splitPath
26
+ splitPath,
27
+ withLock
28
28
  } from '../utils/helpers';
29
29
 
30
- import type { DirentData, FileStat, OPFSOptions, RenameOptions, WatchEvent, WatchOptions, WatchSnapshot } from '../types';
30
+ import type {
31
+ DirentData,
32
+ FileOpenOptions,
33
+ FileStat,
34
+ ImportFileData,
35
+ ImportFilesEntries,
36
+ ImportFilesProgress,
37
+ ImportFilesResult,
38
+ OPFSOptions,
39
+ RenameOptions,
40
+ WatchEvent,
41
+ WatchOptions,
42
+ WatchSnapshot,
43
+ } from '../types';
31
44
 
32
45
 
33
46
  /**
34
47
  * Shared OPFS logic (directories, meta, watch, high-level helpers).
35
- * File I/O backends implement {@link readFile}, {@link writeFile}, {@link appendFile},
36
- * and {@link writeStream}.
48
+ *
49
+ * Subclasses implement byte I/O and file descriptors. Together those methods
50
+ * are the {@link OPFSApi} contract used by the facade and Comlink proxies.
37
51
  */
38
52
  export abstract class BaseOPFS {
39
53
  /** Root directory handle for the file system */
@@ -124,6 +138,11 @@ export abstract class BaseOPFS {
124
138
  }
125
139
  }
126
140
 
141
+ /** Exclusive path lock keyed by absolute OPFS path (`root` + API path). */
142
+ protected async withPathLock<T>(path: string, fn: () => Promise<T>): Promise<T> {
143
+ return withLock(path, fn, this.options.root);
144
+ }
145
+
127
146
  /** Read file contents as bytes */
128
147
  abstract readFile(path: string): Promise<Uint8Array>;
129
148
 
@@ -140,6 +159,40 @@ export abstract class BaseOPFS {
140
159
  onProgress?: (bytesWritten: number) => unknown
141
160
  ): Promise<number>;
142
161
 
162
+ /** Open a file and return a file descriptor */
163
+ abstract open(path: string, options?: FileOpenOptions): Promise<number>;
164
+
165
+ /** Close a file descriptor */
166
+ abstract close(fd: number): Promise<void>;
167
+
168
+ /** Read from a file descriptor into a buffer */
169
+ abstract read(
170
+ fd: number,
171
+ buffer: Uint8Array,
172
+ offset: number,
173
+ length: number,
174
+ position: number | null | undefined
175
+ ): Promise<{ bytesRead: number; buffer: Uint8Array }>;
176
+
177
+ /** Write from a buffer to a file descriptor */
178
+ abstract write(
179
+ fd: number,
180
+ buffer: Uint8Array,
181
+ offset?: number,
182
+ length?: number,
183
+ position?: number | null | undefined,
184
+ emitEvent?: boolean
185
+ ): Promise<number>;
186
+
187
+ /** File stats by descriptor */
188
+ abstract fstat(fd: number): Promise<FileStat>;
189
+
190
+ /** Truncate a file by descriptor */
191
+ abstract ftruncate(fd: number, size?: number): Promise<void>;
192
+
193
+ /** Flush a file descriptor to storage */
194
+ abstract fsync(fd: number): Promise<void>;
195
+
143
196
  /**
144
197
  * Initialize the file system within a given directory.
145
198
  * If no root is specified, uses the OPFS root directory.
@@ -671,7 +724,7 @@ export abstract class BaseOPFS {
671
724
  const parent = await this.getDirectoryHandle(dirname(path), false);
672
725
  const stat = await this.stat(path);
673
726
 
674
- await removeEntry(parent, path, { recursive, force });
727
+ await removeEntry(parent, path, { recursive, force, root: this.options.root });
675
728
 
676
729
  await this.notifyChange({ path, type: WatchEventType.Removed, isDirectory: stat.isDirectory });
677
730
  }
@@ -891,42 +944,105 @@ export abstract class BaseOPFS {
891
944
  }
892
945
 
893
946
  /**
894
- * Bulk-create files from `[path, data]` entries (strings, bytes, or Blobs).
947
+ * Read a file as a `Blob` without copying its bytes into memory.
895
948
  *
896
- * @param entries - Array of `[path, data]` tuples
897
- * @throws {OPFSError} If a write fails
949
+ * OPFS hands back a disk-backed `File`, so the browser can stream it on
950
+ * demand pass it to `URL.createObjectURL()` for `<video>` / `<audio>` /
951
+ * `<img>` and only the played range is actually read. Structured clone keeps
952
+ * the reference intact when the file system runs in a worker.
953
+ *
954
+ * @param path - Path to the file
955
+ * @returns Lazy `File` handle backed by OPFS storage
956
+ * @throws {OPFSError} If the path is missing or is a directory
898
957
  *
899
958
  * @example
900
959
  * ```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
- * ]);
960
+ * const blob = await fs.readBlob('/media/clip.mp4');
961
+ * video.src = URL.createObjectURL(blob);
906
962
  * ```
907
963
  */
908
- async createIndex(entries: [string, string | Uint8Array | Blob][]): Promise<void> {
964
+ async readBlob(path: string): Promise<Blob> {
909
965
  await this.mount();
910
966
 
911
967
  try {
912
- for (const [path, data] of entries) {
913
- const normalizedPath = normalizePath(path);
968
+ const fileHandle = await this.getFileHandle(path, false);
914
969
 
915
- let fileData: Uint8Array;
970
+ return await fileHandle.getFile();
971
+ }
972
+ catch (error: any) {
973
+ if (error instanceof OPFSError) {
974
+ throw error;
975
+ }
916
976
 
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
- }
977
+ throw mapDomError(error, { path, isDirectory: error?.name === 'TypeMismatchError' });
978
+ }
979
+ }
927
980
 
928
- await this.writeFile(normalizedPath, fileData);
981
+ /**
982
+ * Bulk-import files from `[path, data]` entries (strings, bytes, Blobs, or Files).
983
+ * Each entry is written via {@link writeStream} so large Blobs/Files are not
984
+ * fully buffered in memory.
985
+ *
986
+ * Accepts an array of tuples, a `Map`, or any iterable of `[path, data]` pairs.
987
+ *
988
+ * @param entries - Files to write
989
+ * @param onProgress - Per-chunk progress with path, index, and byte counts
990
+ * @returns Written paths, count, and total bytes
991
+ * @throws {OPFSError} If a write fails
992
+ *
993
+ * @example
994
+ * ```typescript
995
+ * const result = await fs.importFiles([
996
+ * ['/config.json', JSON.stringify({ theme: 'dark' })],
997
+ * ['/data/binary.dat', new Uint8Array([1, 2, 3, 4])],
998
+ * ['/upload.txt', fileFromInput],
999
+ * ], (p) => console.log(`${p.path}: ${p.bytesWritten}/${p.bytesTotal}`));
1000
+ *
1001
+ * console.log(result.count, result.bytesWritten, result.paths);
1002
+ * ```
1003
+ */
1004
+ async importFiles(
1005
+ entries: ImportFilesEntries,
1006
+ onProgress?: (progress: ImportFilesProgress) => unknown
1007
+ ): Promise<ImportFilesResult> {
1008
+ await this.mount();
1009
+
1010
+ const list = [...entries].map(([path, data]) => [normalizePath(path), data] as [string, ImportFileData]);
1011
+ const count = list.length;
1012
+ const paths = list.map(([path]) => path);
1013
+ const totalBytes = list.reduce((sum, [, data]) => sum + importEntryByteLength(data), 0);
1014
+ let totalBytesWritten = 0;
1015
+
1016
+ try {
1017
+ for (let index = 0; index < count; index++) {
1018
+ const [path, data] = list[index]!;
1019
+ const { stream, size: bytesTotal } = toImportStream(data);
1020
+ const base = totalBytesWritten;
1021
+
1022
+ const written = await this.writeStream(
1023
+ path,
1024
+ stream,
1025
+ onProgress
1026
+ ? bytesWritten => onProgress({
1027
+ path,
1028
+ index,
1029
+ count,
1030
+ bytesWritten,
1031
+ bytesTotal,
1032
+ totalBytesWritten: base + bytesWritten,
1033
+ totalBytes,
1034
+ })
1035
+ : undefined
1036
+ );
1037
+
1038
+ totalBytesWritten += written;
929
1039
  }
1040
+
1041
+ return {
1042
+ paths,
1043
+ count,
1044
+ bytesWritten: totalBytesWritten,
1045
+ };
930
1046
  }
931
1047
  catch (error) {
932
1048
  if (error instanceof OPFSError) {
@@ -937,6 +1053,14 @@ export abstract class BaseOPFS {
937
1053
  }
938
1054
  }
939
1055
 
1056
+ /**
1057
+ * @deprecated Use {@link importFiles} instead.
1058
+ */
1059
+ async createIndex(entries: ImportFilesEntries): Promise<void> {
1060
+ warnCreateIndexDeprecated();
1061
+ await this.importFiles(entries);
1062
+ }
1063
+
940
1064
  /**
941
1065
  * Dispose of resources and clean up the file system instance
942
1066
  */
@@ -949,3 +1073,44 @@ export abstract class BaseOPFS {
949
1073
  this.watchers.clear();
950
1074
  }
951
1075
  }
1076
+
1077
+ function importEntryByteLength(data: ImportFileData): number {
1078
+ if (typeof data === 'string') {
1079
+ return new TextEncoder().encode(data).byteLength;
1080
+ }
1081
+
1082
+ if (data instanceof Blob) {
1083
+ return data.size;
1084
+ }
1085
+
1086
+ return data.byteLength;
1087
+ }
1088
+
1089
+ function toImportStream(data: ImportFileData): { stream: ReadableStream<Uint8Array>; size: number } {
1090
+ if (data instanceof Blob) {
1091
+ return { stream: data.stream(), size: data.size };
1092
+ }
1093
+
1094
+ const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data;
1095
+
1096
+ return {
1097
+ size: bytes.byteLength,
1098
+ stream: new ReadableStream({
1099
+ start(controller) {
1100
+ controller.enqueue(bytes);
1101
+ controller.close();
1102
+ },
1103
+ }),
1104
+ };
1105
+ }
1106
+
1107
+ let createIndexWarned = false;
1108
+
1109
+ function warnCreateIndexDeprecated(): void {
1110
+ if (createIndexWarned) {
1111
+ return;
1112
+ }
1113
+
1114
+ createIndexWarned = true;
1115
+ console.warn('[opfs-worker] createIndex() is deprecated; use importFiles() instead');
1116
+ }
@@ -3,7 +3,6 @@ import { transfer } from 'comlink';
3
3
  import { BaseOPFS } from './BaseOPFS';
4
4
  import { WatchEventType } from '../types';
5
5
  import { OPFSError, OperationNotSupportedError, mapDomError } from '../utils/errors';
6
- import { withLock } from '../utils/helpers';
7
6
 
8
7
  import type { FileOpenOptions, FileStat } from '../types';
9
8
 
@@ -37,7 +36,7 @@ export class OPFSAsync extends BaseOPFS {
37
36
  await this.mount();
38
37
 
39
38
  try {
40
- return await withLock(path, async() => {
39
+ return await this.withPathLock(path, async() => {
41
40
  const fileHandle = await this.getFileHandle(path, false);
42
41
  const file = await fileHandle.getFile();
43
42
  const buffer = new Uint8Array(await file.arrayBuffer());
@@ -63,7 +62,7 @@ export class OPFSAsync extends BaseOPFS {
63
62
  const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
64
63
 
65
64
  try {
66
- await withLock(path, async() => {
65
+ await this.withPathLock(path, async() => {
67
66
  const existed = await this.exists(path);
68
67
  const fileHandle = await this.getFileHandle(path, true);
69
68
 
@@ -92,7 +91,7 @@ export class OPFSAsync extends BaseOPFS {
92
91
  const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
93
92
 
94
93
  try {
95
- await withLock(path, async() => {
94
+ await this.withPathLock(path, async() => {
96
95
  const fileHandle = await this.getFileHandle(path, true);
97
96
  const { size } = await fileHandle.getFile();
98
97
 
@@ -122,7 +121,7 @@ export class OPFSAsync extends BaseOPFS {
122
121
  await this.mount();
123
122
 
124
123
  try {
125
- return await withLock(path, async() => {
124
+ return await this.withPathLock(path, async() => {
126
125
  const existed = await this.exists(path);
127
126
  const fileHandle = await this.getFileHandle(path, true);
128
127
  const reader = stream.getReader();
@@ -16,8 +16,7 @@ import {
16
16
  normalizePath,
17
17
  resolvePath,
18
18
  safeCloseSyncHandle,
19
- validateReadWriteArgs,
20
- withLock
19
+ validateReadWriteArgs
21
20
  } from '../utils/helpers';
22
21
 
23
22
  import type { FileOpenOptions, FileStat, OPFSOptions } from '../types';
@@ -77,7 +76,7 @@ export class OPFSSync extends BaseOPFS {
77
76
  await this.mount();
78
77
 
79
78
  try {
80
- return await withLock(path, async() => {
79
+ return await this.withPathLock(path, async() => {
81
80
  const fd = await this.open(path);
82
81
 
83
82
  try {
@@ -112,7 +111,7 @@ export class OPFSSync extends BaseOPFS {
112
111
 
113
112
  const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
114
113
 
115
- await withLock(path, async() => {
114
+ await this.withPathLock(path, async() => {
116
115
  const existed = await this.exists(path);
117
116
  const fd = await this.open(path, { create: true, truncate: true });
118
117
 
@@ -136,7 +135,7 @@ export class OPFSSync extends BaseOPFS {
136
135
 
137
136
  const buffer = data instanceof Uint8Array ? data : new Uint8Array(data);
138
137
 
139
- await withLock(path, async() => {
138
+ await this.withPathLock(path, async() => {
140
139
  const fd = await this.open(path, { create: true });
141
140
 
142
141
  try {
@@ -160,7 +159,7 @@ export class OPFSSync extends BaseOPFS {
160
159
  ): Promise<number> {
161
160
  await this.mount();
162
161
 
163
- return withLock(path, async() => {
162
+ return this.withPathLock(path, async() => {
164
163
  const existed = await this.exists(path);
165
164
  const fd = await this.open(path, { create: true, truncate: true });
166
165
  const reader = stream.getReader();
@@ -224,7 +223,7 @@ export class OPFSSync extends BaseOPFS {
224
223
  try {
225
224
  // Use lock for atomic operations when creating files
226
225
  if (create && exclusive) {
227
- return await withLock(normalizedPath, async() => {
226
+ return await this.withPathLock(normalizedPath, async() => {
228
227
  const exists = await this.exists(normalizedPath);
229
228
 
230
229
  if (exists) {
@@ -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
  /**
@@ -10,8 +10,10 @@ import type { DedicatedWorkerOptions } from '../worker/createDedicatedWorker';
10
10
  * Prefer importing from `opfs-worker/sync` when you only need the worker backend
11
11
  * (avoids pulling `OPFSAsync` into the graph). Importing from `opfs-worker` works too.
12
12
  *
13
- * By default uses an inlined worker. Pass `url` / `worker` to load
14
- * `opfs-worker/dedicated.worker.js` instead (see {@link DedicatedWorkerOptions}).
13
+ * By default uses an inlined worker. Calls with the same `root` (and same `url`)
14
+ * reuse one Worker on this page; different roots get different Workers.
15
+ * Pass `url` / `worker` to load `opfs-worker/dedicated.worker.js` instead
16
+ * (see {@link DedicatedWorkerOptions}).
15
17
  *
16
18
  * Need the raw bytes API or the Worker instance? Use `fs.backend` / `fs.worker`.
17
19
  * For the workerless async backend, use {@link createOPFSAsync}.
package/src/index.sync.ts CHANGED
@@ -9,4 +9,6 @@ export { OPFSFacade } from './facade/OPFSFacade';
9
9
  export type { OPFSBackend } from './facade/OPFSFacade';
10
10
 
11
11
  export { createOPFSDedicated } from './facade/createOPFSDedicated';
12
+ /** Short alias for {@link createOPFSDedicated} */
13
+ export { createOPFSDedicated as createOPFS } from './facade/createOPFSDedicated';
12
14
  export type { DedicatedWorkerOptions } from './worker/createDedicatedWorker';
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export {
7
7
  BaseOPFS,
8
8
  OPFSFacade,
9
9
  OPFSSync,
10
+ createOPFS,
10
11
  createOPFSDedicated,
11
12
  } from './index.sync';
12
13
 
@@ -28,11 +29,6 @@ export type {
28
29
  SharedWorkerOptions,
29
30
  } from './index.sharedworker';
30
31
 
31
- /**
32
- * Kept as a short alias for a default Sync backend
33
- */
34
- export { createOPFSDedicated as createOPFS } from './index.sync';
35
-
36
32
  /**
37
33
  * @deprecated Use {@link createOPFSDedicated}. Kept for 1.x → 2.x migration.
38
34
  */
package/src/types.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { BaseOPFS } from './core/BaseOPFS';
1
2
  import type { OPFSSync } from './core/OPFSSync';
2
3
 
3
4
  /**
@@ -58,10 +59,11 @@ export interface WatchEvent {
58
59
  export type { OPFSSync };
59
60
 
60
61
  /**
61
- * Promise-based fs API surface shared by all backends: a Comlink proxy to
62
- * `OPFSSync` in a worker, or an in-process `OPFSAsync` instance.
62
+ * Public bytes API shared by all backends (`OPFSSync`, `OPFSAsync`, or a
63
+ * Comlink proxy to one of them). Declared on {@link BaseOPFS}; subclasses
64
+ * implement the abstract I/O and FD methods.
63
65
  */
64
- export type OPFSApi = { [K in keyof OPFSSync]: OPFSSync[K] };
66
+ export type OPFSApi = Pick<BaseOPFS, keyof BaseOPFS>;
65
67
 
66
68
  export interface OPFSOptions {
67
69
  /** Root path for the file system (default: '/') */
@@ -76,6 +78,43 @@ export interface OPFSOptions {
76
78
  broadcastChannel?: string | BroadcastChannel | null;
77
79
  }
78
80
 
81
+ /** Payload accepted by {@link importFiles} for a single entry. */
82
+ export type ImportFileData = string | Uint8Array | Blob;
83
+
84
+ /**
85
+ * Entries for {@link importFiles}: an array of `[path, data]` tuples, a `Map`,
86
+ * or any iterable that yields the same pairs.
87
+ */
88
+ export type ImportFilesEntries = Iterable<[string, ImportFileData]> | Map<string, ImportFileData>;
89
+
90
+ /** Progress event fired while {@link importFiles} writes entries. */
91
+ export interface ImportFilesProgress {
92
+ /** Path of the file currently being written */
93
+ path: string;
94
+ /** 0-based index of the current file */
95
+ index: number;
96
+ /** Total number of entries in this import */
97
+ count: number;
98
+ /** Bytes written for the current file so far */
99
+ bytesWritten: number;
100
+ /** Size of the current file in bytes */
101
+ bytesTotal: number;
102
+ /** Bytes written across all files so far */
103
+ totalBytesWritten: number;
104
+ /** Sum of all entry sizes in bytes */
105
+ totalBytes: number;
106
+ }
107
+
108
+ /** Result of a finished {@link importFiles} call. */
109
+ export interface ImportFilesResult {
110
+ /** Paths written, in import order */
111
+ paths: string[];
112
+ /** Number of files imported (`paths.length`) */
113
+ count: number;
114
+ /** Total bytes written across all files */
115
+ bytesWritten: number;
116
+ }
117
+
79
118
  export interface RenameOptions {
80
119
  /** Whether to overwrite existing files (default: false) */
81
120
  overwrite?: boolean;
@@ -21,22 +21,48 @@ export function checkOPFSSupport(): void {
21
21
  }
22
22
  }
23
23
 
24
+ /**
25
+ * Join a mount `root` with an API path into an absolute OPFS path.
26
+ *
27
+ * Lock keys and cross-instance coordination use this so `/x.txt` under
28
+ * `root: '/app'` and `/app/x.txt` under `root: '/'` resolve to the same file.
29
+ */
30
+ export function absoluteOpfsPath(root: string, path: string): string {
31
+ const r = normalizePath(root || '/');
32
+ const p = normalizePath(resolvePath(path));
33
+
34
+ if (r === '/') {
35
+ return p;
36
+ }
37
+
38
+ if (p === '/') {
39
+ return r;
40
+ }
41
+
42
+ return `${ r.replace(/\/$/, '') }${ p }`;
43
+ }
44
+
24
45
  /**
25
46
  * Run a callback while holding an exclusive lock on a path
26
47
  *
27
48
  * Locks are always exclusive: OPFS permits a single sync access handle per file,
28
- * so readers can't share access either.
49
+ * so readers can't share access either. The lock key is the absolute OPFS path
50
+ * (`root` + API `path`) so different mounts of the same file serialize correctly.
29
51
  *
30
- * @param path - The path to lock
52
+ * @param path - The API path to lock (relative to `root`)
31
53
  * @param fn - The callback to run while holding the lock
54
+ * @param root - Mount root (default: `/`)
32
55
  * @returns The value returned by the callback
33
56
  */
34
57
  export async function withLock<T>(
35
58
  path: string,
36
- fn: () => Promise<T>
59
+ fn: () => Promise<T>,
60
+ root: string = '/'
37
61
  ): Promise<T> {
38
62
  if (typeof navigator !== 'undefined' && navigator.locks?.request) {
39
- return navigator.locks.request(`opfs:${ path.replace(/\/+/g, '/') }`, { mode: 'exclusive' }, fn);
63
+ const key = absoluteOpfsPath(root, path).replace(/\/+/g, '/');
64
+
65
+ return navigator.locks.request(`opfs:${ key }`, { mode: 'exclusive' }, fn);
40
66
  }
41
67
 
42
68
  return fn();
@@ -343,7 +369,7 @@ export async function convertBlobToUint8Array(blob: Blob): Promise<Uint8Array> {
343
369
  export async function removeEntry(
344
370
  parentHandle: FileSystemDirectoryHandle,
345
371
  path: string,
346
- options: { recursive?: boolean; force?: boolean; useTrash?: boolean } = {}
372
+ options: { recursive?: boolean; force?: boolean; useTrash?: boolean; root?: string } = {}
347
373
  ): Promise<void> {
348
374
  const name = basename(path);
349
375
 
@@ -367,7 +393,7 @@ export async function removeEntry(
367
393
  isDirectory,
368
394
  });
369
395
  }
370
- });
396
+ }, options.root ?? '/');
371
397
  }
372
398
 
373
399
  /**