opfs-worker 2.1.1 → 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 (100) hide show
  1. package/README.md +21 -20
  2. package/dist/BaseOPFS-Bu7FZtCb.cjs +2 -0
  3. package/dist/BaseOPFS-Bu7FZtCb.cjs.map +1 -0
  4. package/dist/{BaseOPFS-BusBkyJX.js → BaseOPFS-C3NE4BKr.js} +790 -548
  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-CO44W23e.js → OPFSAsync-aeLUdIqd.js} +19 -19
  9. package/dist/OPFSAsync-aeLUdIqd.js.map +1 -0
  10. package/dist/{OPFSFacade-hrNXCn6w.cjs → OPFSFacade-BJv2yuWU.cjs} +2 -2
  11. package/dist/{OPFSFacade-hrNXCn6w.cjs.map → OPFSFacade-BJv2yuWU.cjs.map} +1 -1
  12. package/dist/{OPFSFacade-CEaSqTut.js → OPFSFacade-DiogtQoq.js} +2 -2
  13. package/dist/{OPFSFacade-CEaSqTut.js.map → OPFSFacade-DiogtQoq.js.map} +1 -1
  14. package/dist/OPFSSync-3qlSBYmz.cjs +2 -0
  15. package/dist/OPFSSync-3qlSBYmz.cjs.map +1 -0
  16. package/dist/{OPFSSync-D7obTsl3.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 +23 -3
  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-DdhJYoC0.js.map → createOPFSAsync-B611B63X.js.map} +1 -1
  25. package/dist/createOPFSAsync-BATjJg8k.cjs +2 -0
  26. package/dist/{createOPFSAsync-DyWpvmL-.cjs.map → createOPFSAsync-BATjJg8k.cjs.map} +1 -1
  27. package/dist/{createOPFSDedicated-BwTc0NV5.js → createOPFSDedicated-DgudPTB8.js} +1086 -817
  28. package/dist/createOPFSDedicated-DgudPTB8.js.map +1 -0
  29. package/dist/{createOPFSDedicated-CEeRexom.cjs → createOPFSDedicated-EmIwluQo.cjs} +1037 -797
  30. package/dist/createOPFSDedicated-EmIwluQo.cjs.map +1 -0
  31. package/dist/{createOPFSShared-BSFWsoLe.js → createOPFSShared-BWYln8jJ.js} +2 -2
  32. package/dist/{createOPFSShared-BSFWsoLe.js.map → createOPFSShared-BWYln8jJ.js.map} +1 -1
  33. package/dist/{createOPFSShared-BoCft_BS.cjs → createOPFSShared-DPJpCd7z.cjs} +2 -2
  34. package/dist/{createOPFSShared-BoCft_BS.cjs.map → createOPFSShared-DPJpCd7z.cjs.map} +1 -1
  35. package/dist/dedicated.worker.js +1034 -794
  36. package/dist/dedicated.worker.js.map +1 -1
  37. package/dist/facade/createOPFSDedicated.d.ts +4 -2
  38. package/dist/facade/createOPFSDedicated.d.ts.map +1 -1
  39. package/dist/index.async.cjs +1 -1
  40. package/dist/index.async.js +33 -32
  41. package/dist/index.cjs +1 -1
  42. package/dist/index.d.ts +1 -5
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +44 -43
  45. package/dist/index.pure.cjs +1 -1
  46. package/dist/index.pure.js +38 -37
  47. package/dist/index.sharedworker.cjs +1 -1
  48. package/dist/index.sharedworker.js +34 -33
  49. package/dist/index.sync.cjs +1 -1
  50. package/dist/index.sync.d.ts +2 -0
  51. package/dist/index.sync.d.ts.map +1 -1
  52. package/dist/index.sync.js +34 -32
  53. package/dist/shared.worker.js +1056 -820
  54. package/dist/shared.worker.js.map +1 -1
  55. package/dist/types.d.ts +5 -5
  56. package/dist/types.d.ts.map +1 -1
  57. package/dist/utils/helpers.d.ts +13 -3
  58. package/dist/utils/helpers.d.ts.map +1 -1
  59. package/dist/worker/createDedicatedWorker.d.ts +8 -3
  60. package/dist/worker/createDedicatedWorker.d.ts.map +1 -1
  61. package/docs/README.md +18 -0
  62. package/docs/api/README.md +221 -0
  63. package/docs/{file-descriptors.md → api/file-descriptors.md} +13 -13
  64. package/docs/guides/async.md +2 -2
  65. package/docs/guides/dedicated.md +6 -4
  66. package/docs/guides/hashing.md +1 -1
  67. package/docs/guides/pure.md +1 -1
  68. package/docs/guides/sharedworker.md +1 -1
  69. package/docs/guides/streaming.md +2 -2
  70. package/docs/migration.md +8 -9
  71. package/docs/types.md +4 -4
  72. package/package.json +10 -9
  73. package/src/core/BaseOPFS.ts +46 -4
  74. package/src/core/OPFSAsync.ts +4 -5
  75. package/src/core/OPFSSync.ts +6 -7
  76. package/src/facade/createOPFSDedicated.ts +4 -2
  77. package/src/index.sync.ts +2 -0
  78. package/src/index.ts +1 -5
  79. package/src/types.ts +5 -3
  80. package/src/utils/helpers.ts +32 -6
  81. package/src/worker/createDedicatedWorker.ts +85 -7
  82. package/src/worker/dedicated.worker.ts +12 -1
  83. package/dist/BaseOPFS-BusBkyJX.js.map +0 -1
  84. package/dist/BaseOPFS-CYwrD7-q.cjs +0 -4
  85. package/dist/BaseOPFS-CYwrD7-q.cjs.map +0 -1
  86. package/dist/OPFSAsync-B2dR-2VR.cjs +0 -2
  87. package/dist/OPFSAsync-B2dR-2VR.cjs.map +0 -1
  88. package/dist/OPFSAsync-CO44W23e.js.map +0 -1
  89. package/dist/OPFSSync-BPrNtvFS.cjs +0 -2
  90. package/dist/OPFSSync-BPrNtvFS.cjs.map +0 -1
  91. package/dist/OPFSSync-D7obTsl3.js.map +0 -1
  92. package/dist/assets/dedicated.worker-DW6UqFo0.js.map +0 -1
  93. package/dist/createOPFSAsync-DdhJYoC0.js +0 -10
  94. package/dist/createOPFSAsync-DyWpvmL-.cjs +0 -2
  95. package/dist/createOPFSDedicated-BwTc0NV5.js.map +0 -1
  96. package/dist/createOPFSDedicated-CEeRexom.cjs.map +0 -1
  97. package/docs/api/backend.md +0 -36
  98. package/docs/api/create.md +0 -82
  99. package/docs/api/facade.md +0 -53
  100. package/docs/choosing-a-mode.md +0 -41
