@ai-ecoverse/slicc-kernel 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,152 @@
1
+ // src/fs/meta.ts
2
+ var META_DB = "slicc-kernel";
3
+ var META_VERSION = 1;
4
+ var META_STORE = "entries";
5
+ function parentOf(path) {
6
+ return path.slice(0, path.lastIndexOf("/")) || "/";
7
+ }
8
+ function moved(entry, path) {
9
+ return entry.link === void 0 ? { ...entry, path } : { ...entry, path, dir: parentOf(path) };
10
+ }
11
+ function under(path, root) {
12
+ return path === root || path.startsWith(`${root}/`);
13
+ }
14
+ var MemoryMeta = class {
15
+ entries = /* @__PURE__ */ new Map();
16
+ async get(paths) {
17
+ return paths.map((path) => this.entries.get(path));
18
+ }
19
+ async update(path, change) {
20
+ this.set(path, change(this.entries.get(path)));
21
+ }
22
+ set(path, entry) {
23
+ if (entry) this.entries.set(path, entry);
24
+ else this.entries.delete(path);
25
+ }
26
+ async move(from, to, root) {
27
+ await this.remove([to]);
28
+ const carried = [...this.entries.values()].filter((entry) => under(entry.path, from));
29
+ for (const entry of carried) this.entries.delete(entry.path);
30
+ for (const entry of carried) {
31
+ const path = to + entry.path.slice(from.length);
32
+ if (path !== to) this.entries.set(path, moved(entry, path));
33
+ }
34
+ this.entries.set(to, moved(root(carried.find((entry) => entry.path === from)), to));
35
+ }
36
+ async remove(paths) {
37
+ for (const key of [...this.entries.keys()]) {
38
+ if (paths.some((path) => under(key, path))) this.entries.delete(key);
39
+ }
40
+ }
41
+ async links(dir) {
42
+ return [...this.entries.values()].filter((entry) => entry.dir === dir);
43
+ }
44
+ async under(path) {
45
+ return [...this.entries.values()].filter((entry) => under(entry.path, path));
46
+ }
47
+ async all() {
48
+ return [...this.entries.values()];
49
+ }
50
+ };
51
+ function descendants(path) {
52
+ return IDBKeyRange.bound(`${path}/`, `${path}0`, false, true);
53
+ }
54
+ function settle(transaction, value) {
55
+ return new Promise((resolve, reject) => {
56
+ transaction.oncomplete = () => resolve(value());
57
+ transaction.onabort = () => reject(transaction.error ?? new Error("metadata transaction aborted"));
58
+ });
59
+ }
60
+ var IndexedDbMeta = class {
61
+ db;
62
+ constructor(db) {
63
+ this.db = db;
64
+ db.onversionchange = () => db.close();
65
+ }
66
+ transaction(mode) {
67
+ const transaction = this.db.transaction(META_STORE, mode);
68
+ return [transaction, transaction.objectStore(META_STORE)];
69
+ }
70
+ async get(paths) {
71
+ const [transaction, store] = this.transaction("readonly");
72
+ const found = paths.map(() => void 0);
73
+ paths.forEach((path, i) => {
74
+ const request = store.get(path);
75
+ request.onsuccess = () => {
76
+ found[i] = request.result;
77
+ };
78
+ });
79
+ return settle(transaction, () => found);
80
+ }
81
+ async update(path, change) {
82
+ const [transaction, store] = this.transaction("readwrite");
83
+ const request = store.get(path);
84
+ request.onsuccess = () => {
85
+ const next = change(request.result);
86
+ if (next) store.put(next);
87
+ else store.delete(path);
88
+ };
89
+ return settle(transaction, () => void 0);
90
+ }
91
+ async move(from, to, root) {
92
+ const [transaction, store] = this.transaction("readwrite");
93
+ store.delete(to);
94
+ store.delete(descendants(to));
95
+ const source = store.get(from);
96
+ const below = store.getAll(descendants(from));
97
+ below.onsuccess = () => {
98
+ store.delete(from);
99
+ store.delete(descendants(from));
100
+ for (const entry of below.result) {
101
+ store.put(moved(entry, to + entry.path.slice(from.length)));
102
+ }
103
+ store.put(moved(root(source.result), to));
104
+ };
105
+ return settle(transaction, () => void 0);
106
+ }
107
+ async remove(paths) {
108
+ const [transaction, store] = this.transaction("readwrite");
109
+ for (const path of paths) {
110
+ store.delete(path);
111
+ store.delete(descendants(path));
112
+ }
113
+ return settle(transaction, () => void 0);
114
+ }
115
+ async links(dir) {
116
+ const [transaction, store] = this.transaction("readonly");
117
+ const request = store.index("dir").getAll(dir);
118
+ return settle(transaction, () => request.result);
119
+ }
120
+ async under(path) {
121
+ const [transaction, store] = this.transaction("readonly");
122
+ const self = store.get(path);
123
+ const below = store.getAll(descendants(path));
124
+ return settle(
125
+ transaction,
126
+ () => [self.result, ...below.result].filter(
127
+ (entry) => entry !== void 0
128
+ )
129
+ );
130
+ }
131
+ async all() {
132
+ const [transaction, store] = this.transaction("readonly");
133
+ const request = store.getAll();
134
+ return settle(transaction, () => request.result);
135
+ }
136
+ };
137
+ function openMeta(name, factory) {
138
+ return new Promise((resolve, reject) => {
139
+ const request = factory.open(name, META_VERSION);
140
+ request.onupgradeneeded = (event) => {
141
+ if (event.oldVersion < 1) {
142
+ request.result.createObjectStore(META_STORE, { keyPath: "path" }).createIndex("dir", "dir");
143
+ }
144
+ };
145
+ request.onsuccess = () => resolve(new IndexedDbMeta(request.result));
146
+ request.onerror = () => reject(request.error);
147
+ });
148
+ }
149
+
1
150
  // src/fs/types.ts
