@ai-ecoverse/slicc-kernel 1.54.8 → 1.54.9

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
@@ -429,11 +429,13 @@ A `hostfs` mount is a folder exported by the local proxy ([slicc-node](https://g
429
429
  - **Proxy loss:** when the watch stream ends, or is silent for 45 s, the kernel reconnects once at once. If that fails, the mount is `nomedium`, as with [removable media](#removable-media-fsa): operations fail with `ENOMEDIUM` (`ENODEV` for WASI), and the kernel keeps reconnecting with backoff (1 s to 30 s), so the folder comes back by itself when the proxy does. A mount made while the proxy is down starts as `nomedium`, and so does one whose grant or first watch takes more than 15 s; it comes up by itself when they arrive.
430
430
  - **Consistency:** a file that changes on the host while a program reads it fails with `ESTALE`, and the kernel restarts the whole read (up to 3 times) instead of stitching two versions together. On a ranged mount an open file keeps the version it was opened at (its ETag) until it writes, also when it is handed to a child process: a page fetched after the host changed the file fails the program's read with `ESTALE` ("Stale file handle") instead of mixing versions. A file deleted on the host while a program holds it open fails the next read that goes to the host with `ENOENT`, not with NFS's `ESTALE`.
431
431
  - **Over a port:** the hook may answer `{ port, token?, capabilities? }` instead, for a folder that doesn't come from slicc-node over HTTP: for example, a linked `slicc` CLI's export, which the page reaches over its own link. The page implements a `HostfsTransport` for the folder (`capabilities`, `token`, `maxIo`, `call(op, body)`, `read(fh, offset, size, ifMatch?)`, `write(fh, offset, bytes)`, `watch(signal, onEvent, onOpen)`, with slicc-node's ops and answers) and serves it with `serveHostfsPort(port, transport)`, exported from both entries, which returns a function that stops serving and closes the port.
432
+ - **Slow links:** each read or write message carries up to `maxIo` bytes and has to finish within the 20 s call bound, so a page that serves a folder over a slow or remote link sets `capabilities.maxIo` to fit it: 4 MiB over 1 Mbit/s takes about 32 s and would fail every time, while 512 KiB takes about 4 s.
432
433
  - The kernel moves the port into its worker with the grant. Calls, reads and writes are request and reply messages there, and the watch is a stream of messages that the kernel ends by aborting.
433
434
  - Errors keep their `code`. A transport that throws `ENOTCONN` (its link is down) makes the mount `nomedium` and reconnecting, as a lost proxy does: the kernel asks the hook again, and the page answers with a new port.
434
- - A port that dies doesn't hang anyone: pending requests fail with `ENOTCONN` when the port closes (its `close` event), when its watch ends or goes silent, and, on engines that don't report a closed peer, when a request gets no answer within 60 s (plus 1 s per MiB of a read or write), which fails it with `ETIMEDOUT`. After any of these the port counts as gone and is closed, files opened through it are `ESTALE`, and the next operation reconnects. `serveHostfsPort` stops serving when its port closes.
435
+ - A port that dies doesn't hang anyone. Chrome reports no `close` when the peer of a `MessagePort` goes away, so while anything is outstanding (the watch always is), the kernel pings the port every second and `serveHostfsPort` answers at once. No answer within 10 s makes the port gone (a page busy for a few seconds, with a dialog open or paused in devtools, is still alive): pending requests fail with `ENOTCONN`, the mount is `nomedium`, and the kernel asks the hook for a new port. A port whose server never answered a ping (an older `serveHostfsPort`) isn't judged by pings. Pending requests also fail when the port's `close` event fires (Node has it), when its watch ends or goes silent, and, as a last resort, after 60 s without an answer (plus 1 s per MiB of a read or write), with `ETIMEDOUT`. A gone port is closed, and `serveHostfsPort` stops serving when its port closes. A watch started with an already aborted signal also counts the port as gone.
436
+ - **Hanging folders:** every request to a `hostfs` folder, over a port or over HTTP, gets 20 s: each call, and each read or write of up to `maxIo` bytes. The mount tells the kernel the folder's own `maxIo` (at most 16 MiB), so one file operation never puts more than that on the folder at once. One that takes longer fails with `ENOTCONN`, so the mount goes `nomedium` and reconnects by itself, instead of the driver watchdog (30 s) marking the mount `failed` until a remount. The timeout also retires that connection: its requests in flight are aborted (HTTP) or failed (a port), nothing more is sent on it, its open files are `ESTALE`, and a file whose open answers late is released on the folder. Over a port, the kernel keeps the port for another second for cleanup (a temporary write permission it added to open a read-only file is put back), then tells `serveHostfsPort` it is going (`bye`, which Chrome can't signal by itself). The server then releases every file it opened through that port, including one whose open lands later. A request the host already received may still finish there: an operation that timed out may have happened, so check before retrying it (a retried `mkdir` or create can fail with `EEXIST`, a retried `rename` or `unlink` with `ENOENT`). The kernel itself retries only reads and whole-file rewrites after `ESTALE`, never an exclusive create.
435
437
  - A read reply sends a copy of the transport's bytes, so a transport may return a view into storage it keeps (a cache, a pooled or shared buffer).
436
- - `token` names the connection (a link id, say). When the next port's token differs, files that were open become `ESTALE`. Without a token, every port is a new connection.
438
+ - `token` names the connection (a link id, say); without one, every port gets its own name. It doesn't decide whether open files survive a reconnect: a file a program holds open is opened again by path on the new connection, with the ETag it had (`ifMatch`). It goes on if the file is unchanged on the host, and fails with `ESTALE` if the host changed it meanwhile. Files opened through a port that is gone are `ESTALE` until they are opened again.
437
439
  - **Node:** a page's requests carry its `Origin`, and slicc-node grants a token to that origin only. Node's `fetch` sends none, so a Node embedder passes `createNodeKernel` the origin its `hostfs` hook asked for the grant with, as `hostfsOrigin`, and the kernel adds it to every hostfs request. Without it the mount stays `nomedium`. `hostfsFetch(url, init)` replaces the `fetch` those requests use (for an agent or a test proxy), and gets the `Origin` too when both are given.
438
440
 
439
441
  ### Mounting from a program
package/dist/index.js CHANGED
@@ -1646,11 +1646,38 @@ var errorOf = (err) => ({
1646
1646
  });
1647
1647
  function serveHostfsPort(port, transport) {
1648
1648
  const watches = /* @__PURE__ */ new Map();
1649
+ const handles = /* @__PURE__ */ new Set();
1650
+ let stopped = false;
1651
+ const closeHandles = () => {
1652
+ for (const fh of handles) void transport.call("release", { fh }, true).catch(() => {
1653
+ });
1654
+ handles.clear();
1655
+ };
1649
1656
  const send2 = (reply, transfer = []) => port.postMessage(reply, transfer);
1657
+ const pending = /* @__PURE__ */ new Set();
1658
+ const track = (work) => {
1659
+ pending.add(work);
1660
+ const done = () => pending.delete(work);
1661
+ work.then(done, done);
1662
+ return work;
1663
+ };
1664
+ const answer = async (name, body) => {
1665
+ const result = await track(transport.call(name, body));
1666
+ const opened = result?.fh;
1667
+ if (name === "open" && typeof opened === "number") handles.add(opened);
1668
+ if (name === "release") handles.delete(body?.fh);
1669
+ if (stopped) closeHandles();
1670
+ return result;
1671
+ };
1650
1672
  const listener = async ({
1651
1673
  data
1652
1674
  }) => {
1653
1675
  const { id, op } = data;
1676
+ if (op === "ping") return send2({ id, pong: true });
1677
+ if (op === "bye") return stop();
1678
+ if (op === "cleanup")
1679
+ return void Promise.allSettled([...pending]).then(() => answer(data.name, data.body)).catch(() => {
1680
+ });
1654
1681
  if (op === "unwatch") return watches.get(id)?.abort();
1655
1682
  if (op === "watch") {
1656
1683
  const stop2 = new AbortController();
@@ -1671,32 +1698,35 @@ function serveHostfsPort(port, transport) {
1671
1698
  }
1672
1699
  try {
1673
1700
  if (op === "read") {
1674
- const read = await transport.read(
1675
- data.fh,
1676
- data.offset,
1677
- data.size,
1678
- data.ifMatch
1701
+ const read = await track(
1702
+ transport.read(
1703
+ data.fh,
1704
+ data.offset,
1705
+ data.size,
1706
+ data.ifMatch
1707
+ )
1679
1708
  );
1680
1709
  const bytes = read.slice();
1681
1710
  return send2({ id, bytes }, [bytes.buffer]);
1682
1711
  }
1683
1712
  if (op === "write") {
1684
- await transport.write(data.fh, data.offset, data.bytes);
1713
+ await track(
1714
+ transport.write(data.fh, data.offset, data.bytes)
1715
+ );
1685
1716
  return send2({ id });
1686
1717
  }
1687
1718
  if (op !== "call") throw fsError("EINVAL", `unknown hostfs port op ${op}`);
1688
- send2({
1689
- id,
1690
- result: await transport.call(data.name, data.body)
1691
- });
1719
+ send2({ id, result: await answer(data.name, data.body) });
1692
1720
  } catch (err) {
1693
1721
  send2({ id, error: errorOf(err) });
1694
1722
  }
1695
1723
  };
1696
1724
  const stop = () => {
1725
+ stopped = true;
1697
1726
  port.removeEventListener("message", listener);
1698
1727
  port.removeEventListener("close", stop);
1699
1728
  for (const watch of watches.values()) watch.abort();
1729
+ closeHandles();
1700
1730
  port.close();
1701
1731
  };
1702
1732
  port.addEventListener("message", listener);