@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/README.md +266 -294
- package/dist/index.js +1453 -2565
- package/dist/index.js.map +1 -1
- package/dist/workers/async-relay.worker.js +298 -0
- package/dist/workers/async-relay.worker.js.map +1 -0
- package/dist/workers/opfs-sync.worker.js +249 -0
- package/dist/workers/opfs-sync.worker.js.map +1 -0
- package/dist/workers/server.worker.js +1547 -0
- package/dist/workers/server.worker.js.map +1 -0
- package/dist/workers/service.worker.js +39 -0
- package/dist/workers/service.worker.js.map +1 -0
- package/dist/workers/sync-relay.worker.js +2031 -0
- package/dist/workers/sync-relay.worker.js.map +1 -0
- package/package.json +11 -11
- package/dist/index.cjs +0 -2811
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -641
- package/dist/index.d.ts +0 -641
- package/dist/kernel.js +0 -544
- package/dist/kernel.js.map +0 -1
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/
|
|
8
|
-
var
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
90
|
-
|
|
149
|
+
function statusToError(status, syscall, path) {
|
|
150
|
+
const code = STATUS_TO_CODE[status] ?? "EINVAL";
|
|
151
|
+
return createError(code, syscall, path);
|
|
91
152
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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 (
|
|
229
|
-
if (
|
|
230
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
var
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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/
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
//
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
const
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
556
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
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
|
-
|
|
593
|
-
const
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
601
|
-
const
|
|
602
|
-
|
|
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
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
//
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
return
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const {
|
|
730
|
-
if (
|
|
731
|
-
|
|
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
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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
|
-
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
}
|
|
760
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
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
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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
|
-
|
|
886
|
-
const
|
|
887
|
-
if (
|
|
888
|
-
|
|
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
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
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
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
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
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
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.
|
|
1113
|
+
this.startAsFollower();
|
|
1114
|
+
this.waitForLeaderLock();
|
|
1134
1115
|
}
|
|
1135
1116
|
});
|
|
1136
1117
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
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
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1311
|
-
|
|
1312
|
-
if (
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
-
//
|
|
1410
|
+
// ========== Sync API ==========
|
|
1590
1411
|
readFileSync(filePath, options) {
|
|
1591
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1654
|
-
this.invalidateStat(filePath);
|
|
1433
|
+
unlinkSync(this._sync, filePath);
|
|
1655
1434
|
}
|
|
1656
1435
|
readdirSync(filePath, options) {
|
|
1657
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1763
|
-
|
|
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
|
-
|
|
1779
|
-
|
|
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
|
-
|
|
1785
|
-
|
|
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
|
-
|
|
1802
|
-
|
|
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
|
-
|
|
1820
|
-
|
|
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
|
-
|
|
1827
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1924
|
-
|
|
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
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2226
|
-
|
|
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
|
-
|
|
2313
|
-
|
|
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
|
-
|
|
2391
|
-
|
|
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
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
//
|
|
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
|
|
2696
|
-
const
|
|
2697
|
-
const
|
|
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
|
|
2704
|
-
if (closed) {
|
|
2705
|
-
controller.close();
|
|
2706
|
-
return;
|
|
2707
|
-
}
|
|
1518
|
+
pull: async (controller) => {
|
|
2708
1519
|
try {
|
|
2709
|
-
const
|
|
2710
|
-
if (
|
|
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
|
|
2716
|
-
|
|
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(
|
|
2722
|
-
position +=
|
|
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 (
|
|
2728
|
-
controller.error(
|
|
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
|
-
|
|
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
|
|
2750
|
-
if (!initialized
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
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
|
-
|
|
1555
|
+
await this.promises.appendFile(filePath, chunk);
|
|
1556
|
+
position += chunk.byteLength;
|
|
2760
1557
|
},
|
|
2761
|
-
async
|
|
2762
|
-
if (
|
|
2763
|
-
await
|
|
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
|
-
//
|
|
2771
|
-
|
|
2772
|
-
|
|
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
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
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
|
-
|
|
2790
|
-
|
|
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,
|
|
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
|