2
151
  function fsError(code, path) {
3
152
  return Object.assign(new Error(`${code}: ${path}`), { code });
@@ -14,12 +163,26 @@ function normalizePath(path) {
14
163
  function resolvePath(base, path) {
15
164
  return normalizePath(path.startsWith("/") ? path : `${base}/${path}`);
16
165
  }
166
+ function inodeOf(path) {
167
+ let h1 = 3735928559;
168
+ let h2 = 1103547991;
169
+ for (let i = 0; i < path.length; i++) {
170
+ const c = path.charCodeAt(i);
171
+ h1 = Math.imul(h1 ^ c, 2654435761);
172
+ h2 = Math.imul(h2 ^ c, 1597334677);
173
+ }
174
+ h1 = Math.imul(h1 ^ h1 >>> 16, 2246822507) ^ Math.imul(h2 ^ h2 >>> 13, 3266489909);
175
+ h2 = Math.imul(h2 ^ h2 >>> 16, 2246822507) ^ Math.imul(h1 ^ h1 >>> 13, 3266489909);
176
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0) || 1;
177
+ }
17
178
 
18
179
  // src/fs/opfs.ts
19
180
  var S_IFDIR = 16384;
20
181
  var S_IFREG = 32768;
182
+ var S_IFLNK = 40960;
21
183
  var PERM_MASK = 4095;
22
184
  var READ_ATTEMPTS = 3;
185
+ var MAX_LINKS = 40;
23
186
  var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
24
187
  var ERRNO_BY_DOM_ERROR = {
25
188
  NotFoundError: "ENOENT",
@@ -33,26 +196,43 @@ function translate(err, path) {
33
196
  const name = err?.name;
34
197
  return fsError(typeof name === "string" && ERRNO_BY_DOM_ERROR[name] || "EIO", path);
35
198
  }
199
+ function missing(err) {
200
+ const code = err?.code;
201
+ return code === "ENOENT" || code === "ENOTDIR";
202
+ }
36
203
  function split(path) {
37
204
  return path.split("/").filter(Boolean);
38
205
  }
206
+ function parentOf2(path) {
207
+ return path.slice(0, path.lastIndexOf("/")) || "/";
208
+ }
209
+ function baseOf(path) {
210
+ return path.slice(path.lastIndexOf("/") + 1);
211
+ }
212
+ function prefixes(path) {
213
+ const parts = split(path);
214
+ return parts.map((_, i) => `/${parts.slice(0, i + 1).join("/")}`);
215
+ }
39
216
  function within(path, root) {
40
217
  return path === root || path.startsWith(`${root}/`);
41
218
  }
219
+ function isHandle(child) {
220
+ return "kind" in child;
221
+ }
42
222
  var OpfsFs = class {
43
- modes = /* @__PURE__ */ new Map();
44
- times = /* @__PURE__ */ new Map();
45
223
  root;
46
- constructor(root) {
224
+ meta;
225
+ constructor(root, meta = new MemoryMeta()) {
47
226
  this.root = root;
227
+ this.meta = meta;
48
228
  }
49
229
  resolvePath(base, path) {
50
230
  return resolvePath(base, path);
51
231
  }
52
- async dir(path, create = false) {
232
+ async dir(path) {
53
233
  let dir = this.root;
54
234
  try {
55
- for (const part of split(path)) dir = await dir.getDirectoryHandle(part, { create });
235
+ for (const part of split(path)) dir = await dir.getDirectoryHandle(part);
56
236
  } catch (err) {
57
237
  throw translate(err, path);
58
238
  }
@@ -74,34 +254,89 @@ var OpfsFs = class {
74
254
  }
75
255
  return dir.getDirectoryHandle(name);
76
256
  }
77
- mode(path, directory) {
257
+ real(path) {
258
+ return this.handle(path).then(
259
+ () => true,
260
+ () => false
261
+ );
262
+ }
263
+ async linkAlong(path) {
264
+ for (const entry of await this.meta.get(prefixes(path))) {
265
+ if (entry?.link === void 0) continue;
266
+ if (!await this.real(entry.path) && await this.real(parentOf2(entry.path))) return entry;
267
+ }
268
+ return void 0;
269
+ }
270
+ async locate(path, follow) {
271
+ let current = path;
272
+ for (let hops = 0; hops <= MAX_LINKS; hops++) {
273
+ try {
274
+ return { path: current, handle: await this.handle(current) };
275
+ } catch (err) {
276
+ if (!missing(err)) throw err;
277
+ const link = await this.linkAlong(current);
278
+ if (!link) throw err;
279
+ if (link.path === current && !follow) return { path: current, link };
280
+ const rest = current.slice(link.path.length);
281
+ current = resolvePath(parentOf2(link.path), `${link.link}${rest}`);
282
+ }
283
+ }
284
+ throw fsError("ELOOP", path);
285
+ }
286
+ mode(path, directory, meta) {
78
287
  const fallback = directory || EXECUTABLE.test(path) ? 493 : 420;
79
- return (directory ? S_IFDIR : S_IFREG) | (this.modes.get(path) ?? fallback);
288
+ return (directory ? S_IFDIR : S_IFREG) | (meta?.mode ?? fallback);
80
289
  }
81
- async stat(path) {
82
- const handle = await this.handle(path);
290
+ async statOf(path, handle, meta) {
83
291
  const directory = handle.kind === "directory";
84
292
  const file = directory ? void 0 : await handle.getFile();
293
+ const modified = file?.lastModified ?? 0;
294
+ const mtime = meta?.mtimeMs !== void 0 && meta.mtimeFor === modified ? meta.mtimeMs : modified;
85
295
  return {
86
296
  isFile: !directory,
87
297
  isDirectory: directory,
88
298
  isSymbolicLink: false,
89
299
  size: file?.size ?? 0,
90
- mode: this.mode(path, directory),
91
- mtime: new Date(this.times.get(path) ?? file?.lastModified ?? 0)
300
+ mode: this.mode(path, directory, meta),
301
+ mtime: new Date(mtime),
302
+ atime: new Date(meta?.atimeMs ?? mtime),
303
+ ctime: new Date(Math.max(meta?.ctimeMs ?? mtime, modified)),
304
+ ino: meta?.ino ?? inodeOf(path)
305
+ };
306
+ }
307
+ linkStat(entry) {
308
+ const time = new Date(entry.mtimeMs ?? 0);
309
+ return {
310
+ isFile: false,
311
+ isDirectory: false,
312
+ isSymbolicLink: true,
313
+ size: new TextEncoder().encode(entry.link).length,
314
+ mode: S_IFLNK | 511,
315
+ mtime: time,
316
+ atime: time,
317
+ ctime: new Date(entry.ctimeMs ?? 0),
318
+ ino: entry.ino ?? inodeOf(entry.path)
92
319
  };
93
320
  }
94
- lstat(path) {
95
- return this.stat(path);
321
+ async describe(found) {
322
+ if (found.link) return this.linkStat(found.link);
323
+ const [meta] = await this.meta.get([found.path]);
324
+ return this.statOf(found.path, found.handle, meta);
325
+ }
326
+ async stat(path) {
327
+ return this.describe(await this.locate(path, true));
328
+ }
329
+ async lstat(path) {
330
+ return this.describe(await this.locate(path, false));
96
331
  }
97
332
  async exists(path) {
98
- return this.handle(path).then(
333
+ return this.locate(path, true).then(
99
334
  () => true,
100
335
  () => false
101
336
  );
102
337
  }
103
338
  async readFileBuffer(path) {
104
- const handle = await this.handle(path);
339
+ const handle = (await this.locate(path, true)).handle;
105
340
  if (handle.kind === "directory") throw fsError("EISDIR", path);
106
341
  for (let attempt = 1; ; attempt++) {
107
342
  try {
@@ -117,8 +352,19 @@ var OpfsFs = class {
117
352
  async readFile(path) {
118
353
  return new TextDecoder().decode(await this.readFileBuffer(path));
119
354
  }
355
+ async writeTarget(path) {
356
+ try {
357
+ return (await this.locate(path, true)).path;
358
+ } catch (err) {
359
+ if (!missing(err)) throw err;
360
+ }
361
+ const [entry] = await this.meta.get([path]);
362
+ const wanted = entry?.link === void 0 ? path : resolvePath(parentOf2(path), entry.link);
363
+ return resolvePath((await this.locate(parentOf2(wanted), true)).path, baseOf(wanted));
364
+ }
120
365
  async writeFile(path, content) {
121
- const [dir, name] = await this.parent(path);
366
+ const target = await this.writeTarget(path);
367
+ const [dir, name] = await this.parent(target);
122
368
  let file;
123
369
  try {
124
370
  file = await dir.getFileHandle(name, { create: true });
@@ -129,77 +375,146 @@ var OpfsFs = class {
129
375
  const writable = await file.createWritable();
130
376
  await writable.write(content);
131
377
  await writable.close();
132
- this.times.delete(path);
133
378
  }
134
- async readdir(path) {
135
- const handle = await this.handle(path);
379
+ async listing(path) {
380
+ const found = await this.locate(path, true);
381
+ const handle = found.handle;
136
382
  if (handle.kind !== "directory") throw fsError("ENOTDIR", path);
137
- const names = [];
138
- for await (const name of handle.keys()) names.push(name);
139
- return names.sort();
383
+ const children = /* @__PURE__ */ new Map();
384
+ for await (const child of handle.values()) children.set(child.name, child);
385
+ for (const link of await this.meta.links(found.path)) {
386
+ const name = baseOf(link.path);
387
+ if (!children.has(name)) children.set(name, link);
388
+ }
389
+ return [found.path, [...children].sort(([a], [b]) => a < b ? -1 : 1)];
390
+ }
391
+ async readdir(path) {
392
+ return (await this.listing(path))[1].map(([name]) => name);
393
+ }
394
+ async readdirStat(path) {
395
+ const [dir, children] = await this.listing(path);
396
+ const base = dir === "/" ? "" : dir;
397
+ const metas = await this.meta.get(children.map(([name]) => `${base}/${name}`));
398
+ return Promise.all(
399
+ children.map(async ([name, child], i) => {
400
+ if (!isHandle(child)) return [name, this.linkStat(child)];
401
+ const stat = await this.statOf(`${base}/${name}`, child, metas[i]).catch(() => null);
402
+ return [name, stat];
403
+ })
404
+ );
140
405
  }
141
406
  async mkdir(path, options = {}) {
142
- if (options.recursive) {
143
- await this.dir(path, true);
144
- return;
407
+ let found;
408
+ try {
409
+ found = await this.locate(path, true);
410
+ } catch (err) {
411
+ if (!missing(err)) throw err;
145
412
  }
146
- if (await this.exists(path)) throw fsError("EEXIST", path);
147
- const [dir, name] = await this.parent(path);
148
- await dir.getDirectoryHandle(name, { create: true });
413
+ if (found) {
414
+ const directory = found.handle?.kind === "directory";
415
+ if (options.recursive && directory) return;
416
+ throw fsError(options.recursive ? "ENOTDIR" : "EEXIST", path);
417
+ }
418
+ const [entry] = await this.meta.get([path]);
419
+ if (entry?.link !== void 0) throw fsError("EEXIST", path);
420
+ if (options.recursive) await this.mkdir(parentOf2(path), options);
421
+ const parent = await this.locate(parentOf2(path), true);
422
+ if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
423
+ await parent.handle.getDirectoryHandle(baseOf(path), { create: true });
149
424
  }
150
425
  async rm(path, options = {}) {
426
+ let found;
151
427
  try {
152
- const [dir, name] = await this.parent(path);
153
- await dir.removeEntry(name, { recursive: options.recursive === true });
428
+ found = await this.locate(path, false);
154
429
  } catch (err) {
155
430
  const error = translate(err, path);
156
431
  if (options.force && error.code === "ENOENT") return;
157
432
  throw error;
158
433
  }
159
- this.forget(path);
160
- }
161
- forget(path) {
162
- for (const map of [this.modes, this.times]) {
163
- for (const key of [...map.keys()]) if (within(key, path)) map.delete(key);
164
- }
165
- }
166
- carry(from, to) {
167
- for (const map of [this.modes, this.times]) {
168
- for (const [key, value] of [...map]) {
169
- if (!within(key, from)) continue;
170
- map.delete(key);
171
- map.set(to + key.slice(from.length), value);
434
+ if (found.handle) {
435
+ const [dir, name] = await this.parent(found.path);
436
+ try {
437
+ await dir.removeEntry(name, { recursive: options.recursive === true });
438
+ } catch (err) {
439
+ throw translate(err, path);
172
440
  }
173
441
  }
442
+ await this.meta.remove([found.path]);
174
443
  }
175
444
  async rename(from, to) {
176
445
  if (from === to) return;
177
- const source = await this.stat(from);
178
- if (source.isDirectory && within(to, from)) throw fsError("EINVAL", to);
179
- const target = await this.stat(to).catch(() => null);
180
- if (target?.isDirectory && !source.isDirectory) throw fsError("EISDIR", to);
181
- if (target && !target.isDirectory && source.isDirectory) throw fsError("ENOTDIR", to);
182
- if (target?.isDirectory && (await this.readdir(to)).length > 0) {
446
+ const source = await this.locate(from, false);
447
+ const parent = await this.locate(parentOf2(to), true);
448
+ const destination = resolvePath(parent.path, baseOf(to));
449
+ const sourceStat = await this.describe(source);
450
+ if (sourceStat.isDirectory && within(destination, source.path)) throw fsError("EINVAL", to);
451
+ const target = await this.locate(destination, false).catch((err) => {
452
+ if (missing(err)) return null;
453
+ throw err;
454
+ });
455
+ const targetStat = target && await this.describe(target);
456
+ if (targetStat?.isDirectory && !sourceStat.isDirectory) throw fsError("EISDIR", to);
457
+ if (targetStat && !targetStat.isDirectory && sourceStat.isDirectory) {
458
+ throw fsError("ENOTDIR", to);
459
+ }
460
+ if (targetStat?.isDirectory && (await this.readdir(destination)).length > 0) {
183
461
  throw fsError("ENOTEMPTY", to);
184
462
  }
185
- const [dir, name] = await this.parent(to);
186
- const handle = await this.handle(from);
187
- const swap = target?.isDirectory === true;
463
+ const now = Date.now();
464
+ const carry = (entry) => ({
465
+ ino: inodeOf(source.path),
466
+ ...entry,
467
+ path: destination,
468
+ ctimeMs: now
469
+ });
470
+ if (source.link) {
471
+ if (target?.handle) await this.rm(destination);
472
+ await this.meta.move(source.path, destination, carry);
473
+ return;
474
+ }
475
+ const dir = parent.handle;
476
+ const name = baseOf(destination);
477
+ const swap = targetStat?.isDirectory === true;
188
478
  const staging = swap ? `.${name}.${crypto.randomUUID()}` : name;
189
- const moved = await this.place(handle, dir, staging);
190
- if (swap) await this.rm(to, { recursive: true });
191
- this.carry(from, to);
192
- if (!moved) await this.rm(from, { recursive: true });
193
- if (!swap) return;
194
- const staged = await dir.getDirectoryHandle(staging);
195
- if (!await this.place(staged, dir, name)) await dir.removeEntry(staging, { recursive: true });
479
+ const timed = await this.explicitTimes(source.path);
480
+ const moved2 = await this.place(source.handle, dir, staging);
481
+ if (swap) await this.rm(destination, { recursive: true });
482
+ await this.meta.move(source.path, destination, carry);
483
+ if (!moved2) await this.rm(source.path, { recursive: true });
484
+ let copied = !moved2;
485
+ if (swap) {
486
+ const staged = await dir.getDirectoryHandle(staging);
487
+ const placed = await this.place(staged, dir, name);
488
+ if (!placed) await dir.removeEntry(staging, { recursive: true });
489
+ copied ||= !placed;
490
+ }
491
+ if (copied) await this.restamp(destination, timed);
492
+ }
493
+ async modified(path) {
494
+ const handle = await this.handle(path);
495
+ return handle.kind === "file" ? (await handle.getFile()).lastModified : 0;
496
+ }
497
+ async explicitTimes(path) {
498
+ const timed = [];
499
+ for (const entry of await this.meta.under(path)) {
500
+ if (entry.mtimeFor === void 0 || entry.link !== void 0) continue;
501
+ const current = await this.modified(entry.path).catch(() => void 0);
502
+ if (current === entry.mtimeFor) timed.push(entry.path.slice(path.length));
503
+ }
504
+ return timed;
505
+ }
506
+ async restamp(path, timed) {
507
+ for (const rest of timed) {
508
+ const mtimeFor = await this.modified(path + rest);
509
+ await this.meta.update(path + rest, (entry) => entry && { ...entry, mtimeFor });
510
+ }
196
511
  }
197
512
  async place(handle, dir, name) {
198
- const moved = await handle.move?.(dir, name).then(
513
+ const moved2 = await handle.move?.(dir, name).then(
199
514
  () => true,
200
515
  () => false
201
516
  );
202
- if (moved) return true;
517
+ if (moved2) return true;
203
518
  try {
204
519
  await this.copy(handle, dir, name);
205
520
  } catch (err) {
@@ -218,20 +533,65 @@ var OpfsFs = class {
218
533
  const target = await dir.getDirectoryHandle(name, { create: true });
219
534
  for await (const child of handle.values()) await this.copy(child, target, child.name);
220
535
  }
221
- async symlink(_target, path) {
222
- throw fsError("ENOSYS", path);
536
+ async symlink(target, path) {
537
+ const parent = await this.locate(parentOf2(path), true);
538
+ if (parent.handle?.kind !== "directory") throw fsError("ENOTDIR", path);
539
+ const at = resolvePath(parent.path, baseOf(path));
540
+ if (await this.locate(at, false).then(
541
+ () => true,
542
+ (err) => !missing(err)
543
+ )) {
544
+ throw fsError("EEXIST", path);
545
+ }
546
+ const now = Date.now();
547
+ await this.meta.update(at, () => ({
548
+ path: at,
549
+ link: target,
550
+ dir: parent.path,
551
+ ino: inodeOf(at),
552
+ mtimeMs: now,
553
+ ctimeMs: now
554
+ }));
223
555
  }
224
556
  async readlink(path) {
225
- await this.handle(path);
557
+ const found = await this.locate(path, false);
558
+ if (found.link) return found.link.link;
226
559
  throw fsError("EINVAL", path);
227
560
  }
228
561
  async chmod(path, mode) {
229
- await this.handle(path);
230
- this.modes.set(path, mode & PERM_MASK);
231
- }
232
- async utimes(path, _atime, mtime) {
233
- await this.handle(path);
234
- this.times.set(path, mtime.getTime());
562
+ const found = await this.locate(path, true);
563
+ const ctimeMs = Date.now();
564
+ await this.meta.update(found.path, (entry) => ({
565
+ ...entry,
566
+ path: found.path,
567
+ mode: mode & PERM_MASK,
568
+ ctimeMs
569
+ }));
570
+ }
571
+ async utimes(path, atime, mtime) {
572
+ const found = await this.locate(path, true);
573
+ const handle = found.handle;
574
+ const file = handle.kind === "file" ? await handle.getFile() : void 0;
575
+ const ctimeMs = Date.now();
576
+ await this.meta.update(found.path, (entry) => ({
577
+ ...entry,
578
+ path: found.path,
579
+ atimeMs: atime.getTime(),
580
+ mtimeMs: mtime.getTime(),
581
+ mtimeFor: file?.lastModified ?? 0,
582
+ ctimeMs
583
+ }));
584
+ }
585
+ async reconcile() {
586
+ let removed = 0;
587
+ for (const entry of await this.meta.all()) {
588
+ const real = await this.real(entry.path);
589
+ const stale = entry.link === void 0 ? !real : real || !await this.real(parentOf2(entry.path));
590
+ if (!stale) continue;
591
+ await this.meta.update(entry.path, () => void 0);
592
+ removed++;
593
+ }
594
+ return removed;
235
595
  }
236
596
  };
237
597
 
@@ -380,19 +740,34 @@ async function scanCommands(fs, modules) {
380
740
  var S_IFDIR2 = 16384;
381
741
  var S_IFREG2 = 32768;
382
742
  var COMMAND = /^\/(?:usr\/)?bin\/([^/]+)$/;
383
- function missing(err) {
743
+ var MAX_LINKS2 = 40;
744
+ function missing2(err) {
384
745
  return err?.code === "ENOENT";
385
746
  }
386
- function synthetic(directory) {
747
+ function synthetic(directory, path) {
748
+ const epoch = /* @__PURE__ */ new Date(0);
387
749
  return {
388
750
  isFile: !directory,
389
751
  isDirectory: directory,
390
752
  isSymbolicLink: false,
391
753
  size: 0,
392
754
  mode: (directory ? S_IFDIR2 : S_IFREG2) | 493,
393
- mtime: /* @__PURE__ */ new Date(0)
755
+ mtime: epoch,
756
+ atime: epoch,
757
+ ctime: epoch,
758
+ ino: inodeOf(path)
394
759
  };
395
760
  }
761
+ async function followLinks(fs, path) {
762
+ let current = path;
763
+ for (let hops = 0; hops < MAX_LINKS2; hops++) {
764
+ const st = await fs.lstat(current).catch(() => void 0);
765
+ if (!st?.isSymbolicLink) return current;
766
+ const parent = current.slice(0, current.lastIndexOf("/")) || "/";
767
+ current = fs.resolvePath(parent, await fs.readlink(current));
768
+ }
769
+ return current;
770
+ }
396
771
  function withCommandDirs(fs, names) {
397
772
  async function children(path) {
398
773
  if (path === "/") return ["bin", "usr"];
@@ -401,15 +776,18 @@ function withCommandDirs(fs, names) {
401
776
  return void 0;
402
777
  }
403
778
  async function virtual(path) {
404
- if (path === "/usr" || path === "/bin" || path === "/usr/bin") return synthetic(true);
779
+ if (path === "/usr" || path === "/bin" || path === "/usr/bin") return synthetic(true, path);
405
780
  const name = COMMAND.exec(path)?.[1];
406
- return name !== void 0 && (await names()).has(name) ? synthetic(false) : void 0;
781
+ return name !== void 0 && (await names()).has(name) ? synthetic(false, path) : void 0;
782
+ }
783
+ async function linked(path) {
784
+ return await virtual(path) ?? virtual(await followLinks(fs, path));
407
785
  }
408
786
  async function stat(path, real) {
409
787
  try {
410
788
  return await real(path);
411
789
  } catch (err) {
412
- const found = missing(err) ? await virtual(path) : void 0;
790
+ const found = missing2(err) ? await linked(path) : void 0;
413
791
  if (found) return found;
414
792
  throw err;
415
793
  }
@@ -421,13 +799,13 @@ function withCommandDirs(fs, names) {
421
799
  try {
422
800
  return await fs.readFileBuffer(path);
423
801
  } catch (err) {
424
- if (missing(err) && (await virtual(path))?.isFile) return new Uint8Array(0);
802
+ if (missing2(err) && (await linked(path))?.isFile) return new Uint8Array(0);
425
803
  throw err;
426
804
  }
427
805
  },
428
806
  writeFile: (path, content) => fs.writeFile(path, content),
429
807
  async exists(path) {
430
- return await fs.exists(path) || await virtual(path) !== void 0;
808
+ return await fs.exists(path) || await linked(path) !== void 0;
431
809
  },
432
810
  stat: (path) => stat(path, (p) => fs.stat(p)),
433
811
  lstat: (path) => stat(path, (p) => fs.lstat(p)),
@@ -435,11 +813,31 @@ function withCommandDirs(fs, names) {
435
813
  const extra = await children(path);
436
814
  if (!extra) return fs.readdir(path);
437
815
  const real = await fs.readdir(path).catch((err) => {
438
- if (missing(err)) return [];
816
+ if (missing2(err)) return [];
439
817
  throw err;
440
818
  });
441
819
  return [.../* @__PURE__ */ new Set([...real, ...extra])].sort();
442
820
  },
821
+ async readdirStat(path) {
822
+ const extra = await children(path);
823
+ if (!extra) return fs.readdirStat(path);
824
+ const real = await fs.readdirStat(path).catch((err) => {
825
+ if (missing2(err)) return [];
826
+ throw err;
827
+ });
828
+ const seen = new Set(real.map(([name]) => name));
829
+ const base = path === "/" ? "" : path;
830
+ const added = extra.filter((name) => !seen.has(name));
831
+ const virtuals = await Promise.all(
832
+ added.map(
833
+ async (name) => [
834
+ name,
835
+ await virtual(`${base}/${name}`)
836
+ ]
837
+ )
838
+ );
839
+ return [...real, ...virtuals].sort(([a], [b]) => a < b ? -1 : 1);
840
+ },
443
841
  mkdir: (path, options) => fs.mkdir(path, options),
444
842
  rm: (path, options) => fs.rm(path, options),
445
843
  rename: (from, to) => fs.rename(from, to),
@@ -1013,21 +1411,14 @@ function statJson(s) {
1013
1411
  isSymbolicLink: s.isSymbolicLink,
1014
1412
  size: s.size,
1015
1413
  mode: s.mode,
1016
- mtimeMs: s.mtime.getTime()
1414
+ mtimeMs: s.mtime.getTime(),
1415
+ atimeMs: s.atime.getTime(),
1416
+ ctimeMs: s.ctime.getTime(),
1417
+ ino: s.ino
1017
1418
  };
1018
1419
  }
1019
1420
  async function readdirStat(fs, dir) {
1020
- const names = await fs.readdir(dir);
1021
- const base = dir === "/" ? "" : dir;
1022
- return Promise.all(
1023
- names.map(async (name) => {
1024
- try {
1025
- return [name, statJson(await fs.lstat(`${base}/${name}`))];
1026
- } catch {
1027
- return [name, null];
1028
- }
1029
- })
1030
- );
1421
+ return (await fs.readdirStat(dir)).map(([name, stat]) => [name, stat && statJson(stat)]);
1031
1422
  }
1032
1423
  var done = { ok: true, kind: "void" };
1033
1424
  function json(value) {
@@ -1168,7 +1559,7 @@ function attachSyncSabResponder(port, sab, token, opts) {
1168
1559
  Atomics.notify(header, SAB_I_STATE);
1169
1560
  if (offset + chunk >= entry.payload.byteLength) drop(id);
1170
1561
  }
1171
- function settle(id, result) {
1562
+ function settle2(id, result) {
1172
1563
  if (disposed) return;
1173
1564
  const { status, payload } = encodeSabResult(result);
1174
1565
  drop(id);
@@ -1187,7 +1578,7 @@ function attachSyncSabResponder(port, sab, token, opts) {
1187
1578
  const entry = pending.get(data.id);
1188
1579
  const offset = data.offset;
1189
1580
  if (!entry || typeof offset !== "number" || !Number.isInteger(offset) || offset < 0 || offset > entry.payload.byteLength) {
1190
- settle(data.id, { ok: false, errno: "EIO", message: "sync-sab: bad continuation" });
1581
+ settle2(data.id, { ok: false, errno: "EIO", message: "sync-sab: bad continuation" });
1191
1582
  return;
1192
1583
  }
1193
1584
  publish(data.id, entry, offset);
@@ -1201,7 +1592,7 @@ function attachSyncSabResponder(port, sab, token, opts) {
1201
1592
  try {
1202
1593
  dispatched = dispatch(req);
1203
1594
  } catch (err) {
1204
- settle(id, {
1595
+ settle2(id, {
1205
1596
  ok: false,
1206
1597
  errno: "EIO",
1207
1598
  message: err instanceof Error ? err.message : String(err)
@@ -1209,8 +1600,8 @@ function attachSyncSabResponder(port, sab, token, opts) {
1209
1600
  return;
1210
1601
  }
1211
1602
  void dispatched.then(
1212
- (result) => settle(id, result),
1213
- (err) => settle(id, {
1603
+ (result) => settle2(id, result),
1604
+ (err) => settle2(id, {
1214
1605
  ok: false,
1215
1606
  errno: "EIO",
1216
1607
  message: err instanceof Error ? err.message : String(err)
@@ -1888,15 +2279,15 @@ var VfsNodes = class {
1888
2279
  }
1889
2280
  renamed(from, to) {
1890
2281
  if (from === to) return;
1891
- const moved = [];
2282
+ const moved2 = [];
1892
2283
  for (const [p, node] of this.byPath) {
1893
- if (within2(p, from)) moved.push(node);
2284
+ if (within2(p, from)) moved2.push(node);
1894
2285
  else if (within2(p, to)) {
1895
2286
  node.orphaned = true;
1896
2287
  this.byPath.delete(p);
1897
2288
  }
1898
2289
  }
1899
- for (const node of moved) {
2290
+ for (const node of moved2) {
1900
2291
  this.byPath.delete(node.path);
1901
2292
  node.path = to + node.path.slice(from.length);
1902
2293
  this.byPath.set(node.path, node);
@@ -2557,8 +2948,8 @@ function spawnWasmProcess(opts) {
2557
2948
  return dispatchSyncFs(req);
2558
2949
  };
2559
2950
  const responder = attachSyncSabResponder(worker, sab, token, { dispatch });
2560
- let settle;
2561
- const exited = new Promise((resolve) => settle = resolve);
2951
+ let settle2;
2952
+ const exited = new Promise((resolve) => settle2 = resolve);
2562
2953
  let done3 = false;
2563
2954
  let endedBy;
2564
2955
  const finish = (code, sig) => {
@@ -2571,8 +2962,8 @@ function spawnWasmProcess(opts) {
2571
2962
  revokeSyncFsToken(token);
2572
2963
  worker.terminate();
2573
2964
  void process.exit().then(
2574
- () => settle(code),
2575
- () => settle(code)
2965
+ () => settle2(code),
2966
+ () => settle2(code)
2576
2967
  );
2577
2968
  };
2578
2969
  const onMessage = (event) => {
@@ -2795,7 +3186,9 @@ var Launcher = class {
2795
3186
  const command = (await this.commands()).get(name);
2796
3187
  return command && targetOf(command);
2797
3188
  }
2798
- const glue = this.fs.resolvePath(cwd, file);
3189
+ const glue = await followLinks(this.base, this.fs.resolvePath(cwd, file));
3190
+ const linked = COMMAND2.exec(glue)?.[1];
3191
+ if (linked !== void 0) return this.resolve(`/bin/${linked}`, argv0, cwd);
2799
3192
  const wasm = modulePath(glue);
2800
3193
  if (!await this.base.exists(glue) || !await this.base.exists(wasm)) return void 0;
2801
3194
  return { glue, wasm, argv0: baseName(argv0 || file) };
@@ -3033,8 +3426,12 @@ function serveKernel(port, deps) {
3033
3426
  async function handle(req) {
3034
3427
  if (req.op === "init") {
3035
3428
  launcher = (req.root ? Promise.resolve(req.root) : deps.storage()).then(async (root) => {
3429
+ const name = req.metadata ?? META_DB;
3430
+ const meta = name === false ? void 0 : await deps.metadata?.(name);
3431
+ const fs = new OpfsFs(root, meta);
3432
+ await fs.reconcile();
3036
3433
  const started = new Launcher({
3037
- fs: new OpfsFs(root),
3434
+ fs,
3038
3435
  createWorker: deps.createWorker,
3039
3436
  ...req.modules ? { modules: req.modules } : {},
3040
3437
  ...req.env ? { env: req.env } : {}
@@ -3071,6 +3468,7 @@ function serveKernel(port, deps) {
3071
3468
  var processWorker = new URL("./process-worker.js", import.meta.url);
3072
3469
  serveKernel(globalThis, {
3073
3470
  storage: () => navigator.storage.getDirectory(),
3074
- createWorker: () => new Worker(processWorker, { type: "module" })
3471
+ createWorker: () => new Worker(processWorker, { type: "module" }),
3472
+ metadata: (name) => openMeta(name, indexedDB)
3075
3473
  });
3076
3474
  //# sourceMappingURL=kernel-worker.js.map