@@ -23,11 +23,13 @@ import {
23
23
  normalizePath,
24
24
  removeEntry,
25
25
  resolvePath,
26
- splitPath
26
+ splitPath,
27
+ withLock
27
28
  } from '../utils/helpers';
28
29
 
29
30
  import type {
30
31
  DirentData,
32
+ FileOpenOptions,
31
33
  FileStat,
32
34
  ImportFileData,
33
35
  ImportFilesEntries,
@@ -43,8 +45,9 @@ import type {
43
45
 
44
46
  /**
45
47
  * Shared OPFS logic (directories, meta, watch, high-level helpers).
46
- * File I/O backends implement {@link readFile}, {@link writeFile}, {@link appendFile},
47
- * 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.
48
51
  */
49
52
  export abstract class BaseOPFS {
50
53
  /** Root directory handle for the file system */
@@ -135,6 +138,11 @@ export abstract class BaseOPFS {
135
138
  }
136
139
  }
137
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
+
138
146
  /** Read file contents as bytes */
139
147
  abstract readFile(path: string): Promise<Uint8Array>;
140
148
 
@@ -151,6 +159,40 @@ export abstract class BaseOPFS {
151
159
  onProgress?: (bytesWritten: number) => unknown
152
160
  ): Promise<number>;
153
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
+
154
196
  /**
155
197
  * Initialize the file system within a given directory.
156
198
  * If no root is specified, uses the OPFS root directory.
@@ -682,7 +724,7 @@ export abstract class BaseOPFS {
682
724
  const parent = await this.getDirectoryHandle(dirname(path), false);
683
725
  const stat = await this.stat(path);
684
726
 
685
- await removeEntry(parent, path, { recursive, force });
727
+ await removeEntry(parent, path, { recursive, force, root: this.options.root });
686
728
 
687
729
  await this.notifyChange({ path, type: WatchEventType.Removed, isDirectory: stat.isDirectory });
688
730
  }
@@ -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) {
@@ -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: '/') */
@@ -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
  /**
@@ -1,6 +1,7 @@
1
1
  import { wrap } from 'comlink';
2
2
 
3
3
  import WorkerCtor from './dedicated.worker?worker&inline';
4
+ import { normalizePath } from '../utils/helpers';
4
5
 
5
6
  import type { OPFSOptions, OPFSSync } from '../types';
6
7
  import type { Remote } from 'comlink';
@@ -17,7 +18,7 @@ export interface DedicatedWorkerOptions extends OPFSOptions {
17
18
  * prefer the self-contained file (strict CSP without `blob:`, or DIY hosting).
18
19
  */
19
20
  url?: string | URL;
20
- /** Bring your own Worker instance (overrides `url`) */
21
+ /** Bring your own Worker instance (overrides `url`) — not pooled */
21
22
  worker?: Worker;
22
23
  }
23
24
 
@@ -26,10 +27,22 @@ export interface RawWorker {
26
27
  fs: RemoteOPFSSync;
27
28
  /** Underlying browser Worker */
28
29
  worker: Worker;
29
- /** Calls worker `dispose()` then `terminate()` */
30
+ /** Drops this facade's port; terminates the Worker when the last user of this pool entry disposes */
30
31
  dispose: () => void;
31
32
  }
32
33
 
34
+ interface PoolEntry {
35
+ worker: Worker;
36
+ refs: number;
37
+ }
38
+
39
+ /** Same root (+ same worker url) → one Worker per page. */
40
+ const pool = new Map<string, PoolEntry>();
41
+
42
+ function poolKey(root: string, url?: string | URL): string {
43
+ return `${ normalizePath(root) }\0${ url?.toString() ?? 'inline' }`;
44
+ }
45
+
33
46
  /** A `BroadcastChannel` instance can't cross the wire — send its name instead. */
34
47
  function applyWorkerOptions(fs: Pick<RemoteOPFSSync, 'setOptions'>, options?: OPFSOptions): void {
35
48
  if (!options) {
@@ -43,29 +56,94 @@ function applyWorkerOptions(fs: Pick<RemoteOPFSSync, 'setOptions'>, options?: OP
43
56
  void fs.setOptions(options);
44
57
  }
45
58
 
59
+ function connectProxy(worker: Worker): { fs: RemoteOPFSSync; port: MessagePort } {
60
+ const { port1, port2 } = new MessageChannel();
61
+
62
+ worker.postMessage({ type: 'opfs-connect', port: port2 }, [port2]);
63
+
64
+ return { fs: wrap<RemoteOPFSSync>(port1), port: port1 };
65
+ }
66
+
46
67
  /**
47
- * Internal helper: spawn a dedicated worker and wrap `OPFSSync` with Comlink.
68
+ * Internal helper: spawn (or reuse) a dedicated worker and wrap `OPFSSync` with Comlink.
48
69
  * Prefer {@link createOPFSDedicated}; access the proxy / Worker via
49
70
  * `facade.backend` / `facade.worker`.
71
+ *
72
+ * Calls with the same `root` (and same `url`, if any) share one Worker on this page.
73
+ * Different roots get different Workers. Passing `worker` bypasses the pool.
74
+ *
75
+ * `setOptions` hits the shared instance — keep options consistent for the same root.
50
76
  */
51
77
  export function createDedicatedWorker(options: DedicatedWorkerOptions = {}): RawWorker {
52
78
  const { url, worker: providedWorker, ...fsOptions } = options;
79
+ const root = normalizePath(fsOptions.root ?? '/');
80
+
81
+ if (providedWorker) {
82
+ const { fs, port } = connectProxy(providedWorker);
83
+
84
+ applyWorkerOptions(fs, fsOptions);
85
+
86
+ return {
87
+ fs,
88
+ worker: providedWorker,
89
+ dispose() {
90
+ port.close();
91
+ },
92
+ };
93
+ }
53
94
 
54
- const worker = providedWorker
55
- ?? (url ? new Worker(url, { type: 'module' }) : new InlineWorker());
56
- const fs = wrap<RemoteOPFSSync>(worker);
95
+ const key = poolKey(root, url);
96
+ let entry = pool.get(key);
97
+
98
+ if (!entry) {
99
+ entry = {
100
+ worker: url ? new Worker(url, { type: 'module' }) : new InlineWorker(),
101
+ refs: 0,
102
+ };
103
+ pool.set(key, entry);
104
+ }
105
+
106
+ entry.refs += 1;
107
+
108
+ const { worker } = entry;
109
+ const { fs, port } = connectProxy(worker);
57
110
 
58
111
  applyWorkerOptions(fs, fsOptions);
59
112
 
113
+ let disposed = false;
114
+
60
115
  return {
61
116
  fs,
62
117
  worker,
63
118
  dispose() {
119
+ if (disposed) {
120
+ return;
121
+ }
122
+
123
+ disposed = true;
124
+
64
125
  void (async() => {
126
+ const current = pool.get(key);
127
+ const isLast = !!current && current.worker === worker && current.refs <= 1;
128
+
65
129
  try {
66
- await fs.dispose();
130
+ // Only the last facade may dispose the shared OPFSSync backend
131
+ if (isLast) {
132
+ await fs.dispose();
133
+ }
67
134
  }
68
135
  finally {
136
+ port.close();
137
+ }
138
+
139
+ if (!current || current.worker !== worker) {
140
+ return;
141
+ }
142
+
143
+ current.refs -= 1;
144
+
145
+ if (current.refs <= 0) {
146
+ pool.delete(key);
69
147
  worker.terminate();
70
148
  }
71
149
  })();
@@ -9,5 +9,16 @@ import { OPFSSync } from '../core/OPFSSync';
9
9
  * - Inlined into `createOPFSDedicated` via `?worker&inline` (default path)
10
10
  * - Self-contained `dist/dedicated.worker.js` → `opfs-worker/dedicated.worker.js`
11
11
  * for DIY `new Worker(url, { type: 'module' })` + Comlink wrap
12
+ *
13
+ * Also accepts transferred MessagePorts (`type: 'opfs-connect'`) so one
14
+ * OPFSSync instance can serve multiple facades on the same page.
12
15
  */
13
- expose(new OPFSSync());
16
+ const fs = new OPFSSync();
17
+
18
+ expose(fs);
19
+
20
+ addEventListener('message', (event: MessageEvent) => {
21
+ if (event.data?.type === 'opfs-connect' && event.data.port instanceof MessagePort) {
22
+ expose(fs, event.data.port);
23
+ }
24
+ });