@componentor/fs 1.2.8 → 2.0.0
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 +398 -634
- package/dist/index.cjs +2637 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +606 -0
- package/dist/index.d.ts +539 -481
- package/dist/index.js +2358 -2480
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +496 -0
- package/dist/kernel.js.map +1 -0
- package/package.json +39 -45
- package/dist/opfs-hybrid.d.ts +0 -198
- package/dist/opfs-hybrid.js +0 -2743
- package/dist/opfs-hybrid.js.map +0 -1
- package/dist/opfs-worker-proxy.d.ts +0 -224
- package/dist/opfs-worker-proxy.js +0 -274
- package/dist/opfs-worker-proxy.js.map +0 -1
- package/dist/opfs-worker.js +0 -2923
- package/dist/opfs-worker.js.map +0 -1
- package/src/constants.ts +0 -52
- package/src/errors.ts +0 -88
- package/src/file-handle.ts +0 -100
- package/src/global.d.ts +0 -57
- package/src/handle-manager.ts +0 -302
- package/src/index.ts +0 -1416
- package/src/opfs-hybrid.ts +0 -265
- package/src/opfs-worker-proxy.ts +0 -374
- package/src/opfs-worker.ts +0 -253
- package/src/packed-storage.ts +0 -604
- package/src/path-utils.ts +0 -97
- package/src/streams.ts +0 -109
- package/src/symlink-manager.ts +0 -338
- package/src/types.ts +0 -289
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2637 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/path.ts
|
|
12
|
+
var path_exports = {};
|
|
13
|
+
__export(path_exports, {
|
|
14
|
+
basename: () => basename,
|
|
15
|
+
default: () => path_default,
|
|
16
|
+
delimiter: () => delimiter,
|
|
17
|
+
dirname: () => dirname,
|
|
18
|
+
extname: () => extname,
|
|
19
|
+
format: () => format,
|
|
20
|
+
isAbsolute: () => isAbsolute,
|
|
21
|
+
join: () => join,
|
|
22
|
+
normalize: () => normalize,
|
|
23
|
+
parse: () => parse,
|
|
24
|
+
posix: () => posix,
|
|
25
|
+
relative: () => relative,
|
|
26
|
+
resolve: () => resolve,
|
|
27
|
+
sep: () => sep
|
|
28
|
+
});
|
|
29
|
+
var sep = "/";
|
|
30
|
+
var delimiter = ":";
|
|
31
|
+
function normalize(p) {
|
|
32
|
+
if (p.length === 0) return ".";
|
|
33
|
+
const isAbsolute2 = p.charCodeAt(0) === 47;
|
|
34
|
+
const trailingSlash = p.charCodeAt(p.length - 1) === 47;
|
|
35
|
+
const segments = p.split("/");
|
|
36
|
+
const result = [];
|
|
37
|
+
for (const segment of segments) {
|
|
38
|
+
if (segment === "" || segment === ".") {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (segment === "..") {
|
|
42
|
+
if (result.length > 0 && result[result.length - 1] !== "..") {
|
|
43
|
+
result.pop();
|
|
44
|
+
} else if (!isAbsolute2) {
|
|
45
|
+
result.push("..");
|
|
46
|
+
}
|
|
47
|
+
} else {
|
|
48
|
+
result.push(segment);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
let normalized = result.join("/");
|
|
52
|
+
if (isAbsolute2) {
|
|
53
|
+
normalized = "/" + normalized;
|
|
54
|
+
}
|
|
55
|
+
if (trailingSlash && normalized.length > 1) {
|
|
56
|
+
normalized += "/";
|
|
57
|
+
}
|
|
58
|
+
return normalized || (isAbsolute2 ? "/" : ".");
|
|
59
|
+
}
|
|
60
|
+
function join(...paths) {
|
|
61
|
+
if (paths.length === 0) return ".";
|
|
62
|
+
let joined;
|
|
63
|
+
for (const path of paths) {
|
|
64
|
+
if (path.length > 0) {
|
|
65
|
+
if (joined === void 0) {
|
|
66
|
+
joined = path;
|
|
67
|
+
} else {
|
|
68
|
+
joined += "/" + path;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (joined === void 0) return ".";
|
|
73
|
+
return normalize(joined);
|
|
74
|
+
}
|
|
75
|
+
function resolve(...paths) {
|
|
76
|
+
let resolvedPath = "";
|
|
77
|
+
let resolvedAbsolute = false;
|
|
78
|
+
for (let i = paths.length - 1; i >= -1 && !resolvedAbsolute; i--) {
|
|
79
|
+
const path = i >= 0 ? paths[i] : "/";
|
|
80
|
+
if (path == null || path.length === 0) continue;
|
|
81
|
+
resolvedPath = resolvedPath ? path + "/" + resolvedPath : path;
|
|
82
|
+
resolvedAbsolute = path.charCodeAt(0) === 47;
|
|
83
|
+
}
|
|
84
|
+
resolvedPath = normalize(resolvedPath);
|
|
85
|
+
if (resolvedPath.length > 1 && resolvedPath.endsWith("/")) {
|
|
86
|
+
resolvedPath = resolvedPath.slice(0, -1);
|
|
87
|
+
}
|
|
88
|
+
if (resolvedAbsolute) {
|
|
89
|
+
return resolvedPath.length > 0 ? resolvedPath : "/";
|
|
90
|
+
}
|
|
91
|
+
return resolvedPath.length > 0 ? resolvedPath : ".";
|
|
92
|
+
}
|
|
93
|
+
function isAbsolute(p) {
|
|
94
|
+
return p.length > 0 && p.charCodeAt(0) === 47;
|
|
95
|
+
}
|
|
96
|
+
function dirname(p) {
|
|
97
|
+
if (p.length === 0) return ".";
|
|
98
|
+
const hasRoot = p.charCodeAt(0) === 47;
|
|
99
|
+
let end = -1;
|
|
100
|
+
let matchedSlash = true;
|
|
101
|
+
for (let i = p.length - 1; i >= 1; --i) {
|
|
102
|
+
if (p.charCodeAt(i) === 47) {
|
|
103
|
+
if (!matchedSlash) {
|
|
104
|
+
end = i;
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
} else {
|
|
108
|
+
matchedSlash = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (end === -1) return hasRoot ? "/" : ".";
|
|
112
|
+
if (hasRoot && end === 1) return "//";
|
|
113
|
+
return p.slice(0, end);
|
|
114
|
+
}
|
|
115
|
+
function basename(p, ext) {
|
|
116
|
+
let start = 0;
|
|
117
|
+
let end = -1;
|
|
118
|
+
let matchedSlash = true;
|
|
119
|
+
for (let i = p.length - 1; i >= 0; --i) {
|
|
120
|
+
if (p.charCodeAt(i) === 47) {
|
|
121
|
+
if (!matchedSlash) {
|
|
122
|
+
start = i + 1;
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
} else if (end === -1) {
|
|
126
|
+
matchedSlash = false;
|
|
127
|
+
end = i + 1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (end === -1) return "";
|
|
131
|
+
const base = p.slice(start, end);
|
|
132
|
+
if (ext && base.endsWith(ext)) {
|
|
133
|
+
return base.slice(0, base.length - ext.length);
|
|
134
|
+
}
|
|
135
|
+
return base;
|
|
136
|
+
}
|
|
137
|
+
function extname(p) {
|
|
138
|
+
let startDot = -1;
|
|
139
|
+
let startPart = 0;
|
|
140
|
+
let end = -1;
|
|
141
|
+
let matchedSlash = true;
|
|
142
|
+
let preDotState = 0;
|
|
143
|
+
for (let i = p.length - 1; i >= 0; --i) {
|
|
144
|
+
const code = p.charCodeAt(i);
|
|
145
|
+
if (code === 47) {
|
|
146
|
+
if (!matchedSlash) {
|
|
147
|
+
startPart = i + 1;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (end === -1) {
|
|
153
|
+
matchedSlash = false;
|
|
154
|
+
end = i + 1;
|
|
155
|
+
}
|
|
156
|
+
if (code === 46) {
|
|
157
|
+
if (startDot === -1) {
|
|
158
|
+
startDot = i;
|
|
159
|
+
} else if (preDotState !== 1) {
|
|
160
|
+
preDotState = 1;
|
|
161
|
+
}
|
|
162
|
+
} else if (startDot !== -1) {
|
|
163
|
+
preDotState = -1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
|
167
|
+
return "";
|
|
168
|
+
}
|
|
169
|
+
return p.slice(startDot, end);
|
|
170
|
+
}
|
|
171
|
+
function relative(from, to) {
|
|
172
|
+
if (from === to) return "";
|
|
173
|
+
from = resolve(from);
|
|
174
|
+
to = resolve(to);
|
|
175
|
+
if (from === to) return "";
|
|
176
|
+
const fromParts = from.split("/").filter(Boolean);
|
|
177
|
+
const toParts = to.split("/").filter(Boolean);
|
|
178
|
+
let commonLength = 0;
|
|
179
|
+
const minLength = Math.min(fromParts.length, toParts.length);
|
|
180
|
+
for (let i = 0; i < minLength; i++) {
|
|
181
|
+
if (fromParts[i] === toParts[i]) {
|
|
182
|
+
commonLength++;
|
|
183
|
+
} else {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const upCount = fromParts.length - commonLength;
|
|
188
|
+
const relativeParts = [];
|
|
189
|
+
for (let i = 0; i < upCount; i++) {
|
|
190
|
+
relativeParts.push("..");
|
|
191
|
+
}
|
|
192
|
+
for (let i = commonLength; i < toParts.length; i++) {
|
|
193
|
+
relativeParts.push(toParts[i]);
|
|
194
|
+
}
|
|
195
|
+
return relativeParts.join("/") || ".";
|
|
196
|
+
}
|
|
197
|
+
function parse(p) {
|
|
198
|
+
const ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
|
199
|
+
if (p.length === 0) return ret;
|
|
200
|
+
const isAbsolutePath = p.charCodeAt(0) === 47;
|
|
201
|
+
if (isAbsolutePath) {
|
|
202
|
+
ret.root = "/";
|
|
203
|
+
}
|
|
204
|
+
let start = 0;
|
|
205
|
+
let end = -1;
|
|
206
|
+
let startDot = -1;
|
|
207
|
+
let matchedSlash = true;
|
|
208
|
+
let preDotState = 0;
|
|
209
|
+
for (let i = p.length - 1; i >= 0; --i) {
|
|
210
|
+
const code = p.charCodeAt(i);
|
|
211
|
+
if (code === 47) {
|
|
212
|
+
if (!matchedSlash) {
|
|
213
|
+
start = i + 1;
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (end === -1) {
|
|
219
|
+
matchedSlash = false;
|
|
220
|
+
end = i + 1;
|
|
221
|
+
}
|
|
222
|
+
if (code === 46) {
|
|
223
|
+
if (startDot === -1) {
|
|
224
|
+
startDot = i;
|
|
225
|
+
} else if (preDotState !== 1) {
|
|
226
|
+
preDotState = 1;
|
|
227
|
+
}
|
|
228
|
+
} else if (startDot !== -1) {
|
|
229
|
+
preDotState = -1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (end !== -1) {
|
|
233
|
+
if (startDot === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === start + 1) {
|
|
234
|
+
ret.base = p.slice(start, end);
|
|
235
|
+
ret.name = ret.base;
|
|
236
|
+
} else {
|
|
237
|
+
ret.name = p.slice(start, startDot);
|
|
238
|
+
ret.base = p.slice(start, end);
|
|
239
|
+
ret.ext = p.slice(startDot, end);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (start > 0) {
|
|
243
|
+
ret.dir = p.slice(0, start - 1);
|
|
244
|
+
} else if (isAbsolutePath) {
|
|
245
|
+
ret.dir = "/";
|
|
246
|
+
}
|
|
247
|
+
return ret;
|
|
248
|
+
}
|
|
249
|
+
function format(pathObject) {
|
|
250
|
+
const dir = pathObject.dir || pathObject.root || "";
|
|
251
|
+
const base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
|
252
|
+
if (!dir) return base;
|
|
253
|
+
if (dir === pathObject.root) return dir + base;
|
|
254
|
+
return dir + "/" + base;
|
|
255
|
+
}
|
|
256
|
+
var posix = {
|
|
257
|
+
sep,
|
|
258
|
+
delimiter,
|
|
259
|
+
normalize,
|
|
260
|
+
join,
|
|
261
|
+
resolve,
|
|
262
|
+
isAbsolute,
|
|
263
|
+
dirname,
|
|
264
|
+
basename,
|
|
265
|
+
extname,
|
|
266
|
+
relative,
|
|
267
|
+
parse,
|
|
268
|
+
format
|
|
269
|
+
};
|
|
270
|
+
var path_default = posix;
|
|
271
|
+
|
|
272
|
+
// src/constants.ts
|
|
273
|
+
var constants = {
|
|
274
|
+
// File access constants
|
|
275
|
+
F_OK: 0,
|
|
276
|
+
R_OK: 4,
|
|
277
|
+
W_OK: 2,
|
|
278
|
+
X_OK: 1,
|
|
279
|
+
// File copy constants
|
|
280
|
+
COPYFILE_EXCL: 1,
|
|
281
|
+
COPYFILE_FICLONE: 2,
|
|
282
|
+
COPYFILE_FICLONE_FORCE: 4,
|
|
283
|
+
// File open constants
|
|
284
|
+
O_RDONLY: 0,
|
|
285
|
+
O_WRONLY: 1,
|
|
286
|
+
O_RDWR: 2,
|
|
287
|
+
O_CREAT: 64,
|
|
288
|
+
O_EXCL: 128,
|
|
289
|
+
O_TRUNC: 512,
|
|
290
|
+
O_APPEND: 1024,
|
|
291
|
+
O_SYNC: 4096,
|
|
292
|
+
// File type constants
|
|
293
|
+
S_IFMT: 61440,
|
|
294
|
+
S_IFREG: 32768,
|
|
295
|
+
S_IFDIR: 16384,
|
|
296
|
+
S_IFCHR: 8192,
|
|
297
|
+
S_IFBLK: 24576,
|
|
298
|
+
S_IFIFO: 4096,
|
|
299
|
+
S_IFLNK: 40960,
|
|
300
|
+
S_IFSOCK: 49152,
|
|
301
|
+
// File mode constants
|
|
302
|
+
S_IRWXU: 448,
|
|
303
|
+
S_IRUSR: 256,
|
|
304
|
+
S_IWUSR: 128,
|
|
305
|
+
S_IXUSR: 64,
|
|
306
|
+
S_IRWXG: 56,
|
|
307
|
+
S_IRGRP: 32,
|
|
308
|
+
S_IWGRP: 16,
|
|
309
|
+
S_IXGRP: 8,
|
|
310
|
+
S_IRWXO: 7,
|
|
311
|
+
S_IROTH: 4,
|
|
312
|
+
S_IWOTH: 2,
|
|
313
|
+
S_IXOTH: 1
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
// src/errors.ts
|
|
317
|
+
var FSError = class _FSError extends Error {
|
|
318
|
+
code;
|
|
319
|
+
errno;
|
|
320
|
+
syscall;
|
|
321
|
+
path;
|
|
322
|
+
constructor(code, errno, message, syscall, path) {
|
|
323
|
+
super(message);
|
|
324
|
+
this.name = "FSError";
|
|
325
|
+
this.code = code;
|
|
326
|
+
this.errno = errno;
|
|
327
|
+
this.syscall = syscall;
|
|
328
|
+
this.path = path;
|
|
329
|
+
const ErrorWithCapture = Error;
|
|
330
|
+
if (ErrorWithCapture.captureStackTrace) {
|
|
331
|
+
ErrorWithCapture.captureStackTrace(this, _FSError);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
var ErrorCodes = {
|
|
336
|
+
ENOENT: -2,
|
|
337
|
+
EEXIST: -17,
|
|
338
|
+
EISDIR: -21,
|
|
339
|
+
ENOTDIR: -20,
|
|
340
|
+
ENOTEMPTY: -39,
|
|
341
|
+
EACCES: -13,
|
|
342
|
+
EINVAL: -22,
|
|
343
|
+
ENOSPC: -28};
|
|
344
|
+
function createENOENT(syscall, path) {
|
|
345
|
+
return new FSError(
|
|
346
|
+
"ENOENT",
|
|
347
|
+
ErrorCodes.ENOENT,
|
|
348
|
+
`ENOENT: no such file or directory, ${syscall} '${path}'`,
|
|
349
|
+
syscall,
|
|
350
|
+
path
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
function createEEXIST(syscall, path) {
|
|
354
|
+
return new FSError(
|
|
355
|
+
"EEXIST",
|
|
356
|
+
ErrorCodes.EEXIST,
|
|
357
|
+
`EEXIST: file already exists, ${syscall} '${path}'`,
|
|
358
|
+
syscall,
|
|
359
|
+
path
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
function createEISDIR(syscall, path) {
|
|
363
|
+
return new FSError(
|
|
364
|
+
"EISDIR",
|
|
365
|
+
ErrorCodes.EISDIR,
|
|
366
|
+
`EISDIR: illegal operation on a directory, ${syscall} '${path}'`,
|
|
367
|
+
syscall,
|
|
368
|
+
path
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
function createENOTDIR(syscall, path) {
|
|
372
|
+
return new FSError(
|
|
373
|
+
"ENOTDIR",
|
|
374
|
+
ErrorCodes.ENOTDIR,
|
|
375
|
+
`ENOTDIR: not a directory, ${syscall} '${path}'`,
|
|
376
|
+
syscall,
|
|
377
|
+
path
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function createENOTEMPTY(syscall, path) {
|
|
381
|
+
return new FSError(
|
|
382
|
+
"ENOTEMPTY",
|
|
383
|
+
ErrorCodes.ENOTEMPTY,
|
|
384
|
+
`ENOTEMPTY: directory not empty, ${syscall} '${path}'`,
|
|
385
|
+
syscall,
|
|
386
|
+
path
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
function createEACCES(syscall, path) {
|
|
390
|
+
return new FSError(
|
|
391
|
+
"EACCES",
|
|
392
|
+
ErrorCodes.EACCES,
|
|
393
|
+
`EACCES: permission denied, ${syscall} '${path}'`,
|
|
394
|
+
syscall,
|
|
395
|
+
path
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
function createEINVAL(syscall, path) {
|
|
399
|
+
return new FSError(
|
|
400
|
+
"EINVAL",
|
|
401
|
+
ErrorCodes.EINVAL,
|
|
402
|
+
`EINVAL: invalid argument, ${syscall} '${path}'`,
|
|
403
|
+
syscall,
|
|
404
|
+
path
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
function mapErrorCode(errorName, syscall, path) {
|
|
408
|
+
switch (errorName) {
|
|
409
|
+
case "NotFoundError":
|
|
410
|
+
return createENOENT(syscall, path);
|
|
411
|
+
case "NotAllowedError":
|
|
412
|
+
return createEACCES(syscall, path);
|
|
413
|
+
case "TypeMismatchError":
|
|
414
|
+
return createENOTDIR(syscall, path);
|
|
415
|
+
case "InvalidModificationError":
|
|
416
|
+
return createENOTEMPTY(syscall, path);
|
|
417
|
+
case "QuotaExceededError":
|
|
418
|
+
return new FSError("ENOSPC", ErrorCodes.ENOSPC, `ENOSPC: no space left on device, ${syscall} '${path}'`, syscall, path);
|
|
419
|
+
default:
|
|
420
|
+
return new FSError("EINVAL", ErrorCodes.EINVAL, `${errorName}: ${syscall} '${path}'`, syscall, path);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// src/filesystem.ts
|
|
425
|
+
var isWorkerContext = typeof WorkerGlobalScope !== "undefined" && self instanceof WorkerGlobalScope;
|
|
426
|
+
var KERNEL_SOURCE = `
|
|
427
|
+
const LOCK_NAME = 'opfs_fs_lock';
|
|
428
|
+
let messageQueue = [];
|
|
429
|
+
let isReady = false;
|
|
430
|
+
let cachedRoot = null;
|
|
431
|
+
const dirCache = new Map();
|
|
432
|
+
|
|
433
|
+
// Sync handle cache - MAJOR performance optimization
|
|
434
|
+
const syncHandleCache = new Map();
|
|
435
|
+
const MAX_HANDLES = 100;
|
|
436
|
+
|
|
437
|
+
async function getSyncHandle(filePath, create) {
|
|
438
|
+
const cached = syncHandleCache.get(filePath);
|
|
439
|
+
if (cached) return cached;
|
|
440
|
+
|
|
441
|
+
// Evict oldest handles if cache is full
|
|
442
|
+
if (syncHandleCache.size >= MAX_HANDLES) {
|
|
443
|
+
const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
|
|
444
|
+
for (const key of keys) {
|
|
445
|
+
const h = syncHandleCache.get(key);
|
|
446
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const fh = await getFileHandle(filePath, create);
|
|
451
|
+
const access = await fh.createSyncAccessHandle();
|
|
452
|
+
syncHandleCache.set(filePath, access);
|
|
453
|
+
return access;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function closeSyncHandle(filePath) {
|
|
457
|
+
const h = syncHandleCache.get(filePath);
|
|
458
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function closeHandlesUnder(prefix) {
|
|
462
|
+
for (const [p, h] of syncHandleCache) {
|
|
463
|
+
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
464
|
+
try { h.close(); } catch {}
|
|
465
|
+
syncHandleCache.delete(p);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Clear directory cache entries for a path and all descendants
|
|
471
|
+
function clearDirCacheUnder(filePath) {
|
|
472
|
+
// Convert to cache key format (no leading slash)
|
|
473
|
+
const prefix = parsePath(filePath).join('/');
|
|
474
|
+
if (!prefix) {
|
|
475
|
+
// Root directory - clear everything
|
|
476
|
+
dirCache.clear();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
for (const key of dirCache.keys()) {
|
|
480
|
+
if (key === prefix || key.startsWith(prefix + '/')) {
|
|
481
|
+
dirCache.delete(key);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function getRoot() {
|
|
487
|
+
if (!cachedRoot) {
|
|
488
|
+
cachedRoot = await navigator.storage.getDirectory();
|
|
489
|
+
}
|
|
490
|
+
return cachedRoot;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function parsePath(filePath) {
|
|
494
|
+
return filePath.split('/').filter(Boolean);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async function getDirectoryHandle(parts, create = false) {
|
|
498
|
+
if (parts.length === 0) return getRoot();
|
|
499
|
+
|
|
500
|
+
const cacheKey = parts.join('/');
|
|
501
|
+
if (dirCache.has(cacheKey)) {
|
|
502
|
+
return dirCache.get(cacheKey);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let curr = await getRoot();
|
|
506
|
+
let pathSoFar = '';
|
|
507
|
+
|
|
508
|
+
for (const part of parts) {
|
|
509
|
+
pathSoFar += (pathSoFar ? '/' : '') + part;
|
|
510
|
+
|
|
511
|
+
if (dirCache.has(pathSoFar)) {
|
|
512
|
+
curr = dirCache.get(pathSoFar);
|
|
513
|
+
} else {
|
|
514
|
+
curr = await curr.getDirectoryHandle(part, { create });
|
|
515
|
+
dirCache.set(pathSoFar, curr);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
return curr;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function getFileHandle(filePath, create = false) {
|
|
523
|
+
const parts = parsePath(filePath);
|
|
524
|
+
const fileName = parts.pop();
|
|
525
|
+
if (!fileName) throw new Error('Invalid file path');
|
|
526
|
+
const dir = parts.length > 0 ? await getDirectoryHandle(parts, create) : await getRoot();
|
|
527
|
+
return await dir.getFileHandle(fileName, { create });
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function getParentAndName(filePath) {
|
|
531
|
+
const parts = parsePath(filePath);
|
|
532
|
+
const name = parts.pop();
|
|
533
|
+
if (!name) throw new Error('Invalid path');
|
|
534
|
+
const parent = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
|
|
535
|
+
return { parent, name };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async function handleRead(filePath, payload) {
|
|
539
|
+
const access = await getSyncHandle(filePath, false);
|
|
540
|
+
const size = access.getSize();
|
|
541
|
+
const offset = payload?.offset || 0;
|
|
542
|
+
const len = payload?.len || (size - offset);
|
|
543
|
+
const buf = new Uint8Array(len);
|
|
544
|
+
const bytesRead = access.read(buf, { at: offset });
|
|
545
|
+
return { data: buf.slice(0, bytesRead) };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function handleWrite(filePath, payload) {
|
|
549
|
+
const access = await getSyncHandle(filePath, true);
|
|
550
|
+
if (payload?.data) {
|
|
551
|
+
const offset = payload.offset ?? 0;
|
|
552
|
+
if (offset === 0) access.truncate(0);
|
|
553
|
+
access.write(payload.data, { at: offset });
|
|
554
|
+
// Only flush if explicitly requested (default: true for safety)
|
|
555
|
+
if (payload?.flush !== false) access.flush();
|
|
556
|
+
}
|
|
557
|
+
return { success: true };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function handleAppend(filePath, payload) {
|
|
561
|
+
const access = await getSyncHandle(filePath, true);
|
|
562
|
+
if (payload?.data) {
|
|
563
|
+
const size = access.getSize();
|
|
564
|
+
access.write(payload.data, { at: size });
|
|
565
|
+
if (payload?.flush !== false) access.flush();
|
|
566
|
+
}
|
|
567
|
+
return { success: true };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function handleTruncate(filePath, payload) {
|
|
571
|
+
const access = await getSyncHandle(filePath, false);
|
|
572
|
+
access.truncate(payload?.len ?? 0);
|
|
573
|
+
access.flush();
|
|
574
|
+
return { success: true };
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function handleStat(filePath) {
|
|
578
|
+
const parts = parsePath(filePath);
|
|
579
|
+
// Node.js compatible stat shape: mode 33188 = file (0o100644), 16877 = dir (0o40755)
|
|
580
|
+
if (parts.length === 0) {
|
|
581
|
+
return { size: 0, mtimeMs: Date.now(), mode: 16877, type: 'directory' };
|
|
582
|
+
}
|
|
583
|
+
const name = parts.pop();
|
|
584
|
+
const parent = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
|
|
585
|
+
try {
|
|
586
|
+
const fh = await parent.getFileHandle(name);
|
|
587
|
+
// Use getFile() for metadata - faster than createSyncAccessHandle
|
|
588
|
+
const file = await fh.getFile();
|
|
589
|
+
return { size: file.size, mtimeMs: file.lastModified, mode: 33188, type: 'file' };
|
|
590
|
+
} catch {
|
|
591
|
+
try {
|
|
592
|
+
await parent.getDirectoryHandle(name);
|
|
593
|
+
return { size: 0, mtimeMs: Date.now(), mode: 16877, type: 'directory' };
|
|
594
|
+
} catch {
|
|
595
|
+
throw new Error('NotFoundError');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
async function handleExists(filePath) {
|
|
601
|
+
try {
|
|
602
|
+
await handleStat(filePath);
|
|
603
|
+
return { exists: true };
|
|
604
|
+
} catch {
|
|
605
|
+
return { exists: false };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
async function handleMkdir(filePath, payload) {
|
|
610
|
+
const parts = parsePath(filePath);
|
|
611
|
+
if (payload?.recursive) {
|
|
612
|
+
let curr = await getRoot();
|
|
613
|
+
for (const part of parts) {
|
|
614
|
+
curr = await curr.getDirectoryHandle(part, { create: true });
|
|
615
|
+
}
|
|
616
|
+
} else {
|
|
617
|
+
const name = parts.pop();
|
|
618
|
+
if (!name) throw new Error('Invalid path');
|
|
619
|
+
const parent = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
|
|
620
|
+
await parent.getDirectoryHandle(name, { create: true });
|
|
621
|
+
}
|
|
622
|
+
return { success: true };
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function handleRmdir(filePath, payload) {
|
|
626
|
+
closeHandlesUnder(filePath); // Close all cached file handles under this directory
|
|
627
|
+
clearDirCacheUnder(filePath); // Clear stale directory cache entries
|
|
628
|
+
const { parent, name } = await getParentAndName(filePath);
|
|
629
|
+
if (payload?.recursive) {
|
|
630
|
+
await parent.removeEntry(name, { recursive: true });
|
|
631
|
+
} else {
|
|
632
|
+
const dir = await parent.getDirectoryHandle(name);
|
|
633
|
+
const entries = dir.entries();
|
|
634
|
+
const first = await entries.next();
|
|
635
|
+
if (!first.done) {
|
|
636
|
+
const e = new Error('InvalidModificationError');
|
|
637
|
+
e.name = 'InvalidModificationError';
|
|
638
|
+
throw e;
|
|
639
|
+
}
|
|
640
|
+
await parent.removeEntry(name);
|
|
641
|
+
}
|
|
642
|
+
return { success: true };
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function handleUnlink(filePath) {
|
|
646
|
+
closeSyncHandle(filePath); // Close cached handle before deleting
|
|
647
|
+
const { parent, name } = await getParentAndName(filePath);
|
|
648
|
+
await parent.removeEntry(name);
|
|
649
|
+
return { success: true };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function handleReaddir(filePath) {
|
|
653
|
+
const parts = parsePath(filePath);
|
|
654
|
+
const dir = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
|
|
655
|
+
const entries = [];
|
|
656
|
+
for await (const [name] of dir.entries()) {
|
|
657
|
+
entries.push(name);
|
|
658
|
+
}
|
|
659
|
+
return { entries };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function handleRename(oldPath, payload) {
|
|
663
|
+
if (!payload?.newPath) throw new Error('newPath required');
|
|
664
|
+
const newPath = payload.newPath;
|
|
665
|
+
|
|
666
|
+
// Close cached handles for old path (file will be deleted)
|
|
667
|
+
closeSyncHandle(oldPath);
|
|
668
|
+
closeHandlesUnder(oldPath); // For directory renames
|
|
669
|
+
clearDirCacheUnder(oldPath); // Clear stale directory cache entries
|
|
670
|
+
|
|
671
|
+
const oldParts = parsePath(oldPath);
|
|
672
|
+
const newParts = parsePath(newPath);
|
|
673
|
+
const oldName = oldParts.pop();
|
|
674
|
+
const newName = newParts.pop();
|
|
675
|
+
const oldParent = oldParts.length > 0 ? await getDirectoryHandle(oldParts, false) : await getRoot();
|
|
676
|
+
const newParent = newParts.length > 0 ? await getDirectoryHandle(newParts, true) : await getRoot();
|
|
677
|
+
|
|
678
|
+
try {
|
|
679
|
+
const fh = await oldParent.getFileHandle(oldName);
|
|
680
|
+
const file = await fh.getFile();
|
|
681
|
+
const data = new Uint8Array(await file.arrayBuffer());
|
|
682
|
+
|
|
683
|
+
// Use cached handle for new file
|
|
684
|
+
const access = await getSyncHandle(newPath, true);
|
|
685
|
+
access.truncate(0);
|
|
686
|
+
access.write(data, { at: 0 });
|
|
687
|
+
access.flush();
|
|
688
|
+
|
|
689
|
+
await oldParent.removeEntry(oldName);
|
|
690
|
+
return { success: true };
|
|
691
|
+
} catch {
|
|
692
|
+
const oldDir = await oldParent.getDirectoryHandle(oldName);
|
|
693
|
+
async function copyDir(src, dst, dstPath) {
|
|
694
|
+
for await (const [name, handle] of src.entries()) {
|
|
695
|
+
if (handle.kind === 'file') {
|
|
696
|
+
const srcFile = await handle.getFile();
|
|
697
|
+
const data = new Uint8Array(await srcFile.arrayBuffer());
|
|
698
|
+
const filePath = dstPath + '/' + name;
|
|
699
|
+
const access = await getSyncHandle(filePath, true);
|
|
700
|
+
access.truncate(0);
|
|
701
|
+
access.write(data, { at: 0 });
|
|
702
|
+
access.flush();
|
|
703
|
+
} else {
|
|
704
|
+
const newSubDir = await dst.getDirectoryHandle(name, { create: true });
|
|
705
|
+
await copyDir(handle, newSubDir, dstPath + '/' + name);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
const newDir = await newParent.getDirectoryHandle(newName, { create: true });
|
|
710
|
+
await copyDir(oldDir, newDir, newPath);
|
|
711
|
+
await oldParent.removeEntry(oldName, { recursive: true });
|
|
712
|
+
return { success: true };
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function handleCopy(srcPath, payload) {
|
|
717
|
+
if (!payload?.newPath) throw new Error('newPath required');
|
|
718
|
+
const dstPath = payload.newPath;
|
|
719
|
+
const srcParts = parsePath(srcPath);
|
|
720
|
+
const srcName = srcParts.pop();
|
|
721
|
+
const srcParent = srcParts.length > 0 ? await getDirectoryHandle(srcParts, false) : await getRoot();
|
|
722
|
+
const srcFh = await srcParent.getFileHandle(srcName);
|
|
723
|
+
const srcFile = await srcFh.getFile();
|
|
724
|
+
const data = new Uint8Array(await srcFile.arrayBuffer());
|
|
725
|
+
|
|
726
|
+
// Use cached handle for destination
|
|
727
|
+
const access = await getSyncHandle(dstPath, true);
|
|
728
|
+
access.truncate(0);
|
|
729
|
+
access.write(data, { at: 0 });
|
|
730
|
+
access.flush();
|
|
731
|
+
return { success: true };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function handleFlush() {
|
|
735
|
+
// Flush all cached sync handles
|
|
736
|
+
for (const [, handle] of syncHandleCache) {
|
|
737
|
+
try { handle.flush(); } catch {}
|
|
738
|
+
}
|
|
739
|
+
return { success: true };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function handlePurge() {
|
|
743
|
+
// Flush and close all cached sync handles
|
|
744
|
+
for (const [, handle] of syncHandleCache) {
|
|
745
|
+
try { handle.flush(); handle.close(); } catch {}
|
|
746
|
+
}
|
|
747
|
+
syncHandleCache.clear();
|
|
748
|
+
dirCache.clear();
|
|
749
|
+
cachedRoot = null;
|
|
750
|
+
return { success: true };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async function processMessage(msg) {
|
|
754
|
+
const { type, path, payload } = msg;
|
|
755
|
+
switch (type) {
|
|
756
|
+
case 'read': return handleRead(path, payload);
|
|
757
|
+
case 'write': return handleWrite(path, payload);
|
|
758
|
+
case 'append': return handleAppend(path, payload);
|
|
759
|
+
case 'truncate': return handleTruncate(path, payload);
|
|
760
|
+
case 'stat': return handleStat(path);
|
|
761
|
+
case 'exists': return handleExists(path);
|
|
762
|
+
case 'mkdir': return handleMkdir(path, payload);
|
|
763
|
+
case 'rmdir': return handleRmdir(path, payload);
|
|
764
|
+
case 'unlink': return handleUnlink(path);
|
|
765
|
+
case 'readdir': return handleReaddir(path);
|
|
766
|
+
case 'rename': return handleRename(path, payload);
|
|
767
|
+
case 'copy': return handleCopy(path, payload);
|
|
768
|
+
case 'flush': return handleFlush();
|
|
769
|
+
case 'purge': return handlePurge();
|
|
770
|
+
default: throw new Error('Unknown operation: ' + type);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function sendAtomicsResponse(result, payload) {
|
|
775
|
+
const ctrl = payload.ctrl;
|
|
776
|
+
if (result.data && payload.dataBuffer) {
|
|
777
|
+
const view = new Uint8Array(payload.dataBuffer);
|
|
778
|
+
view.set(result.data);
|
|
779
|
+
Atomics.store(ctrl, 0, result.data.length);
|
|
780
|
+
} else if (result.entries && payload.resultBuffer) {
|
|
781
|
+
const json = JSON.stringify(result);
|
|
782
|
+
const encoded = new TextEncoder().encode(json);
|
|
783
|
+
const view = new Uint8Array(payload.resultBuffer);
|
|
784
|
+
view.set(encoded);
|
|
785
|
+
Atomics.store(ctrl, 0, encoded.length);
|
|
786
|
+
} else if (result.success) {
|
|
787
|
+
Atomics.store(ctrl, 0, 1);
|
|
788
|
+
} else if (result.exists !== undefined) {
|
|
789
|
+
Atomics.store(ctrl, 0, result.exists ? 1 : 0);
|
|
790
|
+
} else if (result.isFile !== undefined) {
|
|
791
|
+
if (payload.resultBuffer) {
|
|
792
|
+
const json = JSON.stringify(result);
|
|
793
|
+
const encoded = new TextEncoder().encode(json);
|
|
794
|
+
const view = new Uint8Array(payload.resultBuffer);
|
|
795
|
+
view.set(encoded);
|
|
796
|
+
Atomics.store(ctrl, 0, encoded.length);
|
|
797
|
+
} else {
|
|
798
|
+
Atomics.store(ctrl, 0, result.size || 0);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
Atomics.notify(ctrl, 0);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Handle incoming messages
|
|
805
|
+
async function handleMessage(msg) {
|
|
806
|
+
const { id, payload } = msg;
|
|
807
|
+
try {
|
|
808
|
+
const result = await processMessage(msg);
|
|
809
|
+
if (payload?.ctrl) {
|
|
810
|
+
sendAtomicsResponse(result, payload);
|
|
811
|
+
} else {
|
|
812
|
+
// Use Transferable for data to avoid copying
|
|
813
|
+
if (result.data) {
|
|
814
|
+
const buffer = result.data.buffer;
|
|
815
|
+
self.postMessage({ id, result }, [buffer]);
|
|
816
|
+
} else {
|
|
817
|
+
self.postMessage({ id, result });
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} catch (e) {
|
|
821
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
822
|
+
// Use error name if it's a specific DOM exception, otherwise use message
|
|
823
|
+
// (handleStat throws new Error('NotFoundError') where message contains the type)
|
|
824
|
+
const errorName = error.name || 'Error';
|
|
825
|
+
const errorCode = errorName !== 'Error' ? errorName : (error.message || 'Error');
|
|
826
|
+
if (payload?.ctrl) {
|
|
827
|
+
Atomics.store(payload.ctrl, 0, -1);
|
|
828
|
+
Atomics.notify(payload.ctrl, 0);
|
|
829
|
+
} else {
|
|
830
|
+
self.postMessage({ id, error: errorCode, code: errorCode });
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Process queued messages after ready
|
|
836
|
+
function processQueue() {
|
|
837
|
+
while (messageQueue.length > 0) {
|
|
838
|
+
const msg = messageQueue.shift();
|
|
839
|
+
handleMessage(msg);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// Handle messages directly - no serialization needed because:
|
|
844
|
+
// - Tier 2: Client awaits response before sending next message
|
|
845
|
+
// - Each OPFSFileSystem instance has its own worker
|
|
846
|
+
self.onmessage = (event) => {
|
|
847
|
+
if (isReady) {
|
|
848
|
+
handleMessage(event.data);
|
|
849
|
+
} else {
|
|
850
|
+
messageQueue.push(event.data);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
// Signal ready after a timeout to ensure main thread handler is set
|
|
855
|
+
setTimeout(() => {
|
|
856
|
+
isReady = true;
|
|
857
|
+
processQueue();
|
|
858
|
+
self.postMessage({ type: 'ready' });
|
|
859
|
+
}, 10);
|
|
860
|
+
`;
|
|
861
|
+
function createStats(result) {
|
|
862
|
+
const isFile = result.type ? result.type === "file" : result.isFile ?? false;
|
|
863
|
+
const isDir = result.type ? result.type === "directory" : result.isDirectory ?? false;
|
|
864
|
+
const mtimeMs = result.mtimeMs ?? result.mtime ?? Date.now();
|
|
865
|
+
const size = result.size ?? 0;
|
|
866
|
+
const mode = result.mode ?? (isDir ? 16877 : 33188);
|
|
867
|
+
return {
|
|
868
|
+
isFile: () => isFile,
|
|
869
|
+
isDirectory: () => isDir,
|
|
870
|
+
isBlockDevice: () => false,
|
|
871
|
+
isCharacterDevice: () => false,
|
|
872
|
+
isSymbolicLink: () => false,
|
|
873
|
+
isFIFO: () => false,
|
|
874
|
+
isSocket: () => false,
|
|
875
|
+
dev: 0,
|
|
876
|
+
ino: 0,
|
|
877
|
+
mode,
|
|
878
|
+
nlink: 1,
|
|
879
|
+
uid: 0,
|
|
880
|
+
gid: 0,
|
|
881
|
+
rdev: 0,
|
|
882
|
+
size,
|
|
883
|
+
blksize: 4096,
|
|
884
|
+
blocks: Math.ceil(size / 512),
|
|
885
|
+
atimeMs: mtimeMs,
|
|
886
|
+
mtimeMs,
|
|
887
|
+
ctimeMs: mtimeMs,
|
|
888
|
+
birthtimeMs: mtimeMs,
|
|
889
|
+
atime: new Date(mtimeMs),
|
|
890
|
+
mtime: new Date(mtimeMs),
|
|
891
|
+
ctime: new Date(mtimeMs),
|
|
892
|
+
birthtime: new Date(mtimeMs)
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
function createDirent(name, isDir) {
|
|
896
|
+
return {
|
|
897
|
+
name,
|
|
898
|
+
isFile: () => !isDir,
|
|
899
|
+
isDirectory: () => isDir,
|
|
900
|
+
isBlockDevice: () => false,
|
|
901
|
+
isCharacterDevice: () => false,
|
|
902
|
+
isSymbolicLink: () => false,
|
|
903
|
+
isFIFO: () => false,
|
|
904
|
+
isSocket: () => false
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function generateId() {
|
|
908
|
+
return Math.random().toString(36).substring(2, 15);
|
|
909
|
+
}
|
|
910
|
+
function encodeData(data, _encoding) {
|
|
911
|
+
if (typeof data === "string") {
|
|
912
|
+
return new TextEncoder().encode(data);
|
|
913
|
+
}
|
|
914
|
+
if (data instanceof Uint8Array) {
|
|
915
|
+
return data;
|
|
916
|
+
}
|
|
917
|
+
if (data instanceof ArrayBuffer) {
|
|
918
|
+
return new Uint8Array(data);
|
|
919
|
+
}
|
|
920
|
+
if (ArrayBuffer.isView(data)) {
|
|
921
|
+
return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
922
|
+
}
|
|
923
|
+
return new TextEncoder().encode(String(data ?? ""));
|
|
924
|
+
}
|
|
925
|
+
function decodeData(data, encoding) {
|
|
926
|
+
if (encoding === "utf8" || encoding === "utf-8") {
|
|
927
|
+
return new TextDecoder().decode(data);
|
|
928
|
+
}
|
|
929
|
+
return data;
|
|
930
|
+
}
|
|
931
|
+
var OPFSFileSystem = class _OPFSFileSystem {
|
|
932
|
+
worker = null;
|
|
933
|
+
pending = /* @__PURE__ */ new Map();
|
|
934
|
+
initialized = false;
|
|
935
|
+
initPromise = null;
|
|
936
|
+
// File descriptor table for openSync/readSync/writeSync/closeSync
|
|
937
|
+
fdTable = /* @__PURE__ */ new Map();
|
|
938
|
+
nextFd = 3;
|
|
939
|
+
// Start at 3 (0=stdin, 1=stdout, 2=stderr)
|
|
940
|
+
// Stat cache - reduces FS traffic by 30-50% for git operations
|
|
941
|
+
statCache = /* @__PURE__ */ new Map();
|
|
942
|
+
constructor() {
|
|
943
|
+
this.initWorker();
|
|
944
|
+
}
|
|
945
|
+
// Invalidate stat cache for a path (and parent for directory operations)
|
|
946
|
+
invalidateStat(filePath) {
|
|
947
|
+
const absPath = normalize(resolve(filePath));
|
|
948
|
+
this.statCache.delete(absPath);
|
|
949
|
+
const parent = dirname(absPath);
|
|
950
|
+
if (parent !== absPath) {
|
|
951
|
+
this.statCache.delete(parent);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
// Invalidate all stats under a directory (for recursive operations)
|
|
955
|
+
invalidateStatsUnder(dirPath) {
|
|
956
|
+
const prefix = normalize(resolve(dirPath));
|
|
957
|
+
for (const key of this.statCache.keys()) {
|
|
958
|
+
if (key === prefix || key.startsWith(prefix + "/")) {
|
|
959
|
+
this.statCache.delete(key);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
async initWorker() {
|
|
964
|
+
if (this.initialized) return;
|
|
965
|
+
if (this.initPromise) return this.initPromise;
|
|
966
|
+
this.initPromise = (async () => {
|
|
967
|
+
const blob = new Blob([KERNEL_SOURCE], { type: "application/javascript" });
|
|
968
|
+
this.worker = new Worker(URL.createObjectURL(blob));
|
|
969
|
+
const readyPromise = new Promise((resolve2) => {
|
|
970
|
+
this.worker.onmessage = (event) => {
|
|
971
|
+
const { id, result, error, code, type: msgType } = event.data;
|
|
972
|
+
if (msgType === "ready") {
|
|
973
|
+
resolve2();
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const pending = this.pending.get(id);
|
|
977
|
+
if (pending) {
|
|
978
|
+
this.pending.delete(id);
|
|
979
|
+
if (error) {
|
|
980
|
+
const errCode = code || "Error";
|
|
981
|
+
if (errCode === "NotFoundError" || errCode === "NotAllowedError" || errCode === "TypeMismatchError" || errCode === "InvalidModificationError" || errCode === "QuotaExceededError") {
|
|
982
|
+
pending.reject(mapErrorCode(errCode, pending.type, pending.path));
|
|
983
|
+
} else {
|
|
984
|
+
pending.reject(new FSError(errCode, -1, `${error}: ${pending.type} '${pending.path}'`));
|
|
985
|
+
}
|
|
986
|
+
} else if (result) {
|
|
987
|
+
pending.resolve(result);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
};
|
|
991
|
+
});
|
|
992
|
+
await readyPromise;
|
|
993
|
+
this.initialized = true;
|
|
994
|
+
})();
|
|
995
|
+
return this.initPromise;
|
|
996
|
+
}
|
|
997
|
+
// Async call to worker - uses fast createSyncAccessHandle internally
|
|
998
|
+
async asyncCall(type, filePath, payload) {
|
|
999
|
+
await this.initWorker();
|
|
1000
|
+
if (!this.worker) {
|
|
1001
|
+
throw new Error("Worker not initialized");
|
|
1002
|
+
}
|
|
1003
|
+
const absPath = resolve(filePath);
|
|
1004
|
+
const id = generateId();
|
|
1005
|
+
return new Promise((resolve2, reject) => {
|
|
1006
|
+
this.pending.set(id, { resolve: resolve2, reject, path: absPath, type });
|
|
1007
|
+
const msg = {
|
|
1008
|
+
id,
|
|
1009
|
+
type,
|
|
1010
|
+
path: absPath,
|
|
1011
|
+
payload
|
|
1012
|
+
};
|
|
1013
|
+
if (payload?.data instanceof Uint8Array) {
|
|
1014
|
+
const clone = new Uint8Array(payload.data);
|
|
1015
|
+
const newPayload = { ...payload, data: clone };
|
|
1016
|
+
this.worker.postMessage({ ...msg, payload: newPayload }, [clone.buffer]);
|
|
1017
|
+
} else {
|
|
1018
|
+
this.worker.postMessage(msg);
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
// Kernel worker for Tier 1 sync operations (loaded from URL, not blob)
|
|
1023
|
+
syncKernel = null;
|
|
1024
|
+
syncKernelReady = false;
|
|
1025
|
+
/**
|
|
1026
|
+
* Initialize sync operations with a kernel worker loaded from URL.
|
|
1027
|
+
* Required for Tier 1 (SharedArrayBuffer + Atomics) to work in nested Workers.
|
|
1028
|
+
* @param kernelUrl URL to the kernel.js file (defaults to '/kernel.js')
|
|
1029
|
+
*/
|
|
1030
|
+
async initSync(kernelUrl = "/kernel.js") {
|
|
1031
|
+
if (this.syncKernelReady) return;
|
|
1032
|
+
this.syncKernel = new Worker(kernelUrl, { type: "module" });
|
|
1033
|
+
await new Promise((resolve2, reject) => {
|
|
1034
|
+
const timeout = setTimeout(() => reject(new Error("Kernel init timeout")), 1e4);
|
|
1035
|
+
this.syncKernel.onmessage = (e) => {
|
|
1036
|
+
if (e.data?.type === "ready") {
|
|
1037
|
+
clearTimeout(timeout);
|
|
1038
|
+
this.syncKernelReady = true;
|
|
1039
|
+
resolve2();
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
this.syncKernel.onerror = (e) => {
|
|
1043
|
+
clearTimeout(timeout);
|
|
1044
|
+
reject(new Error(`Kernel error: ${e.message}`));
|
|
1045
|
+
};
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
// Tier 1: SharedArrayBuffer + Atomics via kernel worker
|
|
1049
|
+
// Data is transferred via SharedArrayBuffer (zero-copy)
|
|
1050
|
+
// Synchronization via Atomics.wait/notify
|
|
1051
|
+
// Buffer sizes for Tier 1 communication
|
|
1052
|
+
static META_SIZE = 1024 * 64;
|
|
1053
|
+
// 64KB for metadata/results
|
|
1054
|
+
static DEFAULT_DATA_SIZE = 1024 * 1024 * 10;
|
|
1055
|
+
// 10MB default buffer
|
|
1056
|
+
static MAX_CHUNK_SIZE = 1024 * 1024 * 10;
|
|
1057
|
+
// 10MB max per chunk
|
|
1058
|
+
// Reusable SharedArrayBuffer pool to prevent memory leaks
|
|
1059
|
+
// SharedArrayBuffers are expensive to allocate and don't get GC'd quickly
|
|
1060
|
+
syncBufferPool = null;
|
|
1061
|
+
getSyncBuffers(requiredDataSize) {
|
|
1062
|
+
if (this.syncBufferPool && this.syncBufferPool.dataSize >= requiredDataSize) {
|
|
1063
|
+
return {
|
|
1064
|
+
ctrlBuffer: this.syncBufferPool.ctrl,
|
|
1065
|
+
ctrl: new Int32Array(this.syncBufferPool.ctrl),
|
|
1066
|
+
metaBuffer: this.syncBufferPool.meta,
|
|
1067
|
+
dataBuffer: this.syncBufferPool.data
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
const dataSize = Math.max(
|
|
1071
|
+
_OPFSFileSystem.DEFAULT_DATA_SIZE,
|
|
1072
|
+
Math.min(requiredDataSize + 1024, 1024 * 1024 * 64)
|
|
1073
|
+
// Up to 64MB
|
|
1074
|
+
);
|
|
1075
|
+
const ctrlBuffer = new SharedArrayBuffer(4);
|
|
1076
|
+
const metaBuffer = new SharedArrayBuffer(_OPFSFileSystem.META_SIZE);
|
|
1077
|
+
const dataBuffer = new SharedArrayBuffer(dataSize);
|
|
1078
|
+
this.syncBufferPool = {
|
|
1079
|
+
ctrl: ctrlBuffer,
|
|
1080
|
+
meta: metaBuffer,
|
|
1081
|
+
data: dataBuffer,
|
|
1082
|
+
dataSize
|
|
1083
|
+
};
|
|
1084
|
+
return {
|
|
1085
|
+
ctrlBuffer,
|
|
1086
|
+
ctrl: new Int32Array(ctrlBuffer),
|
|
1087
|
+
metaBuffer,
|
|
1088
|
+
dataBuffer
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
syncCallTier1(type, filePath, payload) {
|
|
1092
|
+
if (!this.syncKernel || !this.syncKernelReady) {
|
|
1093
|
+
throw new Error("Sync kernel not initialized. Call initSync() first.");
|
|
1094
|
+
}
|
|
1095
|
+
const absPath = normalize(resolve(filePath));
|
|
1096
|
+
const data = payload?.data instanceof Uint8Array ? payload.data : null;
|
|
1097
|
+
const dataSize = data?.length ?? 0;
|
|
1098
|
+
if (type === "write" && data && dataSize > _OPFSFileSystem.MAX_CHUNK_SIZE) {
|
|
1099
|
+
return this.syncCallTier1Chunked(absPath, data);
|
|
1100
|
+
}
|
|
1101
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(dataSize);
|
|
1102
|
+
Atomics.store(ctrl, 0, 0);
|
|
1103
|
+
let dataLength = 0;
|
|
1104
|
+
if (data) {
|
|
1105
|
+
const view = new Uint8Array(dataBuffer);
|
|
1106
|
+
view.set(data);
|
|
1107
|
+
dataLength = data.length;
|
|
1108
|
+
}
|
|
1109
|
+
this.syncKernel.postMessage({
|
|
1110
|
+
type,
|
|
1111
|
+
path: absPath,
|
|
1112
|
+
ctrlBuffer,
|
|
1113
|
+
metaBuffer,
|
|
1114
|
+
dataBuffer,
|
|
1115
|
+
dataLength,
|
|
1116
|
+
payload: payload ? { ...payload, data: void 0 } : void 0
|
|
1117
|
+
});
|
|
1118
|
+
const waitResult = Atomics.wait(ctrl, 0, 0, 3e4);
|
|
1119
|
+
if (waitResult === "timed-out") {
|
|
1120
|
+
throw new Error("Operation timed out");
|
|
1121
|
+
}
|
|
1122
|
+
const status = Atomics.load(ctrl, 0);
|
|
1123
|
+
if (status === -1) {
|
|
1124
|
+
const metaView = new Uint8Array(metaBuffer);
|
|
1125
|
+
let end = metaView.indexOf(0);
|
|
1126
|
+
if (end === -1) end = _OPFSFileSystem.META_SIZE;
|
|
1127
|
+
const errorMsg = new TextDecoder().decode(metaView.slice(0, end));
|
|
1128
|
+
throw mapErrorCode(errorMsg || "Error", type, absPath);
|
|
1129
|
+
}
|
|
1130
|
+
if (status === -2) {
|
|
1131
|
+
throw createENOENT(type, absPath);
|
|
1132
|
+
}
|
|
1133
|
+
if (type === "read") {
|
|
1134
|
+
const bytesRead = status;
|
|
1135
|
+
const bufferSize = dataBuffer.byteLength;
|
|
1136
|
+
if (bytesRead === bufferSize) {
|
|
1137
|
+
const stat = this.syncStatTier1(absPath);
|
|
1138
|
+
if (stat && stat.size > bytesRead) {
|
|
1139
|
+
return this.syncCallTier1ChunkedRead(absPath, stat.size);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
const dataView = new Uint8Array(dataBuffer);
|
|
1143
|
+
return { data: dataView.slice(0, bytesRead) };
|
|
1144
|
+
}
|
|
1145
|
+
if (type === "stat") {
|
|
1146
|
+
const view = new DataView(metaBuffer);
|
|
1147
|
+
const typeVal = view.getUint8(0);
|
|
1148
|
+
return {
|
|
1149
|
+
type: typeVal === 0 ? "file" : "directory",
|
|
1150
|
+
mode: view.getUint32(4, true),
|
|
1151
|
+
size: view.getFloat64(8, true),
|
|
1152
|
+
mtimeMs: view.getFloat64(16, true)
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
if (type === "readdir") {
|
|
1156
|
+
const view = new DataView(metaBuffer);
|
|
1157
|
+
const bytes = new Uint8Array(metaBuffer);
|
|
1158
|
+
const count = view.getUint32(0, true);
|
|
1159
|
+
const entries = [];
|
|
1160
|
+
let offset = 4;
|
|
1161
|
+
for (let i = 0; i < count; i++) {
|
|
1162
|
+
const len = view.getUint16(offset, true);
|
|
1163
|
+
offset += 2;
|
|
1164
|
+
const name = new TextDecoder().decode(bytes.slice(offset, offset + len));
|
|
1165
|
+
entries.push(name);
|
|
1166
|
+
offset += len;
|
|
1167
|
+
}
|
|
1168
|
+
return { entries };
|
|
1169
|
+
}
|
|
1170
|
+
if (type === "exists") {
|
|
1171
|
+
return { exists: status === 1 };
|
|
1172
|
+
}
|
|
1173
|
+
return { success: status === 1 };
|
|
1174
|
+
}
|
|
1175
|
+
// Mutex for async operations to prevent buffer reuse race conditions
|
|
1176
|
+
// Multiple concurrent Atomics.waitAsync calls would share the same buffer pool,
|
|
1177
|
+
// causing data corruption when operations complete out of order
|
|
1178
|
+
asyncOperationPromise = Promise.resolve();
|
|
1179
|
+
// Async version of syncCallTier1 using Atomics.waitAsync (works on main thread)
|
|
1180
|
+
// This allows the main thread to use the fast SharedArrayBuffer path without blocking
|
|
1181
|
+
// IMPORTANT: Operations are serialized to prevent buffer reuse race conditions
|
|
1182
|
+
async syncCallTier1Async(type, filePath, payload) {
|
|
1183
|
+
const previousOp = this.asyncOperationPromise;
|
|
1184
|
+
let resolveCurrentOp;
|
|
1185
|
+
this.asyncOperationPromise = new Promise((resolve2) => {
|
|
1186
|
+
resolveCurrentOp = resolve2;
|
|
1187
|
+
});
|
|
1188
|
+
try {
|
|
1189
|
+
await previousOp;
|
|
1190
|
+
return await this.syncCallTier1AsyncImpl(type, filePath, payload);
|
|
1191
|
+
} finally {
|
|
1192
|
+
resolveCurrentOp();
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
// Implementation of async Tier 1 call (called after serialization)
|
|
1196
|
+
async syncCallTier1AsyncImpl(type, filePath, payload) {
|
|
1197
|
+
if (!this.syncKernel || !this.syncKernelReady) {
|
|
1198
|
+
throw new Error("Sync kernel not initialized. Call initSync() first.");
|
|
1199
|
+
}
|
|
1200
|
+
const absPath = normalize(resolve(filePath));
|
|
1201
|
+
const data = payload?.data instanceof Uint8Array ? payload.data : null;
|
|
1202
|
+
const dataSize = data?.length ?? 0;
|
|
1203
|
+
if (type === "write" && data && dataSize > _OPFSFileSystem.MAX_CHUNK_SIZE) {
|
|
1204
|
+
return this.syncCallTier1ChunkedAsync(absPath, data);
|
|
1205
|
+
}
|
|
1206
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(dataSize);
|
|
1207
|
+
Atomics.store(ctrl, 0, 0);
|
|
1208
|
+
let dataLength = 0;
|
|
1209
|
+
if (data) {
|
|
1210
|
+
const view = new Uint8Array(dataBuffer);
|
|
1211
|
+
view.set(data);
|
|
1212
|
+
dataLength = data.length;
|
|
1213
|
+
}
|
|
1214
|
+
this.syncKernel.postMessage({
|
|
1215
|
+
type,
|
|
1216
|
+
path: absPath,
|
|
1217
|
+
ctrlBuffer,
|
|
1218
|
+
metaBuffer,
|
|
1219
|
+
dataBuffer,
|
|
1220
|
+
dataLength,
|
|
1221
|
+
payload: payload ? { ...payload, data: void 0 } : void 0
|
|
1222
|
+
});
|
|
1223
|
+
const waitResult = await Atomics.waitAsync(ctrl, 0, 0, 3e4).value;
|
|
1224
|
+
if (waitResult === "timed-out") {
|
|
1225
|
+
throw new Error("Operation timed out");
|
|
1226
|
+
}
|
|
1227
|
+
const status = Atomics.load(ctrl, 0);
|
|
1228
|
+
if (status === -1) {
|
|
1229
|
+
const metaView = new Uint8Array(metaBuffer);
|
|
1230
|
+
let end = metaView.indexOf(0);
|
|
1231
|
+
if (end === -1) end = _OPFSFileSystem.META_SIZE;
|
|
1232
|
+
const errorMsg = new TextDecoder().decode(metaView.slice(0, end));
|
|
1233
|
+
throw mapErrorCode(errorMsg || "Error", type, absPath);
|
|
1234
|
+
}
|
|
1235
|
+
if (status === -2) {
|
|
1236
|
+
throw createENOENT(type, absPath);
|
|
1237
|
+
}
|
|
1238
|
+
if (type === "read") {
|
|
1239
|
+
const bytesRead = status;
|
|
1240
|
+
const bufferSize = dataBuffer.byteLength;
|
|
1241
|
+
if (bytesRead === bufferSize) {
|
|
1242
|
+
const stat = await this.syncStatTier1Async(absPath);
|
|
1243
|
+
if (stat && stat.size > bytesRead) {
|
|
1244
|
+
return this.syncCallTier1ChunkedReadAsync(absPath, stat.size);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const dataView = new Uint8Array(dataBuffer);
|
|
1248
|
+
return { data: dataView.slice(0, bytesRead) };
|
|
1249
|
+
}
|
|
1250
|
+
if (type === "stat") {
|
|
1251
|
+
const view = new DataView(metaBuffer);
|
|
1252
|
+
const typeVal = view.getUint8(0);
|
|
1253
|
+
return {
|
|
1254
|
+
type: typeVal === 0 ? "file" : "directory",
|
|
1255
|
+
mode: view.getUint32(4, true),
|
|
1256
|
+
size: view.getFloat64(8, true),
|
|
1257
|
+
mtimeMs: view.getFloat64(16, true)
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
if (type === "readdir") {
|
|
1261
|
+
const view = new DataView(metaBuffer);
|
|
1262
|
+
const bytes = new Uint8Array(metaBuffer);
|
|
1263
|
+
const count = view.getUint32(0, true);
|
|
1264
|
+
const entries = [];
|
|
1265
|
+
let offset = 4;
|
|
1266
|
+
for (let i = 0; i < count; i++) {
|
|
1267
|
+
const len = view.getUint16(offset, true);
|
|
1268
|
+
offset += 2;
|
|
1269
|
+
const name = new TextDecoder().decode(bytes.slice(offset, offset + len));
|
|
1270
|
+
entries.push(name);
|
|
1271
|
+
offset += len;
|
|
1272
|
+
}
|
|
1273
|
+
return { entries };
|
|
1274
|
+
}
|
|
1275
|
+
if (type === "exists") {
|
|
1276
|
+
return { exists: status === 1 };
|
|
1277
|
+
}
|
|
1278
|
+
return { success: status === 1 };
|
|
1279
|
+
}
|
|
1280
|
+
// Async stat helper for main thread
|
|
1281
|
+
// NOTE: Called from within syncCallTier1AsyncImpl, so uses impl directly to avoid deadlock
|
|
1282
|
+
async syncStatTier1Async(absPath) {
|
|
1283
|
+
try {
|
|
1284
|
+
const result = await this.syncCallTier1AsyncImpl("stat", absPath);
|
|
1285
|
+
return { size: result.size };
|
|
1286
|
+
} catch {
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
// Async chunked write for main thread
|
|
1291
|
+
async syncCallTier1ChunkedAsync(absPath, data) {
|
|
1292
|
+
const totalSize = data.length;
|
|
1293
|
+
let offset = 0;
|
|
1294
|
+
while (offset < totalSize) {
|
|
1295
|
+
const remaining = totalSize - offset;
|
|
1296
|
+
const currentChunkSize = Math.min(remaining, _OPFSFileSystem.MAX_CHUNK_SIZE);
|
|
1297
|
+
const chunk = data.subarray(offset, offset + currentChunkSize);
|
|
1298
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(currentChunkSize);
|
|
1299
|
+
Atomics.store(ctrl, 0, 0);
|
|
1300
|
+
const view = new Uint8Array(dataBuffer);
|
|
1301
|
+
view.set(chunk);
|
|
1302
|
+
const isFirstChunk = offset === 0;
|
|
1303
|
+
this.syncKernel.postMessage({
|
|
1304
|
+
type: isFirstChunk ? "write" : "append",
|
|
1305
|
+
path: absPath,
|
|
1306
|
+
ctrlBuffer,
|
|
1307
|
+
metaBuffer,
|
|
1308
|
+
dataBuffer,
|
|
1309
|
+
dataLength: currentChunkSize,
|
|
1310
|
+
payload: { flush: false }
|
|
1311
|
+
});
|
|
1312
|
+
const waitResult = await Atomics.waitAsync(ctrl, 0, 0, 3e4).value;
|
|
1313
|
+
if (waitResult === "timed-out") {
|
|
1314
|
+
throw new Error("Chunked write timed out");
|
|
1315
|
+
}
|
|
1316
|
+
const status = Atomics.load(ctrl, 0);
|
|
1317
|
+
if (status === -1 || status === -2) {
|
|
1318
|
+
throw createENOENT("write", absPath);
|
|
1319
|
+
}
|
|
1320
|
+
offset += currentChunkSize;
|
|
1321
|
+
}
|
|
1322
|
+
return { success: true };
|
|
1323
|
+
}
|
|
1324
|
+
// Async chunked read for main thread
|
|
1325
|
+
async syncCallTier1ChunkedReadAsync(absPath, totalSize) {
|
|
1326
|
+
const result = new Uint8Array(totalSize);
|
|
1327
|
+
let offset = 0;
|
|
1328
|
+
while (offset < totalSize) {
|
|
1329
|
+
const remaining = totalSize - offset;
|
|
1330
|
+
const currentChunkSize = Math.min(remaining, _OPFSFileSystem.MAX_CHUNK_SIZE);
|
|
1331
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(currentChunkSize);
|
|
1332
|
+
Atomics.store(ctrl, 0, 0);
|
|
1333
|
+
this.syncKernel.postMessage({
|
|
1334
|
+
type: "readChunk",
|
|
1335
|
+
path: absPath,
|
|
1336
|
+
ctrlBuffer,
|
|
1337
|
+
metaBuffer,
|
|
1338
|
+
dataBuffer,
|
|
1339
|
+
dataLength: 0,
|
|
1340
|
+
payload: { offset, length: currentChunkSize }
|
|
1341
|
+
});
|
|
1342
|
+
const waitResult = await Atomics.waitAsync(ctrl, 0, 0, 3e4).value;
|
|
1343
|
+
if (waitResult === "timed-out") {
|
|
1344
|
+
throw new Error("Chunked read timed out");
|
|
1345
|
+
}
|
|
1346
|
+
const status = Atomics.load(ctrl, 0);
|
|
1347
|
+
if (status === -1 || status === -2) {
|
|
1348
|
+
throw createENOENT("read", absPath);
|
|
1349
|
+
}
|
|
1350
|
+
const bytesRead = status;
|
|
1351
|
+
const dataView = new Uint8Array(dataBuffer);
|
|
1352
|
+
result.set(dataView.subarray(0, bytesRead), offset);
|
|
1353
|
+
offset += bytesRead;
|
|
1354
|
+
}
|
|
1355
|
+
return { data: result };
|
|
1356
|
+
}
|
|
1357
|
+
// Chunked write for files larger than MAX_CHUNK_SIZE
|
|
1358
|
+
syncCallTier1Chunked(absPath, data) {
|
|
1359
|
+
const totalSize = data.length;
|
|
1360
|
+
const chunkSize = _OPFSFileSystem.MAX_CHUNK_SIZE;
|
|
1361
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(chunkSize);
|
|
1362
|
+
const dataView = new Uint8Array(dataBuffer);
|
|
1363
|
+
let offset = 0;
|
|
1364
|
+
while (offset < totalSize) {
|
|
1365
|
+
const remaining = totalSize - offset;
|
|
1366
|
+
const currentChunkSize = Math.min(chunkSize, remaining);
|
|
1367
|
+
const chunk = data.subarray(offset, offset + currentChunkSize);
|
|
1368
|
+
Atomics.store(ctrl, 0, 0);
|
|
1369
|
+
dataView.set(chunk);
|
|
1370
|
+
this.syncKernel.postMessage({
|
|
1371
|
+
type: "write",
|
|
1372
|
+
path: absPath,
|
|
1373
|
+
ctrlBuffer,
|
|
1374
|
+
metaBuffer,
|
|
1375
|
+
dataBuffer,
|
|
1376
|
+
dataLength: currentChunkSize,
|
|
1377
|
+
payload: { offset }
|
|
1378
|
+
// Kernel writes at this offset
|
|
1379
|
+
});
|
|
1380
|
+
const waitResult = Atomics.wait(ctrl, 0, 0, 6e4);
|
|
1381
|
+
if (waitResult === "timed-out") {
|
|
1382
|
+
throw new Error(`Chunked write timed out at offset ${offset}`);
|
|
1383
|
+
}
|
|
1384
|
+
const status = Atomics.load(ctrl, 0);
|
|
1385
|
+
if (status === -1) {
|
|
1386
|
+
const metaView = new Uint8Array(metaBuffer);
|
|
1387
|
+
let end = metaView.indexOf(0);
|
|
1388
|
+
if (end === -1) end = _OPFSFileSystem.META_SIZE;
|
|
1389
|
+
const errorMsg = new TextDecoder().decode(metaView.slice(0, end));
|
|
1390
|
+
throw mapErrorCode(errorMsg || "Error", "write", absPath);
|
|
1391
|
+
}
|
|
1392
|
+
if (status === -2) {
|
|
1393
|
+
throw createENOENT("write", absPath);
|
|
1394
|
+
}
|
|
1395
|
+
offset += currentChunkSize;
|
|
1396
|
+
}
|
|
1397
|
+
return { success: true };
|
|
1398
|
+
}
|
|
1399
|
+
// Chunked read for files larger than buffer size
|
|
1400
|
+
syncCallTier1ChunkedRead(absPath, totalSize) {
|
|
1401
|
+
const chunkSize = _OPFSFileSystem.MAX_CHUNK_SIZE;
|
|
1402
|
+
const result = new Uint8Array(totalSize);
|
|
1403
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(chunkSize);
|
|
1404
|
+
let offset = 0;
|
|
1405
|
+
while (offset < totalSize) {
|
|
1406
|
+
const remaining = totalSize - offset;
|
|
1407
|
+
const currentChunkSize = Math.min(chunkSize, remaining);
|
|
1408
|
+
Atomics.store(ctrl, 0, 0);
|
|
1409
|
+
this.syncKernel.postMessage({
|
|
1410
|
+
type: "read",
|
|
1411
|
+
path: absPath,
|
|
1412
|
+
ctrlBuffer,
|
|
1413
|
+
metaBuffer,
|
|
1414
|
+
dataBuffer,
|
|
1415
|
+
dataLength: 0,
|
|
1416
|
+
payload: { offset, len: currentChunkSize }
|
|
1417
|
+
});
|
|
1418
|
+
const waitResult = Atomics.wait(ctrl, 0, 0, 6e4);
|
|
1419
|
+
if (waitResult === "timed-out") {
|
|
1420
|
+
throw new Error(`Chunked read timed out at offset ${offset}`);
|
|
1421
|
+
}
|
|
1422
|
+
const status = Atomics.load(ctrl, 0);
|
|
1423
|
+
if (status === -1) {
|
|
1424
|
+
const metaView = new Uint8Array(metaBuffer);
|
|
1425
|
+
let end = metaView.indexOf(0);
|
|
1426
|
+
if (end === -1) end = _OPFSFileSystem.META_SIZE;
|
|
1427
|
+
const errorMsg = new TextDecoder().decode(metaView.slice(0, end));
|
|
1428
|
+
throw mapErrorCode(errorMsg || "Error", "read", absPath);
|
|
1429
|
+
}
|
|
1430
|
+
if (status === -2) {
|
|
1431
|
+
throw createENOENT("read", absPath);
|
|
1432
|
+
}
|
|
1433
|
+
const bytesRead = status;
|
|
1434
|
+
const dataView = new Uint8Array(dataBuffer, 0, bytesRead);
|
|
1435
|
+
result.set(dataView, offset);
|
|
1436
|
+
offset += bytesRead;
|
|
1437
|
+
if (bytesRead < currentChunkSize) {
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return { data: result.subarray(0, offset) };
|
|
1442
|
+
}
|
|
1443
|
+
// Get file size via stat (used for chunked reads)
|
|
1444
|
+
syncStatTier1(absPath) {
|
|
1445
|
+
const { ctrlBuffer, ctrl, metaBuffer, dataBuffer } = this.getSyncBuffers(1024);
|
|
1446
|
+
Atomics.store(ctrl, 0, 0);
|
|
1447
|
+
this.syncKernel.postMessage({
|
|
1448
|
+
type: "stat",
|
|
1449
|
+
path: absPath,
|
|
1450
|
+
ctrlBuffer,
|
|
1451
|
+
metaBuffer,
|
|
1452
|
+
dataBuffer,
|
|
1453
|
+
dataLength: 0
|
|
1454
|
+
});
|
|
1455
|
+
const waitResult = Atomics.wait(ctrl, 0, 0, 1e4);
|
|
1456
|
+
if (waitResult === "timed-out") {
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
const status = Atomics.load(ctrl, 0);
|
|
1460
|
+
if (status <= 0) {
|
|
1461
|
+
return null;
|
|
1462
|
+
}
|
|
1463
|
+
const view = new DataView(metaBuffer);
|
|
1464
|
+
return { size: view.getFloat64(8, true) };
|
|
1465
|
+
}
|
|
1466
|
+
syncCall(type, filePath, payload) {
|
|
1467
|
+
if (isWorkerContext && typeof SharedArrayBuffer !== "undefined" && this.syncKernelReady) {
|
|
1468
|
+
return this.syncCallTier1(type, filePath, payload);
|
|
1469
|
+
}
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
`Sync operations require crossOriginIsolated environment (COOP/COEP headers) and initSync() to be called. Current state: crossOriginIsolated=${typeof crossOriginIsolated !== "undefined" ? crossOriginIsolated : "N/A"}, isWorkerContext=${isWorkerContext}, syncKernelReady=${this.syncKernelReady}. Use fs.promises.* for async operations that work everywhere.`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
// --- Synchronous API (Node.js fs compatible) ---
|
|
1475
|
+
readFileSync(filePath, options) {
|
|
1476
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
1477
|
+
const result = this.syncCall("read", filePath);
|
|
1478
|
+
if (!result.data) throw createENOENT("read", filePath);
|
|
1479
|
+
return decodeData(result.data, encoding);
|
|
1480
|
+
}
|
|
1481
|
+
writeFileSync(filePath, data, options) {
|
|
1482
|
+
const opts = typeof options === "string" ? { encoding: options } : options;
|
|
1483
|
+
const encoded = encodeData(data, opts?.encoding);
|
|
1484
|
+
this.syncCall("write", filePath, { data: encoded, flush: opts?.flush });
|
|
1485
|
+
this.invalidateStat(filePath);
|
|
1486
|
+
}
|
|
1487
|
+
appendFileSync(filePath, data, options) {
|
|
1488
|
+
typeof options === "string" ? options : options?.encoding;
|
|
1489
|
+
const encoded = encodeData(data);
|
|
1490
|
+
this.syncCall("append", filePath, { data: encoded });
|
|
1491
|
+
this.invalidateStat(filePath);
|
|
1492
|
+
}
|
|
1493
|
+
existsSync(filePath) {
|
|
1494
|
+
try {
|
|
1495
|
+
const result = this.syncCall("exists", filePath);
|
|
1496
|
+
return result.exists ?? false;
|
|
1497
|
+
} catch {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
mkdirSync(filePath, options) {
|
|
1502
|
+
const recursive = typeof options === "object" ? options?.recursive : false;
|
|
1503
|
+
this.syncCall("mkdir", filePath, { recursive });
|
|
1504
|
+
this.invalidateStat(filePath);
|
|
1505
|
+
return recursive ? filePath : void 0;
|
|
1506
|
+
}
|
|
1507
|
+
rmdirSync(filePath, options) {
|
|
1508
|
+
this.syncCall("rmdir", filePath, { recursive: options?.recursive });
|
|
1509
|
+
if (options?.recursive) {
|
|
1510
|
+
this.invalidateStatsUnder(filePath);
|
|
1511
|
+
} else {
|
|
1512
|
+
this.invalidateStat(filePath);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
rmSync(filePath, options) {
|
|
1516
|
+
try {
|
|
1517
|
+
const result = this.syncCall("stat", filePath);
|
|
1518
|
+
try {
|
|
1519
|
+
if (result.isDirectory || result.type === "directory") {
|
|
1520
|
+
this.syncCall("rmdir", filePath, { recursive: options?.recursive });
|
|
1521
|
+
if (options?.recursive) {
|
|
1522
|
+
this.invalidateStatsUnder(filePath);
|
|
1523
|
+
} else {
|
|
1524
|
+
this.invalidateStat(filePath);
|
|
1525
|
+
}
|
|
1526
|
+
} else {
|
|
1527
|
+
this.syncCall("unlink", filePath);
|
|
1528
|
+
this.invalidateStat(filePath);
|
|
1529
|
+
}
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
if (!options?.force) throw e;
|
|
1532
|
+
}
|
|
1533
|
+
} catch (e) {
|
|
1534
|
+
if (!options?.force) throw e;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
unlinkSync(filePath) {
|
|
1538
|
+
this.syncCall("unlink", filePath);
|
|
1539
|
+
this.invalidateStat(filePath);
|
|
1540
|
+
}
|
|
1541
|
+
readdirSync(filePath, options) {
|
|
1542
|
+
const result = this.syncCall("readdir", filePath);
|
|
1543
|
+
const entries = result.entries || [];
|
|
1544
|
+
const opts = typeof options === "object" ? options : { };
|
|
1545
|
+
if (opts?.withFileTypes) {
|
|
1546
|
+
return entries.map((name) => {
|
|
1547
|
+
try {
|
|
1548
|
+
const stat = this.syncCall("stat", join(filePath, name));
|
|
1549
|
+
const isDir = stat.type === "directory" || stat.isDirectory === true;
|
|
1550
|
+
return createDirent(name, isDir);
|
|
1551
|
+
} catch {
|
|
1552
|
+
return createDirent(name, false);
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
return entries;
|
|
1557
|
+
}
|
|
1558
|
+
statSync(filePath) {
|
|
1559
|
+
const absPath = normalize(resolve(filePath));
|
|
1560
|
+
const cached = this.statCache.get(absPath);
|
|
1561
|
+
if (cached) return cached;
|
|
1562
|
+
const result = this.syncCall("stat", filePath);
|
|
1563
|
+
if (result.type === void 0 && result.isFile === void 0 && result.isDirectory === void 0) {
|
|
1564
|
+
throw createENOENT("stat", filePath);
|
|
1565
|
+
}
|
|
1566
|
+
const stats = createStats(result);
|
|
1567
|
+
this.statCache.set(absPath, stats);
|
|
1568
|
+
return stats;
|
|
1569
|
+
}
|
|
1570
|
+
lstatSync(filePath) {
|
|
1571
|
+
const stats = this.statSync(filePath);
|
|
1572
|
+
if (stats.isFile() && this.isSymlinkSync(filePath)) {
|
|
1573
|
+
return this.createSymlinkStats(stats);
|
|
1574
|
+
}
|
|
1575
|
+
return stats;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Create stats object for a symlink file.
|
|
1579
|
+
*/
|
|
1580
|
+
createSymlinkStats(baseStats) {
|
|
1581
|
+
return {
|
|
1582
|
+
...baseStats,
|
|
1583
|
+
isFile: () => false,
|
|
1584
|
+
isSymbolicLink: () => true,
|
|
1585
|
+
// Symlink mode: 0o120777 (41471 decimal)
|
|
1586
|
+
mode: 41471
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
renameSync(oldPath, newPath) {
|
|
1590
|
+
this.syncCall("rename", oldPath, { newPath });
|
|
1591
|
+
this.invalidateStat(oldPath);
|
|
1592
|
+
this.invalidateStat(newPath);
|
|
1593
|
+
}
|
|
1594
|
+
copyFileSync(src, dest) {
|
|
1595
|
+
this.syncCall("copy", src, { newPath: dest });
|
|
1596
|
+
this.invalidateStat(dest);
|
|
1597
|
+
}
|
|
1598
|
+
truncateSync(filePath, len = 0) {
|
|
1599
|
+
this.syncCall("truncate", filePath, { len });
|
|
1600
|
+
this.invalidateStat(filePath);
|
|
1601
|
+
}
|
|
1602
|
+
/**
|
|
1603
|
+
* Flush all pending writes to storage.
|
|
1604
|
+
* Use this after writes with { flush: false } to ensure data is persisted.
|
|
1605
|
+
*/
|
|
1606
|
+
flushSync() {
|
|
1607
|
+
this.syncCall("flush", "/");
|
|
1608
|
+
}
|
|
1609
|
+
/**
|
|
1610
|
+
* Alias for flushSync() - matches Node.js fdatasync behavior
|
|
1611
|
+
*/
|
|
1612
|
+
fdatasyncSync() {
|
|
1613
|
+
this.flushSync();
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Purge all kernel caches (sync handles, directory handles).
|
|
1617
|
+
* Use between major operations to ensure clean state.
|
|
1618
|
+
*/
|
|
1619
|
+
purgeSync() {
|
|
1620
|
+
this.syncCall("purge", "/");
|
|
1621
|
+
this.statCache.clear();
|
|
1622
|
+
}
|
|
1623
|
+
accessSync(filePath, _mode) {
|
|
1624
|
+
const exists = this.existsSync(filePath);
|
|
1625
|
+
if (!exists) {
|
|
1626
|
+
throw createENOENT("access", filePath);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
// --- Low-level File Descriptor API ---
|
|
1630
|
+
// For efficient packfile access (read specific offsets without loading entire file)
|
|
1631
|
+
openSync(filePath, flags = "r") {
|
|
1632
|
+
const flagNum = typeof flags === "string" ? this.parseFlags(flags) : flags;
|
|
1633
|
+
const isReadOnly = (flagNum & constants.O_WRONLY) === 0 && (flagNum & constants.O_RDWR) === 0;
|
|
1634
|
+
if (isReadOnly && !this.existsSync(filePath)) {
|
|
1635
|
+
throw createENOENT("open", filePath);
|
|
1636
|
+
}
|
|
1637
|
+
const fd = this.nextFd++;
|
|
1638
|
+
this.fdTable.set(fd, {
|
|
1639
|
+
path: normalize(resolve(filePath)),
|
|
1640
|
+
flags: flagNum,
|
|
1641
|
+
position: 0
|
|
1642
|
+
});
|
|
1643
|
+
return fd;
|
|
1644
|
+
}
|
|
1645
|
+
closeSync(fd) {
|
|
1646
|
+
if (!this.fdTable.has(fd)) {
|
|
1647
|
+
throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
1648
|
+
}
|
|
1649
|
+
this.fdTable.delete(fd);
|
|
1650
|
+
}
|
|
1651
|
+
readSync(fd, buffer, offset, length, position) {
|
|
1652
|
+
const entry = this.fdTable.get(fd);
|
|
1653
|
+
if (!entry) {
|
|
1654
|
+
throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
1655
|
+
}
|
|
1656
|
+
const readPos = position !== null ? position : entry.position;
|
|
1657
|
+
const result = this.syncCall("read", entry.path, { offset: readPos, len: length });
|
|
1658
|
+
if (!result.data) {
|
|
1659
|
+
return 0;
|
|
1660
|
+
}
|
|
1661
|
+
const bytesRead = Math.min(result.data.length, length);
|
|
1662
|
+
buffer.set(result.data.subarray(0, bytesRead), offset);
|
|
1663
|
+
if (position === null) {
|
|
1664
|
+
entry.position += bytesRead;
|
|
1665
|
+
}
|
|
1666
|
+
return bytesRead;
|
|
1667
|
+
}
|
|
1668
|
+
writeSync(fd, buffer, offset, length, position) {
|
|
1669
|
+
const entry = this.fdTable.get(fd);
|
|
1670
|
+
if (!entry) {
|
|
1671
|
+
throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
1672
|
+
}
|
|
1673
|
+
const writePos = position !== null ? position : entry.position;
|
|
1674
|
+
const data = buffer.subarray(offset, offset + length);
|
|
1675
|
+
this.syncCall("write", entry.path, {
|
|
1676
|
+
data,
|
|
1677
|
+
offset: writePos,
|
|
1678
|
+
truncate: false
|
|
1679
|
+
});
|
|
1680
|
+
this.invalidateStat(entry.path);
|
|
1681
|
+
if (position === null) {
|
|
1682
|
+
entry.position += length;
|
|
1683
|
+
}
|
|
1684
|
+
return length;
|
|
1685
|
+
}
|
|
1686
|
+
fstatSync(fd) {
|
|
1687
|
+
const entry = this.fdTable.get(fd);
|
|
1688
|
+
if (!entry) {
|
|
1689
|
+
throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
1690
|
+
}
|
|
1691
|
+
return this.statSync(entry.path);
|
|
1692
|
+
}
|
|
1693
|
+
ftruncateSync(fd, len = 0) {
|
|
1694
|
+
const entry = this.fdTable.get(fd);
|
|
1695
|
+
if (!entry) {
|
|
1696
|
+
throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
1697
|
+
}
|
|
1698
|
+
this.truncateSync(entry.path, len);
|
|
1699
|
+
}
|
|
1700
|
+
/**
|
|
1701
|
+
* Resolve a path to an absolute path.
|
|
1702
|
+
* OPFS doesn't support symlinks, so this just normalizes the path.
|
|
1703
|
+
*/
|
|
1704
|
+
realpathSync(filePath) {
|
|
1705
|
+
this.accessSync(filePath);
|
|
1706
|
+
return normalize(resolve(filePath));
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Change file mode (no-op in OPFS - permissions not supported).
|
|
1710
|
+
*/
|
|
1711
|
+
chmodSync(_filePath, _mode) {
|
|
1712
|
+
}
|
|
1713
|
+
/**
|
|
1714
|
+
* Change file owner (no-op in OPFS - ownership not supported).
|
|
1715
|
+
*/
|
|
1716
|
+
chownSync(_filePath, _uid, _gid) {
|
|
1717
|
+
}
|
|
1718
|
+
/**
|
|
1719
|
+
* Change file timestamps (no-op in OPFS - timestamps are read-only).
|
|
1720
|
+
*/
|
|
1721
|
+
utimesSync(_filePath, _atime, _mtime) {
|
|
1722
|
+
}
|
|
1723
|
+
// Magic prefix for symlink files - must be unique enough to not appear in regular files
|
|
1724
|
+
static SYMLINK_MAGIC = "OPFS_SYMLINK_V1:";
|
|
1725
|
+
/**
|
|
1726
|
+
* Create a symbolic link.
|
|
1727
|
+
* Emulated by storing target path in a special file format.
|
|
1728
|
+
*/
|
|
1729
|
+
symlinkSync(target, filePath, _type) {
|
|
1730
|
+
const content = _OPFSFileSystem.SYMLINK_MAGIC + target;
|
|
1731
|
+
this.writeFileSync(filePath, content);
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Read a symbolic link target.
|
|
1735
|
+
*/
|
|
1736
|
+
readlinkSync(filePath) {
|
|
1737
|
+
const content = this.readFileSync(filePath, { encoding: "utf8" });
|
|
1738
|
+
if (!content.startsWith(_OPFSFileSystem.SYMLINK_MAGIC)) {
|
|
1739
|
+
throw new FSError("EINVAL", -22, `EINVAL: invalid argument, readlink '${filePath}'`, "readlink", filePath);
|
|
1740
|
+
}
|
|
1741
|
+
return content.slice(_OPFSFileSystem.SYMLINK_MAGIC.length);
|
|
1742
|
+
}
|
|
1743
|
+
/**
|
|
1744
|
+
* Check if a file is a symlink (sync).
|
|
1745
|
+
*/
|
|
1746
|
+
isSymlinkSync(filePath) {
|
|
1747
|
+
try {
|
|
1748
|
+
const content = this.readFileSync(filePath, { encoding: "utf8" });
|
|
1749
|
+
return content.startsWith(_OPFSFileSystem.SYMLINK_MAGIC);
|
|
1750
|
+
} catch {
|
|
1751
|
+
return false;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* Check if a file is a symlink (async).
|
|
1756
|
+
*/
|
|
1757
|
+
async isSymlinkAsync(filePath) {
|
|
1758
|
+
try {
|
|
1759
|
+
const content = await this.promises.readFile(filePath, { encoding: "utf8" });
|
|
1760
|
+
return content.startsWith(_OPFSFileSystem.SYMLINK_MAGIC);
|
|
1761
|
+
} catch {
|
|
1762
|
+
return false;
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
/**
|
|
1766
|
+
* Create a hard link.
|
|
1767
|
+
* Emulated by copying the file (true hard links not supported in OPFS).
|
|
1768
|
+
*/
|
|
1769
|
+
linkSync(existingPath, newPath) {
|
|
1770
|
+
this.copyFileSync(existingPath, newPath);
|
|
1771
|
+
}
|
|
1772
|
+
parseFlags(flags) {
|
|
1773
|
+
switch (flags) {
|
|
1774
|
+
case "r":
|
|
1775
|
+
return constants.O_RDONLY;
|
|
1776
|
+
case "r+":
|
|
1777
|
+
return constants.O_RDWR;
|
|
1778
|
+
case "w":
|
|
1779
|
+
return constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC;
|
|
1780
|
+
case "w+":
|
|
1781
|
+
return constants.O_RDWR | constants.O_CREAT | constants.O_TRUNC;
|
|
1782
|
+
case "a":
|
|
1783
|
+
return constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND;
|
|
1784
|
+
case "a+":
|
|
1785
|
+
return constants.O_RDWR | constants.O_CREAT | constants.O_APPEND;
|
|
1786
|
+
default:
|
|
1787
|
+
return constants.O_RDONLY;
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
// --- Async Promises API ---
|
|
1791
|
+
// When Tier 1 sync kernel is available, use it for better performance (wrapped in Promise)
|
|
1792
|
+
// Otherwise fall back to async worker
|
|
1793
|
+
// Helper: Use sync kernel if available (in worker context), otherwise async worker
|
|
1794
|
+
async fastCall(type, filePath, payload) {
|
|
1795
|
+
if (this.syncKernelReady) {
|
|
1796
|
+
if (isWorkerContext) {
|
|
1797
|
+
return Promise.resolve(this.syncCallTier1(type, filePath, payload));
|
|
1798
|
+
} else {
|
|
1799
|
+
return this.syncCallTier1Async(type, filePath, payload);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
return this.asyncCall(type, filePath, payload);
|
|
1803
|
+
}
|
|
1804
|
+
promises = {
|
|
1805
|
+
readFile: async (filePath, options) => {
|
|
1806
|
+
if (!filePath) {
|
|
1807
|
+
throw createENOENT("read", filePath || "");
|
|
1808
|
+
}
|
|
1809
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
1810
|
+
if (this.syncKernelReady) {
|
|
1811
|
+
if (isWorkerContext) {
|
|
1812
|
+
const result2 = this.syncCallTier1("read", filePath);
|
|
1813
|
+
if (!result2.data) throw createENOENT("read", filePath);
|
|
1814
|
+
return decodeData(result2.data, encoding);
|
|
1815
|
+
} else {
|
|
1816
|
+
const result2 = await this.syncCallTier1Async("read", filePath);
|
|
1817
|
+
if (!result2.data) throw createENOENT("read", filePath);
|
|
1818
|
+
return decodeData(result2.data, encoding);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
const result = await this.asyncCall("read", filePath);
|
|
1822
|
+
if (!result.data) throw createENOENT("read", filePath);
|
|
1823
|
+
return decodeData(result.data, encoding);
|
|
1824
|
+
},
|
|
1825
|
+
writeFile: async (filePath, data, options) => {
|
|
1826
|
+
const opts = typeof options === "string" ? { encoding: options } : options;
|
|
1827
|
+
const encoded = encodeData(data, opts?.encoding);
|
|
1828
|
+
await this.fastCall("write", filePath, { data: encoded, flush: opts?.flush });
|
|
1829
|
+
this.invalidateStat(filePath);
|
|
1830
|
+
},
|
|
1831
|
+
appendFile: async (filePath, data, options) => {
|
|
1832
|
+
const opts = typeof options === "string" ? { encoding: options } : options;
|
|
1833
|
+
const encoded = encodeData(data, opts?.encoding);
|
|
1834
|
+
await this.fastCall("append", filePath, { data: encoded, flush: opts?.flush });
|
|
1835
|
+
this.invalidateStat(filePath);
|
|
1836
|
+
},
|
|
1837
|
+
mkdir: async (filePath, options) => {
|
|
1838
|
+
const recursive = typeof options === "object" ? options?.recursive : false;
|
|
1839
|
+
await this.fastCall("mkdir", filePath, { recursive });
|
|
1840
|
+
return recursive ? filePath : void 0;
|
|
1841
|
+
},
|
|
1842
|
+
rmdir: async (filePath, options) => {
|
|
1843
|
+
await this.fastCall("rmdir", filePath, { recursive: options?.recursive });
|
|
1844
|
+
},
|
|
1845
|
+
rm: async (filePath, options) => {
|
|
1846
|
+
try {
|
|
1847
|
+
const result = await this.fastCall("stat", filePath);
|
|
1848
|
+
try {
|
|
1849
|
+
if (result.isDirectory || result.type === "directory") {
|
|
1850
|
+
await this.fastCall("rmdir", filePath, { recursive: options?.recursive });
|
|
1851
|
+
if (options?.recursive) {
|
|
1852
|
+
this.invalidateStatsUnder(filePath);
|
|
1853
|
+
} else {
|
|
1854
|
+
this.invalidateStat(filePath);
|
|
1855
|
+
}
|
|
1856
|
+
} else {
|
|
1857
|
+
await this.fastCall("unlink", filePath);
|
|
1858
|
+
this.invalidateStat(filePath);
|
|
1859
|
+
}
|
|
1860
|
+
} catch (e) {
|
|
1861
|
+
if (!options?.force) throw e;
|
|
1862
|
+
}
|
|
1863
|
+
} catch (e) {
|
|
1864
|
+
if (!options?.force) throw e;
|
|
1865
|
+
}
|
|
1866
|
+
},
|
|
1867
|
+
unlink: async (filePath) => {
|
|
1868
|
+
await this.fastCall("unlink", filePath);
|
|
1869
|
+
},
|
|
1870
|
+
readdir: async (filePath, options) => {
|
|
1871
|
+
const result = await this.fastCall("readdir", filePath);
|
|
1872
|
+
const entries = result.entries || [];
|
|
1873
|
+
const opts = typeof options === "object" ? options : { };
|
|
1874
|
+
if (opts?.withFileTypes) {
|
|
1875
|
+
const dirents = [];
|
|
1876
|
+
for (const name of entries) {
|
|
1877
|
+
try {
|
|
1878
|
+
const stat = await this.fastCall("stat", join(filePath, name));
|
|
1879
|
+
const isDir = stat.type === "directory" || stat.isDirectory === true;
|
|
1880
|
+
dirents.push(createDirent(name, isDir));
|
|
1881
|
+
} catch {
|
|
1882
|
+
dirents.push(createDirent(name, false));
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return dirents;
|
|
1886
|
+
}
|
|
1887
|
+
return entries;
|
|
1888
|
+
},
|
|
1889
|
+
stat: async (filePath) => {
|
|
1890
|
+
const result = await this.fastCall("stat", filePath);
|
|
1891
|
+
return createStats(result);
|
|
1892
|
+
},
|
|
1893
|
+
access: async (filePath, _mode) => {
|
|
1894
|
+
const result = await this.fastCall("exists", filePath);
|
|
1895
|
+
if (!result.exists) {
|
|
1896
|
+
throw createENOENT("access", filePath);
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1899
|
+
rename: async (oldFilePath, newFilePath) => {
|
|
1900
|
+
await this.fastCall("rename", oldFilePath, { newPath: resolve(newFilePath) });
|
|
1901
|
+
},
|
|
1902
|
+
copyFile: async (srcPath, destPath) => {
|
|
1903
|
+
await this.fastCall("copy", srcPath, { newPath: resolve(destPath) });
|
|
1904
|
+
},
|
|
1905
|
+
truncate: async (filePath, len = 0) => {
|
|
1906
|
+
await this.fastCall("truncate", filePath, { len });
|
|
1907
|
+
this.invalidateStat(filePath);
|
|
1908
|
+
},
|
|
1909
|
+
lstat: async (filePath) => {
|
|
1910
|
+
const result = await this.fastCall("stat", filePath);
|
|
1911
|
+
const stats = createStats(result);
|
|
1912
|
+
if (stats.isFile()) {
|
|
1913
|
+
const isSymlink = await this.isSymlinkAsync(filePath);
|
|
1914
|
+
if (isSymlink) {
|
|
1915
|
+
return this.createSymlinkStats(stats);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
return stats;
|
|
1919
|
+
},
|
|
1920
|
+
realpath: async (filePath) => {
|
|
1921
|
+
await this.promises.access(filePath);
|
|
1922
|
+
return normalize(resolve(filePath));
|
|
1923
|
+
},
|
|
1924
|
+
exists: async (filePath) => {
|
|
1925
|
+
try {
|
|
1926
|
+
const result = await this.fastCall("exists", filePath);
|
|
1927
|
+
return result.exists ?? false;
|
|
1928
|
+
} catch {
|
|
1929
|
+
return false;
|
|
1930
|
+
}
|
|
1931
|
+
},
|
|
1932
|
+
chmod: async (_filePath, _mode) => {
|
|
1933
|
+
},
|
|
1934
|
+
chown: async (_filePath, _uid, _gid) => {
|
|
1935
|
+
},
|
|
1936
|
+
utimes: async (_filePath, _atime, _mtime) => {
|
|
1937
|
+
},
|
|
1938
|
+
symlink: async (target, filePath, _type) => {
|
|
1939
|
+
const content = _OPFSFileSystem.SYMLINK_MAGIC + target;
|
|
1940
|
+
await this.promises.writeFile(filePath, content);
|
|
1941
|
+
},
|
|
1942
|
+
readlink: async (filePath) => {
|
|
1943
|
+
const content = await this.promises.readFile(filePath, { encoding: "utf8" });
|
|
1944
|
+
if (!content.startsWith(_OPFSFileSystem.SYMLINK_MAGIC)) {
|
|
1945
|
+
throw new FSError("EINVAL", -22, `EINVAL: invalid argument, readlink '${filePath}'`, "readlink", filePath);
|
|
1946
|
+
}
|
|
1947
|
+
return content.slice(_OPFSFileSystem.SYMLINK_MAGIC.length);
|
|
1948
|
+
},
|
|
1949
|
+
link: async (existingPath, newPath) => {
|
|
1950
|
+
await this.promises.copyFile(existingPath, newPath);
|
|
1951
|
+
},
|
|
1952
|
+
open: async (filePath, flags = "r", _mode) => {
|
|
1953
|
+
const flagNum = typeof flags === "string" ? this.parseFlags(flags) : flags;
|
|
1954
|
+
const isReadOnly = (flagNum & constants.O_WRONLY) === 0 && (flagNum & constants.O_RDWR) === 0;
|
|
1955
|
+
if (isReadOnly) {
|
|
1956
|
+
const exists = await this.promises.exists(filePath);
|
|
1957
|
+
if (!exists) {
|
|
1958
|
+
throw createENOENT("open", filePath);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
const fd = this.nextFd++;
|
|
1962
|
+
this.fdTable.set(fd, {
|
|
1963
|
+
path: normalize(resolve(filePath)),
|
|
1964
|
+
flags: flagNum,
|
|
1965
|
+
position: 0
|
|
1966
|
+
});
|
|
1967
|
+
return this.createFileHandle(fd, filePath);
|
|
1968
|
+
},
|
|
1969
|
+
opendir: async (dirPath) => {
|
|
1970
|
+
return this.createDir(dirPath);
|
|
1971
|
+
},
|
|
1972
|
+
mkdtemp: async (prefix) => {
|
|
1973
|
+
const suffix = Math.random().toString(36).substring(2, 8);
|
|
1974
|
+
const tmpDir = `${prefix}${suffix}`;
|
|
1975
|
+
await this.promises.mkdir(tmpDir, { recursive: true });
|
|
1976
|
+
return tmpDir;
|
|
1977
|
+
},
|
|
1978
|
+
watch: (filePath, options) => {
|
|
1979
|
+
return this.createAsyncWatcher(filePath, options);
|
|
1980
|
+
},
|
|
1981
|
+
/**
|
|
1982
|
+
* Flush all pending writes to storage.
|
|
1983
|
+
* Use after writes with { flush: false } to ensure data is persisted.
|
|
1984
|
+
*/
|
|
1985
|
+
flush: async () => {
|
|
1986
|
+
await this.fastCall("flush", "/");
|
|
1987
|
+
},
|
|
1988
|
+
/**
|
|
1989
|
+
* Purge all kernel caches.
|
|
1990
|
+
* Use between major operations to ensure clean state.
|
|
1991
|
+
*/
|
|
1992
|
+
purge: async () => {
|
|
1993
|
+
await this.fastCall("purge", "/");
|
|
1994
|
+
this.statCache.clear();
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
/**
|
|
1998
|
+
* Async flush - use after promises.writeFile with { flush: false }
|
|
1999
|
+
*/
|
|
2000
|
+
async flush() {
|
|
2001
|
+
await this.fastCall("flush", "/");
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Async purge - clears all kernel caches
|
|
2005
|
+
*/
|
|
2006
|
+
async purge() {
|
|
2007
|
+
await this.fastCall("purge", "/");
|
|
2008
|
+
this.statCache.clear();
|
|
2009
|
+
}
|
|
2010
|
+
// Constants
|
|
2011
|
+
constants = constants;
|
|
2012
|
+
// --- FileHandle Implementation ---
|
|
2013
|
+
createFileHandle(fd, filePath) {
|
|
2014
|
+
const self2 = this;
|
|
2015
|
+
const absPath = normalize(resolve(filePath));
|
|
2016
|
+
return {
|
|
2017
|
+
fd,
|
|
2018
|
+
async read(buffer, offset = 0, length, position = null) {
|
|
2019
|
+
const len = length ?? buffer.length - offset;
|
|
2020
|
+
const entry = self2.fdTable.get(fd);
|
|
2021
|
+
if (!entry) throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
2022
|
+
const readPos = position !== null ? position : entry.position;
|
|
2023
|
+
const result = await self2.fastCall("read", absPath, { offset: readPos, len });
|
|
2024
|
+
if (!result.data) {
|
|
2025
|
+
return { bytesRead: 0, buffer };
|
|
2026
|
+
}
|
|
2027
|
+
const bytesRead = Math.min(result.data.length, len);
|
|
2028
|
+
buffer.set(result.data.subarray(0, bytesRead), offset);
|
|
2029
|
+
if (position === null) {
|
|
2030
|
+
entry.position += bytesRead;
|
|
2031
|
+
}
|
|
2032
|
+
return { bytesRead, buffer };
|
|
2033
|
+
},
|
|
2034
|
+
async write(buffer, offset = 0, length, position = null) {
|
|
2035
|
+
const len = length ?? buffer.length - offset;
|
|
2036
|
+
const entry = self2.fdTable.get(fd);
|
|
2037
|
+
if (!entry) throw new FSError("EBADF", -9, `bad file descriptor: ${fd}`);
|
|
2038
|
+
const writePos = position !== null ? position : entry.position;
|
|
2039
|
+
const data = buffer.subarray(offset, offset + len);
|
|
2040
|
+
await self2.fastCall("write", absPath, { data, offset: writePos, truncate: false });
|
|
2041
|
+
self2.invalidateStat(absPath);
|
|
2042
|
+
if (position === null) {
|
|
2043
|
+
entry.position += len;
|
|
2044
|
+
}
|
|
2045
|
+
return { bytesWritten: len, buffer };
|
|
2046
|
+
},
|
|
2047
|
+
async readFile(options) {
|
|
2048
|
+
return self2.promises.readFile(absPath, options);
|
|
2049
|
+
},
|
|
2050
|
+
async writeFile(data, options) {
|
|
2051
|
+
return self2.promises.writeFile(absPath, data, options);
|
|
2052
|
+
},
|
|
2053
|
+
async truncate(len = 0) {
|
|
2054
|
+
await self2.fastCall("truncate", absPath, { len });
|
|
2055
|
+
self2.invalidateStat(absPath);
|
|
2056
|
+
},
|
|
2057
|
+
async stat() {
|
|
2058
|
+
return self2.promises.stat(absPath);
|
|
2059
|
+
},
|
|
2060
|
+
async sync() {
|
|
2061
|
+
await self2.fastCall("flush", "/");
|
|
2062
|
+
},
|
|
2063
|
+
async datasync() {
|
|
2064
|
+
await self2.fastCall("flush", "/");
|
|
2065
|
+
},
|
|
2066
|
+
async close() {
|
|
2067
|
+
self2.fdTable.delete(fd);
|
|
2068
|
+
}
|
|
2069
|
+
};
|
|
2070
|
+
}
|
|
2071
|
+
// --- Dir Implementation ---
|
|
2072
|
+
createDir(dirPath) {
|
|
2073
|
+
const self2 = this;
|
|
2074
|
+
const absPath = normalize(resolve(dirPath));
|
|
2075
|
+
let entries = null;
|
|
2076
|
+
let index = 0;
|
|
2077
|
+
let closed = false;
|
|
2078
|
+
const loadEntries = async () => {
|
|
2079
|
+
if (entries === null) {
|
|
2080
|
+
const result = await self2.fastCall("readdir", absPath);
|
|
2081
|
+
entries = result.entries || [];
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
const dir = {
|
|
2085
|
+
path: absPath,
|
|
2086
|
+
async read() {
|
|
2087
|
+
if (closed) throw new FSError("EBADF", -9, "Directory handle was closed");
|
|
2088
|
+
await loadEntries();
|
|
2089
|
+
if (index >= entries.length) return null;
|
|
2090
|
+
const name = entries[index++];
|
|
2091
|
+
try {
|
|
2092
|
+
const stat = await self2.fastCall("stat", join(absPath, name));
|
|
2093
|
+
const isDir = stat.type === "directory" || stat.isDirectory === true;
|
|
2094
|
+
return createDirent(name, isDir);
|
|
2095
|
+
} catch {
|
|
2096
|
+
return createDirent(name, false);
|
|
2097
|
+
}
|
|
2098
|
+
},
|
|
2099
|
+
async close() {
|
|
2100
|
+
closed = true;
|
|
2101
|
+
entries = null;
|
|
2102
|
+
},
|
|
2103
|
+
[Symbol.asyncIterator]() {
|
|
2104
|
+
const iterator = {
|
|
2105
|
+
next: async () => {
|
|
2106
|
+
const dirent = await dir.read();
|
|
2107
|
+
if (dirent === null) {
|
|
2108
|
+
return { done: true, value: void 0 };
|
|
2109
|
+
}
|
|
2110
|
+
return { done: false, value: dirent };
|
|
2111
|
+
},
|
|
2112
|
+
[Symbol.asyncIterator]() {
|
|
2113
|
+
return this;
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
return iterator;
|
|
2117
|
+
}
|
|
2118
|
+
};
|
|
2119
|
+
return dir;
|
|
2120
|
+
}
|
|
2121
|
+
// --- Watch Implementation (Native FileSystemObserver with polling fallback) ---
|
|
2122
|
+
watchedFiles = /* @__PURE__ */ new Map();
|
|
2123
|
+
// Check if native FileSystemObserver is available
|
|
2124
|
+
static hasNativeObserver = typeof globalThis.FileSystemObserver !== "undefined";
|
|
2125
|
+
// Get OPFS directory handle for a path
|
|
2126
|
+
async getDirectoryHandle(dirPath, create = false) {
|
|
2127
|
+
const parts = dirPath.split("/").filter(Boolean);
|
|
2128
|
+
let current = await navigator.storage.getDirectory();
|
|
2129
|
+
for (const part of parts) {
|
|
2130
|
+
current = await current.getDirectoryHandle(part, { create });
|
|
2131
|
+
}
|
|
2132
|
+
return current;
|
|
2133
|
+
}
|
|
2134
|
+
// Get OPFS file handle for a path
|
|
2135
|
+
async getFileHandle(filePath, create = false) {
|
|
2136
|
+
const parts = filePath.split("/").filter(Boolean);
|
|
2137
|
+
const fileName = parts.pop();
|
|
2138
|
+
if (!fileName) throw new Error("Invalid file path");
|
|
2139
|
+
let current = await navigator.storage.getDirectory();
|
|
2140
|
+
for (const part of parts) {
|
|
2141
|
+
current = await current.getDirectoryHandle(part, { create });
|
|
2142
|
+
}
|
|
2143
|
+
return current.getFileHandle(fileName, { create });
|
|
2144
|
+
}
|
|
2145
|
+
// Convert FileSystemObserver change type to Node.js event type
|
|
2146
|
+
mapChangeType(type) {
|
|
2147
|
+
switch (type) {
|
|
2148
|
+
case "appeared":
|
|
2149
|
+
case "disappeared":
|
|
2150
|
+
case "moved":
|
|
2151
|
+
return "rename";
|
|
2152
|
+
case "modified":
|
|
2153
|
+
return "change";
|
|
2154
|
+
default:
|
|
2155
|
+
return "change";
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
createAsyncWatcher(filePath, options) {
|
|
2159
|
+
const absPath = normalize(resolve(filePath));
|
|
2160
|
+
if (_OPFSFileSystem.hasNativeObserver) {
|
|
2161
|
+
return this.createNativeAsyncWatcher(absPath, options);
|
|
2162
|
+
}
|
|
2163
|
+
return this.createPollingAsyncWatcher(absPath, options);
|
|
2164
|
+
}
|
|
2165
|
+
createNativeAsyncWatcher(absPath, options) {
|
|
2166
|
+
const self2 = this;
|
|
2167
|
+
return {
|
|
2168
|
+
[Symbol.asyncIterator]() {
|
|
2169
|
+
const eventQueue = [];
|
|
2170
|
+
let resolveNext = null;
|
|
2171
|
+
let observer = null;
|
|
2172
|
+
let aborted = false;
|
|
2173
|
+
let initialized = false;
|
|
2174
|
+
if (options?.signal) {
|
|
2175
|
+
options.signal.addEventListener("abort", () => {
|
|
2176
|
+
aborted = true;
|
|
2177
|
+
observer?.disconnect();
|
|
2178
|
+
if (resolveNext) {
|
|
2179
|
+
resolveNext({ done: true, value: void 0 });
|
|
2180
|
+
resolveNext = null;
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
const callback = (records) => {
|
|
2185
|
+
for (const record of records) {
|
|
2186
|
+
if (record.type === "errored" || record.type === "unknown") continue;
|
|
2187
|
+
const filename = record.relativePathComponents.length > 0 ? record.relativePathComponents[record.relativePathComponents.length - 1] : basename(absPath);
|
|
2188
|
+
const event = {
|
|
2189
|
+
eventType: self2.mapChangeType(record.type),
|
|
2190
|
+
filename
|
|
2191
|
+
};
|
|
2192
|
+
if (resolveNext) {
|
|
2193
|
+
resolveNext({ done: false, value: event });
|
|
2194
|
+
resolveNext = null;
|
|
2195
|
+
} else {
|
|
2196
|
+
eventQueue.push(event);
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
};
|
|
2200
|
+
const init = async () => {
|
|
2201
|
+
if (initialized) return;
|
|
2202
|
+
initialized = true;
|
|
2203
|
+
try {
|
|
2204
|
+
observer = new globalThis.FileSystemObserver(callback);
|
|
2205
|
+
const stat = await self2.promises.stat(absPath);
|
|
2206
|
+
const handle = stat.isDirectory() ? await self2.getDirectoryHandle(absPath) : await self2.getFileHandle(absPath);
|
|
2207
|
+
await observer.observe(handle, { recursive: options?.recursive });
|
|
2208
|
+
} catch (e) {
|
|
2209
|
+
aborted = true;
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
const iterator = {
|
|
2213
|
+
async next() {
|
|
2214
|
+
if (aborted) {
|
|
2215
|
+
return { done: true, value: void 0 };
|
|
2216
|
+
}
|
|
2217
|
+
await init();
|
|
2218
|
+
if (aborted) {
|
|
2219
|
+
return { done: true, value: void 0 };
|
|
2220
|
+
}
|
|
2221
|
+
if (eventQueue.length > 0) {
|
|
2222
|
+
return { done: false, value: eventQueue.shift() };
|
|
2223
|
+
}
|
|
2224
|
+
return new Promise((resolve2) => {
|
|
2225
|
+
resolveNext = resolve2;
|
|
2226
|
+
});
|
|
2227
|
+
},
|
|
2228
|
+
[Symbol.asyncIterator]() {
|
|
2229
|
+
return this;
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
return iterator;
|
|
2233
|
+
}
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
createPollingAsyncWatcher(absPath, options) {
|
|
2237
|
+
const self2 = this;
|
|
2238
|
+
const interval = 1e3;
|
|
2239
|
+
return {
|
|
2240
|
+
[Symbol.asyncIterator]() {
|
|
2241
|
+
let lastMtimeMs = null;
|
|
2242
|
+
let lastEntries = null;
|
|
2243
|
+
let aborted = false;
|
|
2244
|
+
let pollTimeout = null;
|
|
2245
|
+
if (options?.signal) {
|
|
2246
|
+
options.signal.addEventListener("abort", () => {
|
|
2247
|
+
aborted = true;
|
|
2248
|
+
if (pollTimeout) clearTimeout(pollTimeout);
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
const checkForChanges = async () => {
|
|
2252
|
+
if (aborted) return null;
|
|
2253
|
+
try {
|
|
2254
|
+
const stat = await self2.promises.stat(absPath);
|
|
2255
|
+
if (stat.isDirectory()) {
|
|
2256
|
+
const entries = await self2.promises.readdir(absPath);
|
|
2257
|
+
const currentEntries = new Set(entries);
|
|
2258
|
+
if (lastEntries === null) {
|
|
2259
|
+
lastEntries = currentEntries;
|
|
2260
|
+
return null;
|
|
2261
|
+
}
|
|
2262
|
+
for (const entry of currentEntries) {
|
|
2263
|
+
if (!lastEntries.has(entry)) {
|
|
2264
|
+
lastEntries = currentEntries;
|
|
2265
|
+
return { eventType: "rename", filename: entry };
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
for (const entry of lastEntries) {
|
|
2269
|
+
if (!currentEntries.has(entry)) {
|
|
2270
|
+
lastEntries = currentEntries;
|
|
2271
|
+
return { eventType: "rename", filename: entry };
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
lastEntries = currentEntries;
|
|
2275
|
+
} else {
|
|
2276
|
+
if (lastMtimeMs === null) {
|
|
2277
|
+
lastMtimeMs = stat.mtimeMs;
|
|
2278
|
+
return null;
|
|
2279
|
+
}
|
|
2280
|
+
if (stat.mtimeMs !== lastMtimeMs) {
|
|
2281
|
+
lastMtimeMs = stat.mtimeMs;
|
|
2282
|
+
return { eventType: "change", filename: basename(absPath) };
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
} catch {
|
|
2286
|
+
if (lastMtimeMs !== null || lastEntries !== null) {
|
|
2287
|
+
lastMtimeMs = null;
|
|
2288
|
+
lastEntries = null;
|
|
2289
|
+
return { eventType: "rename", filename: basename(absPath) };
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
return null;
|
|
2293
|
+
};
|
|
2294
|
+
const iterator = {
|
|
2295
|
+
async next() {
|
|
2296
|
+
if (aborted) {
|
|
2297
|
+
return { done: true, value: void 0 };
|
|
2298
|
+
}
|
|
2299
|
+
while (!aborted) {
|
|
2300
|
+
const event = await checkForChanges();
|
|
2301
|
+
if (event) {
|
|
2302
|
+
return { done: false, value: event };
|
|
2303
|
+
}
|
|
2304
|
+
await new Promise((resolve2) => {
|
|
2305
|
+
pollTimeout = setTimeout(resolve2, interval);
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
return { done: true, value: void 0 };
|
|
2309
|
+
},
|
|
2310
|
+
[Symbol.asyncIterator]() {
|
|
2311
|
+
return this;
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
return iterator;
|
|
2315
|
+
}
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
/**
|
|
2319
|
+
* Watch a file or directory for changes.
|
|
2320
|
+
* Uses native FileSystemObserver when available, falls back to polling.
|
|
2321
|
+
*/
|
|
2322
|
+
watch(filePath, options = {}, listener) {
|
|
2323
|
+
const absPath = normalize(resolve(filePath));
|
|
2324
|
+
const opts = typeof options === "function" ? {} : options;
|
|
2325
|
+
const cb = typeof options === "function" ? options : listener;
|
|
2326
|
+
if (_OPFSFileSystem.hasNativeObserver) {
|
|
2327
|
+
return this.createNativeWatcher(absPath, opts, cb);
|
|
2328
|
+
}
|
|
2329
|
+
return this.createPollingWatcher(absPath, cb);
|
|
2330
|
+
}
|
|
2331
|
+
createNativeWatcher(absPath, opts, cb) {
|
|
2332
|
+
const self2 = this;
|
|
2333
|
+
let observer = null;
|
|
2334
|
+
let closed = false;
|
|
2335
|
+
const callback = (records) => {
|
|
2336
|
+
if (closed) return;
|
|
2337
|
+
for (const record of records) {
|
|
2338
|
+
if (record.type === "errored" || record.type === "unknown") continue;
|
|
2339
|
+
const filename = record.relativePathComponents.length > 0 ? record.relativePathComponents[record.relativePathComponents.length - 1] : basename(absPath);
|
|
2340
|
+
cb?.(self2.mapChangeType(record.type), filename);
|
|
2341
|
+
}
|
|
2342
|
+
};
|
|
2343
|
+
(async () => {
|
|
2344
|
+
if (closed) return;
|
|
2345
|
+
try {
|
|
2346
|
+
observer = new globalThis.FileSystemObserver(callback);
|
|
2347
|
+
const stat = await self2.promises.stat(absPath);
|
|
2348
|
+
const handle = stat.isDirectory() ? await self2.getDirectoryHandle(absPath) : await self2.getFileHandle(absPath);
|
|
2349
|
+
await observer.observe(handle, { recursive: opts.recursive });
|
|
2350
|
+
} catch {
|
|
2351
|
+
}
|
|
2352
|
+
})();
|
|
2353
|
+
const watcher = {
|
|
2354
|
+
close: () => {
|
|
2355
|
+
closed = true;
|
|
2356
|
+
observer?.disconnect();
|
|
2357
|
+
},
|
|
2358
|
+
ref: () => watcher,
|
|
2359
|
+
unref: () => watcher
|
|
2360
|
+
};
|
|
2361
|
+
return watcher;
|
|
2362
|
+
}
|
|
2363
|
+
createPollingWatcher(absPath, cb) {
|
|
2364
|
+
const interval = 1e3;
|
|
2365
|
+
let lastMtimeMs = null;
|
|
2366
|
+
let lastEntries = null;
|
|
2367
|
+
let closed = false;
|
|
2368
|
+
const poll = async () => {
|
|
2369
|
+
if (closed) return;
|
|
2370
|
+
try {
|
|
2371
|
+
const stat = await this.promises.stat(absPath);
|
|
2372
|
+
if (stat.isDirectory()) {
|
|
2373
|
+
const entries = await this.promises.readdir(absPath);
|
|
2374
|
+
const currentEntries = new Set(entries);
|
|
2375
|
+
if (lastEntries !== null) {
|
|
2376
|
+
for (const entry of currentEntries) {
|
|
2377
|
+
if (!lastEntries.has(entry)) {
|
|
2378
|
+
cb?.("rename", entry);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
for (const entry of lastEntries) {
|
|
2382
|
+
if (!currentEntries.has(entry)) {
|
|
2383
|
+
cb?.("rename", entry);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
lastEntries = currentEntries;
|
|
2388
|
+
} else {
|
|
2389
|
+
if (lastMtimeMs !== null && stat.mtimeMs !== lastMtimeMs) {
|
|
2390
|
+
cb?.("change", basename(absPath));
|
|
2391
|
+
}
|
|
2392
|
+
lastMtimeMs = stat.mtimeMs;
|
|
2393
|
+
}
|
|
2394
|
+
} catch {
|
|
2395
|
+
if (lastMtimeMs !== null || lastEntries !== null) {
|
|
2396
|
+
cb?.("rename", basename(absPath));
|
|
2397
|
+
lastMtimeMs = null;
|
|
2398
|
+
lastEntries = null;
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
const intervalId = setInterval(poll, interval);
|
|
2403
|
+
poll();
|
|
2404
|
+
const watcher = {
|
|
2405
|
+
close: () => {
|
|
2406
|
+
closed = true;
|
|
2407
|
+
clearInterval(intervalId);
|
|
2408
|
+
},
|
|
2409
|
+
ref: () => watcher,
|
|
2410
|
+
unref: () => watcher
|
|
2411
|
+
};
|
|
2412
|
+
return watcher;
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Watch a file for changes using native FileSystemObserver or stat polling.
|
|
2416
|
+
*/
|
|
2417
|
+
watchFile(filePath, options = {}, listener) {
|
|
2418
|
+
const absPath = normalize(resolve(filePath));
|
|
2419
|
+
const opts = typeof options === "function" ? {} : options;
|
|
2420
|
+
const cb = typeof options === "function" ? options : listener;
|
|
2421
|
+
const interval = opts.interval ?? 5007;
|
|
2422
|
+
let lastStat = null;
|
|
2423
|
+
let observer;
|
|
2424
|
+
const poll = async () => {
|
|
2425
|
+
try {
|
|
2426
|
+
const stat = await this.promises.stat(absPath);
|
|
2427
|
+
if (lastStat !== null) {
|
|
2428
|
+
if (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size) {
|
|
2429
|
+
cb?.(stat, lastStat);
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
2432
|
+
lastStat = stat;
|
|
2433
|
+
} catch {
|
|
2434
|
+
const emptyStat = createStats({ type: "file", size: 0, mtimeMs: 0, mode: 0 });
|
|
2435
|
+
if (lastStat !== null) {
|
|
2436
|
+
cb?.(emptyStat, lastStat);
|
|
2437
|
+
}
|
|
2438
|
+
lastStat = emptyStat;
|
|
2439
|
+
}
|
|
2440
|
+
};
|
|
2441
|
+
if (_OPFSFileSystem.hasNativeObserver && cb) {
|
|
2442
|
+
const self2 = this;
|
|
2443
|
+
const observerCallback = async () => {
|
|
2444
|
+
try {
|
|
2445
|
+
const stat = await self2.promises.stat(absPath);
|
|
2446
|
+
if (lastStat !== null && (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size)) {
|
|
2447
|
+
cb(stat, lastStat);
|
|
2448
|
+
}
|
|
2449
|
+
lastStat = stat;
|
|
2450
|
+
} catch {
|
|
2451
|
+
const emptyStat = createStats({ type: "file", size: 0, mtimeMs: 0, mode: 0 });
|
|
2452
|
+
if (lastStat !== null) {
|
|
2453
|
+
cb(emptyStat, lastStat);
|
|
2454
|
+
}
|
|
2455
|
+
lastStat = emptyStat;
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
(async () => {
|
|
2459
|
+
try {
|
|
2460
|
+
lastStat = await self2.promises.stat(absPath);
|
|
2461
|
+
observer = new globalThis.FileSystemObserver(observerCallback);
|
|
2462
|
+
const handle = await self2.getFileHandle(absPath);
|
|
2463
|
+
await observer.observe(handle);
|
|
2464
|
+
} catch {
|
|
2465
|
+
if (!this.watchedFiles.get(absPath)?.interval) {
|
|
2466
|
+
const entry = this.watchedFiles.get(absPath);
|
|
2467
|
+
if (entry) {
|
|
2468
|
+
entry.interval = setInterval(poll, interval);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
})();
|
|
2473
|
+
if (!this.watchedFiles.has(absPath)) {
|
|
2474
|
+
this.watchedFiles.set(absPath, {
|
|
2475
|
+
observer,
|
|
2476
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
2477
|
+
lastStat: null
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
this.watchedFiles.get(absPath).listeners.add(cb);
|
|
2481
|
+
} else {
|
|
2482
|
+
if (!this.watchedFiles.has(absPath)) {
|
|
2483
|
+
this.watchedFiles.set(absPath, {
|
|
2484
|
+
interval: setInterval(poll, interval),
|
|
2485
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
2486
|
+
lastStat: null
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
if (cb) this.watchedFiles.get(absPath).listeners.add(cb);
|
|
2490
|
+
poll();
|
|
2491
|
+
}
|
|
2492
|
+
const watcher = {
|
|
2493
|
+
ref: () => watcher,
|
|
2494
|
+
unref: () => watcher
|
|
2495
|
+
};
|
|
2496
|
+
return watcher;
|
|
2497
|
+
}
|
|
2498
|
+
/**
|
|
2499
|
+
* Stop watching a file.
|
|
2500
|
+
*/
|
|
2501
|
+
unwatchFile(filePath, listener) {
|
|
2502
|
+
const absPath = normalize(resolve(filePath));
|
|
2503
|
+
const entry = this.watchedFiles.get(absPath);
|
|
2504
|
+
if (entry) {
|
|
2505
|
+
if (listener) {
|
|
2506
|
+
entry.listeners.delete(listener);
|
|
2507
|
+
if (entry.listeners.size === 0) {
|
|
2508
|
+
if (entry.interval) clearInterval(entry.interval);
|
|
2509
|
+
if (entry.observer) entry.observer.disconnect();
|
|
2510
|
+
this.watchedFiles.delete(absPath);
|
|
2511
|
+
}
|
|
2512
|
+
} else {
|
|
2513
|
+
if (entry.interval) clearInterval(entry.interval);
|
|
2514
|
+
if (entry.observer) entry.observer.disconnect();
|
|
2515
|
+
this.watchedFiles.delete(absPath);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
// --- Stream Implementation ---
|
|
2520
|
+
/**
|
|
2521
|
+
* Create a readable stream for a file.
|
|
2522
|
+
*/
|
|
2523
|
+
createReadStream(filePath, options) {
|
|
2524
|
+
const opts = typeof options === "string" ? { } : options ?? {};
|
|
2525
|
+
const absPath = normalize(resolve(filePath));
|
|
2526
|
+
const start = opts.start ?? 0;
|
|
2527
|
+
const end = opts.end;
|
|
2528
|
+
const highWaterMark = opts.highWaterMark ?? 64 * 1024;
|
|
2529
|
+
let position = start;
|
|
2530
|
+
let closed = false;
|
|
2531
|
+
const self2 = this;
|
|
2532
|
+
return new ReadableStream({
|
|
2533
|
+
async pull(controller) {
|
|
2534
|
+
if (closed) {
|
|
2535
|
+
controller.close();
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
try {
|
|
2539
|
+
const maxRead = end !== void 0 ? Math.min(highWaterMark, end - position + 1) : highWaterMark;
|
|
2540
|
+
if (maxRead <= 0) {
|
|
2541
|
+
controller.close();
|
|
2542
|
+
closed = true;
|
|
2543
|
+
return;
|
|
2544
|
+
}
|
|
2545
|
+
const result = await self2.fastCall("read", absPath, { offset: position, len: maxRead });
|
|
2546
|
+
if (!result.data || result.data.length === 0) {
|
|
2547
|
+
controller.close();
|
|
2548
|
+
closed = true;
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
controller.enqueue(result.data);
|
|
2552
|
+
position += result.data.length;
|
|
2553
|
+
if (end !== void 0 && position > end) {
|
|
2554
|
+
controller.close();
|
|
2555
|
+
closed = true;
|
|
2556
|
+
}
|
|
2557
|
+
} catch (e) {
|
|
2558
|
+
controller.error(e);
|
|
2559
|
+
closed = true;
|
|
2560
|
+
}
|
|
2561
|
+
},
|
|
2562
|
+
cancel() {
|
|
2563
|
+
closed = true;
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
}
|
|
2567
|
+
/**
|
|
2568
|
+
* Create a writable stream for a file.
|
|
2569
|
+
*/
|
|
2570
|
+
createWriteStream(filePath, options) {
|
|
2571
|
+
const opts = typeof options === "string" ? { } : options ?? {};
|
|
2572
|
+
const absPath = normalize(resolve(filePath));
|
|
2573
|
+
const start = opts.start ?? 0;
|
|
2574
|
+
const shouldFlush = opts.flush !== false;
|
|
2575
|
+
let position = start;
|
|
2576
|
+
let initialized = false;
|
|
2577
|
+
const self2 = this;
|
|
2578
|
+
return new WritableStream({
|
|
2579
|
+
async write(chunk) {
|
|
2580
|
+
if (!initialized && start === 0) {
|
|
2581
|
+
await self2.fastCall("write", absPath, { data: chunk, offset: 0, flush: false });
|
|
2582
|
+
position = chunk.length;
|
|
2583
|
+
initialized = true;
|
|
2584
|
+
} else {
|
|
2585
|
+
await self2.fastCall("write", absPath, { data: chunk, offset: position, truncate: false, flush: false });
|
|
2586
|
+
position += chunk.length;
|
|
2587
|
+
initialized = true;
|
|
2588
|
+
}
|
|
2589
|
+
self2.invalidateStat(absPath);
|
|
2590
|
+
},
|
|
2591
|
+
async close() {
|
|
2592
|
+
if (shouldFlush) {
|
|
2593
|
+
await self2.fastCall("flush", "/");
|
|
2594
|
+
}
|
|
2595
|
+
},
|
|
2596
|
+
async abort() {
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
// --- Sync methods for opendir and mkdtemp ---
|
|
2601
|
+
/**
|
|
2602
|
+
* Open a directory for iteration (sync).
|
|
2603
|
+
*/
|
|
2604
|
+
opendirSync(dirPath) {
|
|
2605
|
+
return this.createDir(dirPath);
|
|
2606
|
+
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Create a unique temporary directory (sync).
|
|
2609
|
+
*/
|
|
2610
|
+
mkdtempSync(prefix) {
|
|
2611
|
+
const suffix = Math.random().toString(36).substring(2, 8);
|
|
2612
|
+
const tmpDir = `${prefix}${suffix}`;
|
|
2613
|
+
this.mkdirSync(tmpDir, { recursive: true });
|
|
2614
|
+
return tmpDir;
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
|
|
2618
|
+
// src/index.ts
|
|
2619
|
+
var fs = new OPFSFileSystem();
|
|
2620
|
+
var index_default = fs;
|
|
2621
|
+
|
|
2622
|
+
exports.FSError = FSError;
|
|
2623
|
+
exports.OPFSFileSystem = OPFSFileSystem;
|
|
2624
|
+
exports.constants = constants;
|
|
2625
|
+
exports.createEACCES = createEACCES;
|
|
2626
|
+
exports.createEEXIST = createEEXIST;
|
|
2627
|
+
exports.createEINVAL = createEINVAL;
|
|
2628
|
+
exports.createEISDIR = createEISDIR;
|
|
2629
|
+
exports.createENOENT = createENOENT;
|
|
2630
|
+
exports.createENOTDIR = createENOTDIR;
|
|
2631
|
+
exports.createENOTEMPTY = createENOTEMPTY;
|
|
2632
|
+
exports.default = index_default;
|
|
2633
|
+
exports.fs = fs;
|
|
2634
|
+
exports.mapErrorCode = mapErrorCode;
|
|
2635
|
+
exports.path = path_exports;
|
|
2636
|
+
//# sourceMappingURL=index.cjs.map
|
|
2637
|
+
//# sourceMappingURL=index.cjs.map
|