@componentor/fs 1.1.7
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/LICENSE +21 -0
- package/README.md +742 -0
- package/dist/index.d.ts +544 -0
- package/dist/index.js +2551 -0
- package/dist/index.js.map +1 -0
- package/dist/opfs-hybrid.d.ts +198 -0
- package/dist/opfs-hybrid.js +2552 -0
- package/dist/opfs-hybrid.js.map +1 -0
- package/dist/opfs-worker-proxy.d.ts +224 -0
- package/dist/opfs-worker-proxy.js +274 -0
- package/dist/opfs-worker-proxy.js.map +1 -0
- package/dist/opfs-worker.js +2732 -0
- package/dist/opfs-worker.js.map +1 -0
- package/package.json +66 -0
- package/src/constants.ts +52 -0
- package/src/errors.ts +88 -0
- package/src/file-handle.ts +100 -0
- package/src/global.d.ts +57 -0
- package/src/handle-manager.ts +250 -0
- package/src/index.ts +1404 -0
- package/src/opfs-hybrid.ts +265 -0
- package/src/opfs-worker-proxy.ts +374 -0
- package/src/opfs-worker.ts +253 -0
- package/src/packed-storage.ts +426 -0
- package/src/path-utils.ts +97 -0
- package/src/streams.ts +109 -0
- package/src/symlink-manager.ts +329 -0
- package/src/types.ts +285 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPFS Hybrid - Routes read/write operations to different backends
|
|
3
|
+
*
|
|
4
|
+
* Allows optimal performance by using:
|
|
5
|
+
* - Main thread for reads (no message passing overhead)
|
|
6
|
+
* - Worker for writes (sync access handles are faster)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import OPFS from './index.js'
|
|
10
|
+
import { OPFSWorker } from './opfs-worker-proxy.js'
|
|
11
|
+
import type {
|
|
12
|
+
ReadFileOptions,
|
|
13
|
+
WriteFileOptions,
|
|
14
|
+
BatchWriteEntry,
|
|
15
|
+
BatchReadResult,
|
|
16
|
+
ReaddirOptions,
|
|
17
|
+
Stats,
|
|
18
|
+
StatFs,
|
|
19
|
+
RmOptions,
|
|
20
|
+
CpOptions,
|
|
21
|
+
SymlinkDefinition,
|
|
22
|
+
DiskUsage
|
|
23
|
+
} from './types.js'
|
|
24
|
+
|
|
25
|
+
export type Backend = 'main' | 'worker'
|
|
26
|
+
|
|
27
|
+
export interface OPFSHybridOptions {
|
|
28
|
+
/** Backend for read operations (default: 'main') */
|
|
29
|
+
read?: Backend
|
|
30
|
+
/** Backend for write operations (default: 'worker') */
|
|
31
|
+
write?: Backend
|
|
32
|
+
/** Worker URL (required if using worker backend) */
|
|
33
|
+
workerUrl?: URL | string
|
|
34
|
+
/** Enable verbose logging */
|
|
35
|
+
verbose?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hybrid OPFS implementation that routes operations to optimal backends
|
|
40
|
+
*/
|
|
41
|
+
export class OPFSHybrid {
|
|
42
|
+
private mainFs: OPFS
|
|
43
|
+
private workerFs: OPFSWorker | null = null
|
|
44
|
+
private readBackend: Backend
|
|
45
|
+
private writeBackend: Backend
|
|
46
|
+
private workerUrl?: URL | string
|
|
47
|
+
private workerReady: Promise<void> | null = null
|
|
48
|
+
private verbose: boolean
|
|
49
|
+
|
|
50
|
+
constructor(options: OPFSHybridOptions = {}) {
|
|
51
|
+
this.readBackend = options.read ?? 'main'
|
|
52
|
+
this.writeBackend = options.write ?? 'worker'
|
|
53
|
+
this.workerUrl = options.workerUrl
|
|
54
|
+
this.verbose = options.verbose ?? false
|
|
55
|
+
|
|
56
|
+
// Always create main fs (needed for main backend or as fallback)
|
|
57
|
+
this.mainFs = new OPFS({ useSync: false, verbose: this.verbose })
|
|
58
|
+
|
|
59
|
+
// Create worker if needed
|
|
60
|
+
if (this.readBackend === 'worker' || this.writeBackend === 'worker') {
|
|
61
|
+
if (!this.workerUrl) {
|
|
62
|
+
throw new Error('workerUrl is required when using worker backend')
|
|
63
|
+
}
|
|
64
|
+
this.workerFs = new OPFSWorker({ workerUrl: this.workerUrl })
|
|
65
|
+
this.workerReady = this.workerFs.ready()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Wait for all backends to be ready
|
|
71
|
+
*/
|
|
72
|
+
async ready(): Promise<void> {
|
|
73
|
+
if (this.workerReady) {
|
|
74
|
+
await this.workerReady
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Terminate worker if active
|
|
80
|
+
*/
|
|
81
|
+
terminate(): void {
|
|
82
|
+
if (this.workerFs) {
|
|
83
|
+
this.workerFs.terminate()
|
|
84
|
+
this.workerFs = null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private getReadFs(): OPFS | OPFSWorker {
|
|
89
|
+
if (this.readBackend === 'worker' && this.workerFs) {
|
|
90
|
+
return this.workerFs
|
|
91
|
+
}
|
|
92
|
+
return this.mainFs
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private getWriteFs(): OPFS | OPFSWorker {
|
|
96
|
+
if (this.writeBackend === 'worker' && this.workerFs) {
|
|
97
|
+
return this.workerFs
|
|
98
|
+
}
|
|
99
|
+
return this.mainFs
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============ Read Operations ============
|
|
103
|
+
|
|
104
|
+
async readFile(path: string, options?: ReadFileOptions): Promise<Uint8Array | string> {
|
|
105
|
+
return this.getReadFs().readFile(path, options)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async readFileBatch(paths: string[]): Promise<BatchReadResult[]> {
|
|
109
|
+
return this.getReadFs().readFileBatch(paths)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async readdir(path: string, options?: ReaddirOptions): Promise<string[] | import('./types.js').Dirent[]> {
|
|
113
|
+
return this.getReadFs().readdir(path, options)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async stat(path: string): Promise<Stats> {
|
|
117
|
+
return this.getReadFs().stat(path)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async lstat(path: string): Promise<Stats> {
|
|
121
|
+
return this.getReadFs().lstat(path)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async exists(path: string): Promise<boolean> {
|
|
125
|
+
return this.getReadFs().exists(path)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async access(path: string, mode?: number): Promise<void> {
|
|
129
|
+
return this.getReadFs().access(path, mode)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async readlink(path: string): Promise<string> {
|
|
133
|
+
return this.getReadFs().readlink(path)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async realpath(path: string): Promise<string> {
|
|
137
|
+
return this.getReadFs().realpath(path)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async statfs(path?: string): Promise<StatFs> {
|
|
141
|
+
return this.getReadFs().statfs(path)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async du(path: string): Promise<DiskUsage> {
|
|
145
|
+
return this.getReadFs().du(path)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ============ Write Operations ============
|
|
149
|
+
|
|
150
|
+
async writeFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
|
|
151
|
+
return this.getWriteFs().writeFile(path, data, options)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async writeFileBatch(entries: BatchWriteEntry[]): Promise<void> {
|
|
155
|
+
return this.getWriteFs().writeFileBatch(entries)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async appendFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
|
|
159
|
+
return this.getWriteFs().appendFile(path, data, options)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async mkdir(path: string): Promise<void> {
|
|
163
|
+
return this.getWriteFs().mkdir(path)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async rmdir(path: string): Promise<void> {
|
|
167
|
+
// rmdir affects both backends' state
|
|
168
|
+
if (this.readBackend !== this.writeBackend && this.workerFs) {
|
|
169
|
+
// Clear via worker (does actual deletion and resets worker's symlink cache)
|
|
170
|
+
await this.workerFs.rmdir(path)
|
|
171
|
+
// Reset main thread's cache (no actual file operations, just cache invalidation)
|
|
172
|
+
this.mainFs.resetCache()
|
|
173
|
+
} else {
|
|
174
|
+
return this.getWriteFs().rmdir(path)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async unlink(path: string): Promise<void> {
|
|
179
|
+
return this.getWriteFs().unlink(path)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async truncate(path: string, len?: number): Promise<void> {
|
|
183
|
+
return this.getWriteFs().truncate(path, len)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async symlink(target: string, path: string): Promise<void> {
|
|
187
|
+
// Symlinks affect both backends' symlink cache
|
|
188
|
+
if (this.readBackend !== this.writeBackend && this.workerFs) {
|
|
189
|
+
await this.workerFs.symlink(target, path)
|
|
190
|
+
// Reset main thread's symlink cache so it reloads from disk
|
|
191
|
+
this.mainFs.resetCache()
|
|
192
|
+
} else {
|
|
193
|
+
return this.getWriteFs().symlink(target, path)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async symlinkBatch(symlinks: SymlinkDefinition[]): Promise<void> {
|
|
198
|
+
if (this.readBackend !== this.writeBackend && this.workerFs) {
|
|
199
|
+
await this.workerFs.symlinkBatch(symlinks)
|
|
200
|
+
// Reset main thread's symlink cache so it reloads from disk
|
|
201
|
+
this.mainFs.resetCache()
|
|
202
|
+
} else {
|
|
203
|
+
return this.getWriteFs().symlinkBatch(symlinks)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async rename(oldPath: string, newPath: string): Promise<void> {
|
|
208
|
+
return this.getWriteFs().rename(oldPath, newPath)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async copyFile(src: string, dest: string, mode?: number): Promise<void> {
|
|
212
|
+
return this.getWriteFs().copyFile(src, dest, mode)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async cp(src: string, dest: string, options?: CpOptions): Promise<void> {
|
|
216
|
+
return this.getWriteFs().cp(src, dest, options)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async rm(path: string, options?: RmOptions): Promise<void> {
|
|
220
|
+
return this.getWriteFs().rm(path, options)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async chmod(path: string, mode: number): Promise<void> {
|
|
224
|
+
return this.getWriteFs().chmod(path, mode)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async chown(path: string, uid: number, gid: number): Promise<void> {
|
|
228
|
+
return this.getWriteFs().chown(path, uid, gid)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async utimes(path: string, atime: Date | number, mtime: Date | number): Promise<void> {
|
|
232
|
+
return this.getWriteFs().utimes(path, atime, mtime)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async lutimes(path: string, atime: Date | number, mtime: Date | number): Promise<void> {
|
|
236
|
+
return this.getWriteFs().lutimes(path, atime, mtime)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async mkdtemp(prefix: string): Promise<string> {
|
|
240
|
+
return this.getWriteFs().mkdtemp(prefix)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reset internal caches on both backends
|
|
245
|
+
*/
|
|
246
|
+
async resetCache(): Promise<void> {
|
|
247
|
+
this.mainFs.resetCache()
|
|
248
|
+
if (this.workerFs) {
|
|
249
|
+
await this.workerFs.resetCache()
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Force full garbage collection on both backends
|
|
255
|
+
* More aggressive than resetCache() - reinitializes the worker's OPFS instance
|
|
256
|
+
*/
|
|
257
|
+
async gc(): Promise<void> {
|
|
258
|
+
this.mainFs.resetCache()
|
|
259
|
+
if (this.workerFs) {
|
|
260
|
+
await this.workerFs.gc()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export default OPFSHybrid
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OPFS Worker Proxy
|
|
3
|
+
* Main thread class that communicates with an OPFS worker
|
|
4
|
+
*
|
|
5
|
+
* This allows non-blocking OPFS operations on the main thread
|
|
6
|
+
* while the actual work happens in a dedicated Web Worker
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
ReadFileOptions,
|
|
11
|
+
WriteFileOptions,
|
|
12
|
+
BatchWriteEntry,
|
|
13
|
+
BatchReadResult,
|
|
14
|
+
ReaddirOptions,
|
|
15
|
+
Dirent,
|
|
16
|
+
Stats,
|
|
17
|
+
StatFs,
|
|
18
|
+
RmOptions,
|
|
19
|
+
CpOptions,
|
|
20
|
+
DiskUsage,
|
|
21
|
+
SymlinkDefinition
|
|
22
|
+
} from './types.js'
|
|
23
|
+
import { constants } from './constants.js'
|
|
24
|
+
import { FSError } from './errors.js'
|
|
25
|
+
|
|
26
|
+
interface PendingRequest {
|
|
27
|
+
resolve: (value: unknown) => void
|
|
28
|
+
reject: (error: Error) => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface WorkerResponse {
|
|
32
|
+
id?: number
|
|
33
|
+
type?: string
|
|
34
|
+
result?: unknown
|
|
35
|
+
error?: { message: string; code?: string }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface OPFSWorkerOptions {
|
|
39
|
+
/** URL to the worker script (default: auto-detect) */
|
|
40
|
+
workerUrl?: string | URL
|
|
41
|
+
/** Worker initialization options */
|
|
42
|
+
workerOptions?: WorkerOptions
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* OPFS Worker Proxy - runs OPFS operations in a Web Worker
|
|
47
|
+
*
|
|
48
|
+
* Benefits:
|
|
49
|
+
* - Non-blocking main thread
|
|
50
|
+
* - Uses sync access handles (faster) in the worker
|
|
51
|
+
* - Compatible with libraries that reuse buffers (e.g., isomorphic-git)
|
|
52
|
+
*/
|
|
53
|
+
export class OPFSWorker {
|
|
54
|
+
private worker: Worker | null = null
|
|
55
|
+
private pendingRequests = new Map<number, PendingRequest>()
|
|
56
|
+
private nextId = 1
|
|
57
|
+
private readyPromise: Promise<void>
|
|
58
|
+
private readyResolve!: () => void
|
|
59
|
+
|
|
60
|
+
/** File system constants */
|
|
61
|
+
public readonly constants = constants
|
|
62
|
+
|
|
63
|
+
constructor(options: OPFSWorkerOptions = {}) {
|
|
64
|
+
this.readyPromise = new Promise((resolve) => {
|
|
65
|
+
this.readyResolve = resolve
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
this.initWorker(options)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private initWorker(options: OPFSWorkerOptions): void {
|
|
72
|
+
const { workerUrl, workerOptions = { type: 'module' } } = options
|
|
73
|
+
|
|
74
|
+
if (workerUrl) {
|
|
75
|
+
this.worker = new Worker(workerUrl, workerOptions)
|
|
76
|
+
} else {
|
|
77
|
+
// Try to create worker from the bundled script
|
|
78
|
+
// Users should provide workerUrl in production
|
|
79
|
+
throw new Error(
|
|
80
|
+
'OPFSWorker requires a workerUrl option pointing to the worker script. ' +
|
|
81
|
+
'Example: new OPFSWorker({ workerUrl: new URL("./opfs-worker.js", import.meta.url) })'
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.worker.onmessage = (event: MessageEvent<WorkerResponse>) => {
|
|
86
|
+
const { id, type, result, error } = event.data
|
|
87
|
+
|
|
88
|
+
// Handle ready signal
|
|
89
|
+
if (type === 'ready') {
|
|
90
|
+
this.readyResolve()
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle response to a request
|
|
95
|
+
if (id !== undefined) {
|
|
96
|
+
const pending = this.pendingRequests.get(id)
|
|
97
|
+
if (pending) {
|
|
98
|
+
this.pendingRequests.delete(id)
|
|
99
|
+
if (error) {
|
|
100
|
+
const fsError = new FSError(error.message, error.code || 'UNKNOWN')
|
|
101
|
+
pending.reject(fsError)
|
|
102
|
+
} else {
|
|
103
|
+
pending.resolve(result)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
this.worker.onerror = (event) => {
|
|
110
|
+
console.error('[OPFSWorker] Worker error:', event)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Wait for the worker to be ready
|
|
116
|
+
*/
|
|
117
|
+
async ready(): Promise<void> {
|
|
118
|
+
return this.readyPromise
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Terminate the worker
|
|
123
|
+
*/
|
|
124
|
+
terminate(): void {
|
|
125
|
+
if (this.worker) {
|
|
126
|
+
this.worker.terminate()
|
|
127
|
+
this.worker = null
|
|
128
|
+
|
|
129
|
+
// Reject all pending requests
|
|
130
|
+
for (const [, pending] of this.pendingRequests) {
|
|
131
|
+
pending.reject(new Error('Worker terminated'))
|
|
132
|
+
}
|
|
133
|
+
this.pendingRequests.clear()
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private call<T>(method: string, args: unknown[], transfer?: Transferable[]): Promise<T> {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
if (!this.worker) {
|
|
140
|
+
reject(new Error('Worker not initialized or terminated'))
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const id = this.nextId++
|
|
145
|
+
this.pendingRequests.set(id, {
|
|
146
|
+
resolve: resolve as (value: unknown) => void,
|
|
147
|
+
reject
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const message = { id, method, args }
|
|
151
|
+
if (transfer && transfer.length > 0) {
|
|
152
|
+
this.worker.postMessage(message, transfer)
|
|
153
|
+
} else {
|
|
154
|
+
this.worker.postMessage(message)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// File operations
|
|
160
|
+
|
|
161
|
+
async readFile(path: string, options?: ReadFileOptions): Promise<string | Uint8Array> {
|
|
162
|
+
const result = await this.call<string | Uint8Array>('readFile', [path, options])
|
|
163
|
+
return result
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async writeFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
|
|
167
|
+
// Note: We don't use Transferables here because the caller may reuse the buffer
|
|
168
|
+
// (e.g., isomorphic-git reuses buffers). Structured cloning copies the data.
|
|
169
|
+
await this.call<void>('writeFile', [path, data, options])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async readFileBatch(paths: string[]): Promise<BatchReadResult[]> {
|
|
173
|
+
return this.call<BatchReadResult[]>('readFileBatch', [paths])
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async writeFileBatch(entries: BatchWriteEntry[]): Promise<void> {
|
|
177
|
+
// Note: We don't use Transferables here because the caller may reuse the buffers
|
|
178
|
+
await this.call<void>('writeFileBatch', [entries])
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async appendFile(path: string, data: string | Uint8Array, options?: WriteFileOptions): Promise<void> {
|
|
182
|
+
// Note: We don't use Transferables here because the caller may reuse the buffer
|
|
183
|
+
await this.call<void>('appendFile', [path, data, options])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async copyFile(src: string, dest: string, mode?: number): Promise<void> {
|
|
187
|
+
await this.call<void>('copyFile', [src, dest, mode])
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async unlink(path: string): Promise<void> {
|
|
191
|
+
await this.call<void>('unlink', [path])
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async truncate(path: string, len?: number): Promise<void> {
|
|
195
|
+
await this.call<void>('truncate', [path, len])
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Directory operations
|
|
199
|
+
|
|
200
|
+
async mkdir(path: string): Promise<void> {
|
|
201
|
+
await this.call<void>('mkdir', [path])
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async rmdir(path: string): Promise<void> {
|
|
205
|
+
await this.call<void>('rmdir', [path])
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async readdir(path: string, options?: ReaddirOptions): Promise<string[] | Dirent[]> {
|
|
209
|
+
const result = await this.call<string[] | { name: string }[]>('readdir', [path, options])
|
|
210
|
+
|
|
211
|
+
// Reconstruct Dirent objects with methods
|
|
212
|
+
if (options?.withFileTypes && Array.isArray(result)) {
|
|
213
|
+
return result.map((item) => {
|
|
214
|
+
if (typeof item === 'object' && 'name' in item) {
|
|
215
|
+
const entry = item as { name: string; _isFile?: boolean; _isDir?: boolean; _isSymlink?: boolean }
|
|
216
|
+
return {
|
|
217
|
+
name: entry.name,
|
|
218
|
+
isFile: () => entry._isFile ?? false,
|
|
219
|
+
isDirectory: () => entry._isDir ?? false,
|
|
220
|
+
isSymbolicLink: () => entry._isSymlink ?? false
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return item as unknown as Dirent
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return result as string[]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async cp(src: string, dest: string, options?: CpOptions): Promise<void> {
|
|
231
|
+
await this.call<void>('cp', [src, dest, options])
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async rm(path: string, options?: RmOptions): Promise<void> {
|
|
235
|
+
await this.call<void>('rm', [path, options])
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Stat operations
|
|
239
|
+
|
|
240
|
+
async stat(path: string): Promise<Stats> {
|
|
241
|
+
const result = await this.call<{
|
|
242
|
+
type: string
|
|
243
|
+
size: number
|
|
244
|
+
mode: number
|
|
245
|
+
ctime: string
|
|
246
|
+
ctimeMs: number
|
|
247
|
+
mtime: string
|
|
248
|
+
mtimeMs: number
|
|
249
|
+
target?: string
|
|
250
|
+
}>('stat', [path])
|
|
251
|
+
|
|
252
|
+
return this.deserializeStats(result)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async lstat(path: string): Promise<Stats> {
|
|
256
|
+
const result = await this.call<{
|
|
257
|
+
type: string
|
|
258
|
+
size: number
|
|
259
|
+
mode: number
|
|
260
|
+
ctime: string
|
|
261
|
+
ctimeMs: number
|
|
262
|
+
mtime: string
|
|
263
|
+
mtimeMs: number
|
|
264
|
+
target?: string
|
|
265
|
+
}>('lstat', [path])
|
|
266
|
+
|
|
267
|
+
return this.deserializeStats(result)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private deserializeStats(data: {
|
|
271
|
+
type: string
|
|
272
|
+
size: number
|
|
273
|
+
mode: number
|
|
274
|
+
ctime: string
|
|
275
|
+
ctimeMs: number
|
|
276
|
+
mtime: string
|
|
277
|
+
mtimeMs: number
|
|
278
|
+
target?: string
|
|
279
|
+
}): Stats {
|
|
280
|
+
const ctime = new Date(data.ctime)
|
|
281
|
+
const mtime = new Date(data.mtime)
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
type: data.type as 'file' | 'dir' | 'symlink',
|
|
285
|
+
size: data.size,
|
|
286
|
+
mode: data.mode,
|
|
287
|
+
ctime,
|
|
288
|
+
ctimeMs: data.ctimeMs,
|
|
289
|
+
mtime,
|
|
290
|
+
mtimeMs: data.mtimeMs,
|
|
291
|
+
target: data.target,
|
|
292
|
+
isFile: () => data.type === 'file',
|
|
293
|
+
isDirectory: () => data.type === 'dir',
|
|
294
|
+
isSymbolicLink: () => data.type === 'symlink'
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async exists(path: string): Promise<boolean> {
|
|
299
|
+
return this.call<boolean>('exists', [path])
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async access(path: string, mode?: number): Promise<void> {
|
|
303
|
+
await this.call<void>('access', [path, mode])
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async statfs(path?: string): Promise<StatFs> {
|
|
307
|
+
return this.call<StatFs>('statfs', [path])
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async du(path: string): Promise<DiskUsage> {
|
|
311
|
+
return this.call<DiskUsage>('du', [path])
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Symlink operations
|
|
315
|
+
|
|
316
|
+
async symlink(target: string, path: string): Promise<void> {
|
|
317
|
+
await this.call<void>('symlink', [target, path])
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async readlink(path: string): Promise<string> {
|
|
321
|
+
return this.call<string>('readlink', [path])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async symlinkBatch(links: SymlinkDefinition[]): Promise<void> {
|
|
325
|
+
await this.call<void>('symlinkBatch', [links])
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async realpath(path: string): Promise<string> {
|
|
329
|
+
return this.call<string>('realpath', [path])
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Other operations
|
|
333
|
+
|
|
334
|
+
async rename(oldPath: string, newPath: string): Promise<void> {
|
|
335
|
+
await this.call<void>('rename', [oldPath, newPath])
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async mkdtemp(prefix: string): Promise<string> {
|
|
339
|
+
return this.call<string>('mkdtemp', [prefix])
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async chmod(path: string, mode: number): Promise<void> {
|
|
343
|
+
await this.call<void>('chmod', [path, mode])
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async chown(path: string, uid: number, gid: number): Promise<void> {
|
|
347
|
+
await this.call<void>('chown', [path, uid, gid])
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async utimes(path: string, atime: Date | number, mtime: Date | number): Promise<void> {
|
|
351
|
+
await this.call<void>('utimes', [path, atime, mtime])
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async lutimes(path: string, atime: Date | number, mtime: Date | number): Promise<void> {
|
|
355
|
+
await this.call<void>('lutimes', [path, atime, mtime])
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Reset internal caches to free memory
|
|
360
|
+
* Useful for long-running benchmarks or after bulk operations
|
|
361
|
+
*/
|
|
362
|
+
async resetCache(): Promise<void> {
|
|
363
|
+
await this.call<void>('resetCache', [])
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Force full garbage collection by reinitializing the OPFS instance in the worker
|
|
368
|
+
* This completely releases all handles and caches, preventing memory leaks in long-running operations
|
|
369
|
+
* More aggressive than resetCache() - use when resetCache() isn't sufficient
|
|
370
|
+
*/
|
|
371
|
+
async gc(): Promise<void> {
|
|
372
|
+
await this.call<void>('gc', [])
|
|
373
|
+
}
|
|
374
|
+
}
|