@ai-ecoverse/slicc-kernel 1.133.3 → 1.133.4

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 CHANGED
@@ -709,7 +709,7 @@ The proxy answers `200` with `Content-Type: application/vnd.slicc.raw-fetch`: a
709
709
 
710
710
  `/` is the OPFS root. Each process mounts the top-level directories that exist when it starts (plus `/usr` and `/bin`), so files in them are shared by all processes and visible through the OPFS API as soon as the process that wrote them has closed them; `run` resolves only after that. `createKernel` creates `/tmp` and `/home` in OPFS, so they are shared too. Files directly in `/`, and directories created there after a process started, are OPFS as well, the same for Emscripten and WASI programs; only `/dev` and `/proc` live in each process's memory. A program's `mkdir` (WASI `path_create_directory`, Emscripten's `mkdir`) is exclusive, as on Linux: it fails with `EEXIST` when the name exists, and of several processes or threads making one name at once exactly one succeeds, so a directory works as a lock. Under a missing directory it fails with `ENOENT`, as on Linux (`mkdir -p` and `os.makedirs` make the parents in the program); a JS program's `ctx.fs.mkdir` makes parents itself. The kernel makes the system `PNPM_HOME` (`/usr/local/share/pnpm`, or the embedder's) when it starts, since pnpm makes only its `bin` directory there. A full origin storage (`QuotaExceededError`, which also comes early in a browser's private or off-the-record contexts, whatever `navigator.storage.estimate()` says) is `ENOSPC`, never retried. A read that meets `NotFoundError` while a writer outside the kernel swaps the file in with `createWritable` (and a `stat`, `lstat` or `utimes` that meets it) looks the name up again, up to three times, before it fails ([#1075](https://github.com/ai-ecoverse/slicc-kernel/issues/1075)), so a program whose package is overwritten in place keeps starting. An OPFS or IndexedDB failure that has no POSIX counterpart is `EIO`; its message names the DOM error and the operation (`lookup`, `read`, `write`, `commit` or `sidecar`), and the kernel worker logs the first of each kind to its console. File contents are buffered per open file and written back on close, `fsync` and exit, so a refused write (`ENOSPC`, `EIO`) surfaces at that close or `fsync`, and a program that ignores close errors doesn't see it (`echo x > f` can exit 0 with `f` empty, as with any buffered write-back). A write-back error a close has already reported to a WASI program isn't raised again when it exits, so the program keeps its own exit status. Metadata operations (`mkdir`, `rename`, `rm`, …) go straight to OPFS. The kernel worker is the only writer.
711
711
 
712
- **Renaming a directory on the OPFS root** ([#1122](https://github.com/ai-ecoverse/slicc-kernel/issues/1122)): Chrome's OPFS can move a file but not a directory. So the kernel renames a directory by creating the new tree and moving each file into it (copying a file only if it can't move, e.g. while it's open elsewhere), then removing the old tree. Within the kernel the rename is atomic to readers: a lookup or listing under the old or the new name, or of either parent, waits until the whole tree has moved. So a stage-then-rename updater (pnpm's, an atomic config write) is never seen half there. Another tab sharing the root reads OPFS directly and can see the tree mid-move. A root whose directories can move (the Node memory root) renames them in one step. **If the page reloads or crashes mid-rename**, some files are already in the new tree and the rest are still in the old one. So before the first move, the kernel writes the rename's intent (`{ from, to }`) to its metadata store, and clears it after the last. At the next boot, when no other tab shares the root (the same rule as for the boot's other repair of stale metadata), the kernel finishes every recorded rename: it moves what's left into the new tree, removes the old name, and clears the record. While another tab shares the root, the split tree stays as it is (both names visible, each with part of the files) until a boot that's alone.
712
+ **Renaming a directory on the OPFS root** ([#1122](https://github.com/ai-ecoverse/slicc-kernel/issues/1122)): Chrome's OPFS can move a file but not a directory. So the kernel renames a directory by creating the new tree and moving each file into it (copying a file only if it can't move, e.g. while it's open elsewhere), then removing the old tree. Within the kernel the rename is atomic to readers: a lookup or listing under the old or the new name, or of either parent, waits until the whole tree has moved. So a stage-then-rename updater (pnpm's, an atomic config write) is never seen half there. Another tab sharing the root reads OPFS directly and can see the tree mid-move. A root whose directories can move (the Node memory root) renames them in one step. **If the page reloads or crashes mid-rename**, some files are already in the new tree and the rest are still in the old one. So before the first move, the kernel writes the rename's intent (`{ from, to }`) to its metadata store, and clears it after the last. At the next boot, when no other tab shares the root (the same rule as for the boot's other repair of stale metadata), the kernel finishes every recorded rename: it moves what's left into the new tree, removes the old name, and clears the record. The moved directory and its files keep their owner, mode and inode, as after a rename that wasn't stopped. While another tab shares the root, a kernel finishes only renames whose kernel is gone ([#1141](https://github.com/ai-ecoverse/slicc-kernel/issues/1141)): the renaming kernel holds a Web Lock (`slicc-rename:<id>`, named in the record) for the whole rename, and the browser releases it when that tab closes or its worker ends. A kernel that boots beside a peer, and every kernel that sees a peer go, finishes each recorded rename whose lock is free, taking that lock while it does, so two survivors never replay the same rename and a live kernel's rename is never touched. A record without a lock (written by an older kernel, or where Web Locks are missing) waits for a boot that's alone, as before. While another tab finishes a rename, this kernel's readers wait as for its own; the other tabs can see the tree mid-move, as during any rename.
713
713
 
714
714
  A lookup follows at most 40 symlinks, as Linux does: the 41st is `ELOOP`, for WASI programs, JS programs and the kernel API alike, with or without `..` in a link's target. Emscripten programs resolve paths in their glue, which stops one link earlier, at 40.
715
715
 
@@ -900,6 +900,7 @@ var RemoteWatch = class {
900
900
  settled = false;
901
901
  quiet;
902
902
  crowd;
903
+ gone;
903
904
  told;
904
905
  counted = 0;
905
906
  recount() {
@@ -989,6 +990,7 @@ var RemoteWatch = class {
989
990
  this.peers.delete(peer);
990
991
  this.rebuild();
991
992
  this.recount();
993
+ this.gone?.();
992
994
  for (const wait2 of this.waiting.values()) {
993
995
  if (wait2.left.delete(peer) && wait2.left.size === 0) wait2.done();
994
996
  }
@@ -1024,14 +1026,16 @@ var RemoteWatch = class {
1024
1026
  async prune() {
1025
1027
  if (!this.locks) return;
1026
1028
  const held = new Set(((await this.locks.query()).held ?? []).map((lock) => lock.name));
1029
+ let left = false;
1027
1030
  for (const tab of /* @__PURE__ */ new Set([...this.others.keys(), ...this.peers])) {
1028
1031
  if (!held.has(this.lockOf(tab))) {
1032
+ if (this.peers.delete(tab)) left = true;
1029
1033
  this.others.delete(tab);
1030
- this.peers.delete(tab);
1031
1034
  }
1032
1035
  }
1033
1036
  this.rebuild();
1034
1037
  this.recount();
1038
+ if (left) this.gone?.();
1035
1039
  }
1036
1040
  watched() {
1037
1041
  return this.union.size > 0;
@@ -1121,6 +1125,7 @@ var REPLACED_ATTEMPTS = 4;
1121
1125
  var RECONCILE_LISTINGS = 8;
1122
1126
  var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
1123
1127
  var RENAMES = "/.slicc-renames";
1128
+ var RENAME_LOCK = "slicc-rename:";
1124
1129
  function movable(handle) {
1125
1130
  return typeof handle.move === "function";
1126
1131
  }
@@ -1287,8 +1292,11 @@ var OpfsFs = class {
1287
1292
  this.lockPrefix = `slicc-excl:${channel ?? ""}:`;
1288
1293
  this.rootName = channel;
1289
1294
  this.channel = channel === void 0 ? void 0 : new BroadcastChannel(channel);
1290
- this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch`);
1291
- if (this.remote) this.remote.quiet = () => this.quiet().then(() => this.settle?.());
1295
+ this.remote = channel === void 0 ? void 0 : new RemoteWatch(`${channel}:watch`, { locks: this.locks ?? null });
1296
+ if (this.remote) {
1297
+ this.remote.quiet = () => this.quiet().then(() => this.settle?.());
1298
+ this.remote.gone = () => void this.orphans();
1299
+ }
1292
1300
  if (this.channel) {
1293
1301
  const remote = this.remote;
1294
1302
  meta.listen?.((c) => {
@@ -2361,14 +2369,17 @@ var OpfsFs = class {
2361
2369
  const dir = parent.handle;
2362
2370
  const name2 = baseOf(destination);
2363
2371
  const swap = targetStat?.isDirectory === true;
2372
+ let lock;
2364
2373
  const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
2365
2374
  const copying = sourceStat.isDirectory && !movable(source.handle);
2366
2375
  const staged = resolvePath(parent.path, staging);
2367
2376
  const place = async () => {
2368
2377
  this.forget(destination);
2369
2378
  const timed2 = await this.explicitTimes(source.path);
2370
- const first = copying ? await this.intend({ from: source.path, to: staged }) : void 0;
2371
- const second = copying && swap ? await this.intend({ from: staged, to: destination, replace: true }) : void 0;
2379
+ const root = { ino: inodeOf(source.path), ...moving };
2380
+ const owned4 = lock ? { lock } : {};
2381
+ const first = copying ? await this.intend({ from: source.path, to: staged, root, ...owned4 }) : void 0;
2382
+ const second = copying && swap ? await this.intend({ from: staged, to: destination, replace: true, root, ...owned4 }) : void 0;
2372
2383
  const moved2 = await this.replacingTarget(
2373
2384
  targetStat !== null,
2374
2385
  () => this.place(source.handle, dir, staging)
@@ -2377,7 +2388,7 @@ var OpfsFs = class {
2377
2388
  throw err;
2378
2389
  });
2379
2390
  if (swap) await this.rm(destination, { recursive: true });
2380
- if (second) await this.intend({ from: staged, to: destination }, second);
2391
+ if (second) await this.intend({ from: staged, to: destination, root, ...owned4 }, second);
2381
2392
  await this.meta.move(source.path, destination, carry, left);
2382
2393
  this.movePending(source.path, destination);
2383
2394
  if (!moved2) {
@@ -2402,7 +2413,11 @@ var OpfsFs = class {
2402
2413
  const entry = { from: source.path, to: destination, done: promise };
2403
2414
  this.renamingDirs.add(entry);
2404
2415
  try {
2405
- await place();
2416
+ const locks = this.locks;
2417
+ if (locks) {
2418
+ lock = `${RENAME_LOCK}${crypto.randomUUID()}`;
2419
+ await locks.request(lock, place);
2420
+ } else await place();
2406
2421
  } finally {
2407
2422
  this.renamingDirs.delete(entry);
2408
2423
  resolve2();
@@ -2434,12 +2449,40 @@ var OpfsFs = class {
2434
2449
  } catch {
2435
2450
  }
2436
2451
  }
2437
- async finishRenames() {
2438
- const intents = (await this.meta.under(RENAMES)).filter((entry) => entry.rename !== void 0).sort(
2452
+ replaying = Promise.resolve();
2453
+ orphans() {
2454
+ this.replaying = this.replaying.then(() => this.finishRenames(true)).catch(() => 0);
2455
+ return this.replaying;
2456
+ }
2457
+ async finishRenames(orphaned = false) {
2458
+ const locks = this.locks;
2459
+ const held = /* @__PURE__ */ new Set();
2460
+ if (orphaned && locks) {
2461
+ const { held: h = [], pending = [] } = await locks.query();
2462
+ for (const lock of [...h, ...pending]) held.add(lock.name);
2463
+ }
2464
+ const intents = (await this.meta.under(RENAMES)).filter(
2465
+ (entry) => entry.rename !== void 0 && (!orphaned || locks !== void 0 && entry.rename.lock !== void 0 && !held.has(entry.rename.lock))
2466
+ ).sort(
2439
2467
  (a, b) => a.rename.at - b.rename.at
2440
2468
  );
2441
2469
  for (const entry of intents) {
2442
- const { from, to, replace } = entry.rename;
2470
+ const lock = entry.rename.lock;
2471
+ if (!orphaned || !locks || !lock) await this.finishRename(entry);
2472
+ else await locks.request(lock, () => this.finishRename(entry));
2473
+ }
2474
+ return intents.length;
2475
+ }
2476
+ async finishRename(entry) {
2477
+ const [current] = await this.meta.under(entry.path);
2478
+ if (current?.rename === void 0) return;
2479
+ const { from, to, replace, root } = current.rename;
2480
+ const { promise, resolve: resolve2 } = Promise.withResolvers();
2481
+ const barrier = { from, to, done: promise };
2482
+ this.renamingDirs.add(barrier);
2483
+ this.forget(from);
2484
+ this.forget(to);
2485
+ try {
2443
2486
  const left = await this.real(from);
2444
2487
  if (left && replace && await this.real(to)) await this.rm(to, { recursive: true });
2445
2488
  if (left) {
@@ -2456,11 +2499,23 @@ var OpfsFs = class {
2456
2499
  this.forget(to);
2457
2500
  }
2458
2501
  if ((await this.meta.under(from)).length > 0) {
2459
- await this.meta.move(from, to, (moved2) => ({ ...occupied(moved2), path: to }));
2502
+ await this.meta.move(
2503
+ from,
2504
+ to,
2505
+ (moved2) => ({
2506
+ ...occupied(moved2),
2507
+ ...root,
2508
+ ino: occupied(moved2).ino ?? root?.ino ?? inodeOf(from),
2509
+ path: to
2510
+ }),
2511
+ void 0
2512
+ );
2460
2513
  }
2461
2514
  await this.meta.update(entry.path, () => void 0);
2515
+ } finally {
2516
+ this.renamingDirs.delete(barrier);
2517
+ resolve2();
2462
2518
  }
2463
- return intents.length;
2464
2519
  }
2465
2520
  async settledRename(path) {
2466
2521
  if (this.renamingDirs.size === 0 && this.copying.size === 0 || typeof path !== "string")
@@ -2744,7 +2799,10 @@ var OpfsFs = class {
2744
2799
  };
2745
2800
  }
2746
2801
  async reconcile() {
2747
- if (this.remote && await this.remote.peered()) return 0;
2802
+ if (this.remote && await this.remote.peered()) {
2803
+ await this.orphans();
2804
+ return 0;
2805
+ }
2748
2806
  await this.finishRenames();
2749
2807
  let removed = 0;
2750
2808
  const entries = await this.meta.all();
@@ -7034,7 +7092,7 @@ function satisfies(version, range2) {
7034
7092
  }
7035
7093
 
7036
7094
  // src/version.ts
7037
- var KERNEL_VERSION = true ? "1.133.3" : "0.0.0-dev";
7095
+ var KERNEL_VERSION = true ? "1.133.4" : "0.0.0-dev";
7038
7096
  var OS_RELEASE = "6.0.0-slicc";
7039
7097
  function procVersion() {
7040
7098
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC