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.
- package/README.md +21 -20
- package/dist/BaseOPFS-Bu7FZtCb.cjs +2 -0
- package/dist/BaseOPFS-Bu7FZtCb.cjs.map +1 -0
- package/dist/{BaseOPFS-BusBkyJX.js → BaseOPFS-C3NE4BKr.js} +790 -548
- package/dist/BaseOPFS-C3NE4BKr.js.map +1 -0
- package/dist/OPFSAsync-C0RsxCX5.cjs +2 -0
- package/dist/OPFSAsync-C0RsxCX5.cjs.map +1 -0
- package/dist/{OPFSAsync-CO44W23e.js → OPFSAsync-aeLUdIqd.js} +19 -19
- package/dist/OPFSAsync-aeLUdIqd.js.map +1 -0
- package/dist/{OPFSFacade-hrNXCn6w.cjs → OPFSFacade-BJv2yuWU.cjs} +2 -2
- package/dist/{OPFSFacade-hrNXCn6w.cjs.map → OPFSFacade-BJv2yuWU.cjs.map} +1 -1
- package/dist/{OPFSFacade-CEaSqTut.js → OPFSFacade-DiogtQoq.js} +2 -2
- package/dist/{OPFSFacade-CEaSqTut.js.map → OPFSFacade-DiogtQoq.js.map} +1 -1
- package/dist/OPFSSync-3qlSBYmz.cjs +2 -0
- package/dist/OPFSSync-3qlSBYmz.cjs.map +1 -0
- package/dist/{OPFSSync-D7obTsl3.js → OPFSSync-CvSOswhW.js} +57 -57
- package/dist/OPFSSync-CvSOswhW.js.map +1 -0
- package/dist/assets/dedicated.worker-Bqqr9UBA.js.map +1 -0
- package/dist/core/BaseOPFS.d.ts +23 -3
- package/dist/core/BaseOPFS.d.ts.map +1 -1
- package/dist/core/OPFSAsync.d.ts.map +1 -1
- package/dist/core/OPFSSync.d.ts.map +1 -1
- package/dist/createOPFSAsync-B611B63X.js +10 -0
- package/dist/{createOPFSAsync-DdhJYoC0.js.map → createOPFSAsync-B611B63X.js.map} +1 -1
- package/dist/createOPFSAsync-BATjJg8k.cjs +2 -0
- package/dist/{createOPFSAsync-DyWpvmL-.cjs.map → createOPFSAsync-BATjJg8k.cjs.map} +1 -1
- package/dist/{createOPFSDedicated-BwTc0NV5.js → createOPFSDedicated-DgudPTB8.js} +1086 -817
- package/dist/createOPFSDedicated-DgudPTB8.js.map +1 -0
- package/dist/{createOPFSDedicated-CEeRexom.cjs → createOPFSDedicated-EmIwluQo.cjs} +1037 -797
- package/dist/createOPFSDedicated-EmIwluQo.cjs.map +1 -0
- package/dist/{createOPFSShared-BSFWsoLe.js → createOPFSShared-BWYln8jJ.js} +2 -2
- package/dist/{createOPFSShared-BSFWsoLe.js.map → createOPFSShared-BWYln8jJ.js.map} +1 -1
- package/dist/{createOPFSShared-BoCft_BS.cjs → createOPFSShared-DPJpCd7z.cjs} +2 -2
- package/dist/{createOPFSShared-BoCft_BS.cjs.map → createOPFSShared-DPJpCd7z.cjs.map} +1 -1
- package/dist/dedicated.worker.js +1034 -794
- package/dist/dedicated.worker.js.map +1 -1
- package/dist/facade/createOPFSDedicated.d.ts +4 -2
- package/dist/facade/createOPFSDedicated.d.ts.map +1 -1
- package/dist/index.async.cjs +1 -1
- package/dist/index.async.js +33 -32
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -43
- package/dist/index.pure.cjs +1 -1
- package/dist/index.pure.js +38 -37
- package/dist/index.sharedworker.cjs +1 -1
- package/dist/index.sharedworker.js +34 -33
- package/dist/index.sync.cjs +1 -1
- package/dist/index.sync.d.ts +2 -0
- package/dist/index.sync.d.ts.map +1 -1
- package/dist/index.sync.js +34 -32
- package/dist/shared.worker.js +1056 -820
- package/dist/shared.worker.js.map +1 -1
- package/dist/types.d.ts +5 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/helpers.d.ts +13 -3
- package/dist/utils/helpers.d.ts.map +1 -1
- package/dist/worker/createDedicatedWorker.d.ts +8 -3
- package/dist/worker/createDedicatedWorker.d.ts.map +1 -1
- package/docs/README.md +18 -0
- package/docs/api/README.md +221 -0
- package/docs/{file-descriptors.md → api/file-descriptors.md} +13 -13
- package/docs/guides/async.md +2 -2
- package/docs/guides/dedicated.md +6 -4
- package/docs/guides/hashing.md +1 -1
- package/docs/guides/pure.md +1 -1
- package/docs/guides/sharedworker.md +1 -1
- package/docs/guides/streaming.md +2 -2
- package/docs/migration.md +8 -9
- package/docs/types.md +4 -4
- package/package.json +10 -9
- package/src/core/BaseOPFS.ts +46 -4
- package/src/core/OPFSAsync.ts +4 -5
- package/src/core/OPFSSync.ts +6 -7
- package/src/facade/createOPFSDedicated.ts +4 -2
- package/src/index.sync.ts +2 -0
- package/src/index.ts +1 -5
- package/src/types.ts +5 -3
- package/src/utils/helpers.ts +32 -6
- package/src/worker/createDedicatedWorker.ts +85 -7
- package/src/worker/dedicated.worker.ts +12 -1
- package/dist/BaseOPFS-BusBkyJX.js.map +0 -1
- package/dist/BaseOPFS-CYwrD7-q.cjs +0 -4
- package/dist/BaseOPFS-CYwrD7-q.cjs.map +0 -1
- package/dist/OPFSAsync-B2dR-2VR.cjs +0 -2
- package/dist/OPFSAsync-B2dR-2VR.cjs.map +0 -1
- package/dist/OPFSAsync-CO44W23e.js.map +0 -1
- package/dist/OPFSSync-BPrNtvFS.cjs +0 -2
- package/dist/OPFSSync-BPrNtvFS.cjs.map +0 -1
- package/dist/OPFSSync-D7obTsl3.js.map +0 -1
- package/dist/assets/dedicated.worker-DW6UqFo0.js.map +0 -1
- package/dist/createOPFSAsync-DdhJYoC0.js +0 -10
- package/dist/createOPFSAsync-DyWpvmL-.cjs +0 -2
- package/dist/createOPFSDedicated-BwTc0NV5.js.map +0 -1
- package/dist/createOPFSDedicated-CEeRexom.cjs.map +0 -1
- package/docs/api/backend.md +0 -36
- package/docs/api/create.md +0 -82
- package/docs/api/facade.md +0 -53
- package/docs/choosing-a-mode.md +0 -41
package/src/core/BaseOPFS.ts
CHANGED
|
@@ -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
|
-
*
|
|
47
|
-
* and
|
|
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
|
}
|
package/src/core/OPFSAsync.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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();
|
package/src/core/OPFSSync.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
14
|
-
*
|
|
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
|
-
*
|
|
62
|
-
*
|
|
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 =
|
|
66
|
+
export type OPFSApi = Pick<BaseOPFS, keyof BaseOPFS>;
|
|
65
67
|
|
|
66
68
|
export interface OPFSOptions {
|
|
67
69
|
/** Root path for the file system (default: '/') */
|
package/src/utils/helpers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|