@componentor/fs 2.0.12 → 3.0.1

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