@ai-ecoverse/slicc-kernel 1.110.1 → 1.110.3

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
@@ -865,7 +865,7 @@ The kernel writes `/etc/passwd` and `/etc/group` when it starts (see [Users](#us
865
865
 
866
866
  OPFS stores names, bytes, sizes and modification times, nothing else. Everything POSIX needs on top of that lives in an IndexedDB sidecar, so it survives reloads and new kernels:
867
867
 
868
- - mode bits (`chmod`);
868
+ - mode bits (`chmod`; a `chmod` to the mode a file or directory already has writes nothing and leaves its ctime as it was, which saves a sidecar write for every installer that sets the mode a file was made with);
869
869
  - access and change times, and a modification time set explicitly with `utime` (it holds until the file is written again: any write or truncate moves it to the time of the write, also in Chrome, whose OPFS doesn't move a file's `lastModified` for writes through a sync access handle, so the kernel records the write's time itself, and it reaches the metadata store up to 100 ms after the write, so a tab closed inside that window keeps the old mtime on disk; the time follows a rename and is dropped with the file); a directory's times move whenever an entry is created, removed or renamed in it, as POSIX asks, and a time set explicitly after such a change keeps winning, also when the change was made in another kernel and its write lands later; an Emscripten program reads a directory's times again after any call that can wait on another process (a pipe or socket read, `wait`, `poll`, a sleep), so a program that waits for a child or a peer to fill a directory sees the new times (rsync's final directory times rely on it), while a walk without waits reads each directory once; `chown`/`fchown` leave the times alone (Emscripten stamps the setattr of a chown with the current time, which isn't taken as an mtime);
870
870
  - inode numbers that stay stable across renames;
871
871
  - symbolic links (`symlink`, `readlink`, `lstat`, and following them in paths), which exist only in the sidecar, with their own times: `touch -h`, `lutimes` and `utimensat` with `AT_SYMLINK_NOFOLLOW` set the link's, never the target's. A link's own mode can't be changed, as on Linux: `chmod -h`, `lchmod` and `fchmodat` with `AT_SYMLINK_NOFOLLOW` fail with `EOPNOTSUPP` (which `chmod -h` and `tar` ignore for links) and never follow it, and `open` with `O_PATH | O_NOFOLLOW` gives a descriptor for the link itself.
@@ -927,17 +927,22 @@ The Biome, TypeScript, lefthook, Renovate and CI configuration comes from [slicc
927
927
 
928
928
  `tools/big-file.mjs [bytes]` is a manual check, not part of any suite. It writes a file of that size (64 MiB by default) through a tmpfs mount with the unit-test launcher, reads it back, and prints the peak RSS and its ratio to the file size. A 256 MiB file peaks at about 1.8 times its size. Before ranged I/O (#127), a 1.1 GiB file peaked at 18 GB of RSS. **Do not run it past 1 GiB on a workstation.** It refuses anything over 256 MiB unless `SLICC_BIG_FILE_I_HAVE_THE_RAM=1` is set. Measure a small file and extrapolate instead. The unit suite checks a 32 MiB file the same way, and allows a fixed 192 MiB for garbage V8 has yet to collect, which dominates at that size.
929
929
 
930
- Releases are cut by semantic-release on every push to `main`, in two jobs, and only on `main`. The first job runs on the bench pool, so it starts within a minute of the push, before `main` has moved on. It computes the version, tags the commit and creates the GitHub release, with `npmPublish: false`. A run that starts after `main` has moved skips; the run for the newer commit releases it. The second job runs on a GitHub-hosted runner, because npm's trusted publishing and provenance accept no self-hosted runner. It publishes the tagged commit with `tools/release-recover.mjs`. However long it waits for a runner, it publishes exactly the commit the run started on, so the provenance, the tag and the package's `gitHead` agree. For any version tagged at HEAD, `tools/release-recover.mjs` pushes the tag if origin lacks it, publishes the version if npm lacks it (with retries) and it is newer than npm's `latest` (an older one stays unpublished, so `latest` never moves back to it, and an `npm view` error fails the job instead of guessing; #837), and creates its GitHub release if that is missing too. So a failure after the version is tagged (a rejected push, a failed `npm publish`) doesn't strand it, and re-running the Release workflow completes it the same way.
930
+ Releases are cut by semantic-release on every push to `main`, in two jobs, and only on `main`. The first job is light work and runs on the light runner (`CERT_LIGHT_RUNNER`), so it starts within seconds of the push, before `main` has moved on. It computes the version, tags the commit and creates the GitHub release, with `npmPublish: false`. A run that starts after `main` has moved skips; the run for the newer commit releases it. The second job runs on a GitHub-hosted runner, because npm's trusted publishing and provenance accept no self-hosted runner. It publishes the tagged commit with `tools/release-recover.mjs`. However long it waits for a runner, it publishes exactly the commit the run started on, so the provenance, the tag and the package's `gitHead` agree. For any version tagged at HEAD, `tools/release-recover.mjs` pushes the tag if origin lacks it, publishes the version if npm lacks it (with retries) and it is newer than npm's `latest` (an older one stays unpublished, so `latest` never moves back to it, and an `npm view` error fails the job instead of guessing; #837), and creates its GitHub release if that is missing too. So a failure after the version is tagged (a rejected push, a failed `npm publish`) doesn't strand it, and re-running the Release workflow completes it the same way.
931
931
 
932
932
  ### Certification (`cert-review`)
933
933
 
934
- `.github/workflows/cert-review.yml` certifies every release from CI. It runs when the Release workflow succeeds on `main`. The version is the tag on the released commit. The delta is read from the newest earlier release whose `cert-review` status is `success` or `pending`, skipping failed and unreviewed ones (or `base.json`'s release, or the tag before), so a release whose review was cancelled or failed is covered by the next one, and the line says `covers <first>–<last>`. It also runs on `workflow_dispatch` with a `version`, optionally with `prev` and `base`, or with `planted` (a branch) and `pr` for proofs. A dispatch off `main`, or a planted one, is a dry run. Each release (its commit) has its own concurrency group, so no pending review is cancelled by a newer one. The tools are in `tools/cert-review/`:
934
+ Every change is certified in its pull request, before it merges (`cert-review-pr.yml`, next section), and `cert-review` is a required check of the merge queue, so every commit on `main`, and every release built from it, was certified first. A release is then **verified on npm** (`release / verify on npm` in `release.yml`, `tools/cert-review/verify.mjs`), in code, for every tag the run made:
935
+
936
+ - It waits up to an hour for npm to serve the version (an accepted publish can take a while to appear), then downloads the tarball and checks `dist.integrity` (sha512), the SLSA v1 provenance (the tarball's sha512 is its subject; `release.yml` on `refs/heads/main`, a GitHub-hosted builder; `gitCommit` = `gitHead` = the tag, on `main`), and `npm audit signatures`. A version that `release-recover.mjs` skipped because npm's `latest` is newer (#837) has nothing to verify.
937
+ - It then writes `certified @ai-ecoverse/slicc-kernel@<v> sha256=<64 hex> (covers <first>–<v>; ci: <run>; pr-certified: <each merged pull request's cert-review>; known: <open known-issue issues>)` into the GitHub release's notes, between `<!-- cert-verify -->` markers. homescoop's ci reads exactly that prefix and re-hashes the npm tarball against it. `covers` starts after the last version npm serves, so a skipped version is covered by the next. Notes that already carry the identical line stay unchanged.
938
+ - A mismatch writes no line. The job is red with a `supply-chain alert`, and so is a line already in the notes with another sha256.
939
+
940
+ The tools in `tools/cert-review/`:
935
941
 
936
- - **plan:** works out the version, the previous tag, the commit and the base (`base.mjs`). The base is the newest earlier release whose `cert-review` commit status is `success`, or, until there is one, the release `base.json` names (the cert lane's last line). CI never reads the npm `certified` dist-tag.
937
942
  - **delta** (`delta.mjs`): lists the pull requests merged between the two tags (their merge commits, plus `pr` for a planted branch). Each gets its conventional-commit kind (`fix`, `feat`, `perf` and `revert` are behaviour changes) and the integration test files and titles it adds or changes. The titles are read from the pull request's patch: the added `test(…)` lines and the hunk headers git puts above changed lines. Escapes are decoded as JavaScript does. A title git cut short matches as a prefix, and each `${…}` in a template-literal title matches anything there, so `` `a forked child of a ${kind} WASIX program…` `` matches the rows it ran as. A title made only of placeholders is skipped.
938
943
  - **suite** (`suite.mjs`, `reporter.mjs`) runs on the bench pool, without secrets. It takes the tag's tree with its own dependencies and Chromium, and replaces `dist/` with the **published npm tarball** (for a planted branch, its own `npm pack`). The test command runs in a session of its own (`detached`), and `suite.mjs` notes a `SIGINT` (`ignored` in its summary) instead of dying of it. That way a stray signal to the step's process group can't cut the run short, while a `SIGTERM` (a real cancel) still ends it. The log is written as the run goes, and a `SIGTERM` still writes the summary with the results so far (`exit: 'signal SIGTERM'`). A run that left only its `.jsonl` (the runner was lost) is read from that, as `exit: 'no summary'`. The NEGATIVE step runs even when the release suite's step failed. Before it reports, `suite.mjs` re-runs once just the test files that left no result (no rows, a cancelled row, or a file-level failure, such as a lost process), with a second `node --test` over those files, and merges their results (`retried`, `firstExit`, `unfinished` in the summary). That holds for the release suite and for each NEGATIVE run. A fix whose test passes on its baseline stays `no-negative`.
939
- - The release job runs the release's whole `test/integration/` suite, then each fix's test files again on the dist of **the newest published release before the fix was merged** (the NEGATIVE). `delta.mjs` groups the files by that baseline: for most fixes it is the previous release, but a fix for a bug that shipped inside the delta (a regression in v1.99.1 fixed for v1.99.2) is checked against the release that had the bug.
940
- - The base job runs the base release's own suite on its own published dist.
944
+ - The candidate job runs the pull request's whole `test/integration/` suite on its packed build.
945
+ - The base job runs `main`'s own suite on `main`'s packed build, then the NEGATIVE (#861): `main`'s tree, with all of the pull request's `test/**` changes applied (test files, helpers and fixtures), on `main`'s build. So a fix in `tools/` fails there just as a fix in `src/` does. A test file that fails to load on `main` (a module or export that only the pull request adds) leaves no result for its titles, which is `prev-not-run`, never a NEGATIVE.
941
946
  - Each run uses the tree's own `npm test` command, with a reporter that records every test and subtest with its status and error.
942
947
  - **kernel-compat** (`compat.mjs`) supplies the base-set and catalog evidence: homescoop's kernel-compat run for the release.
943
948
  - Its reference kernel is the certified base, not slicc-bios's pin. cert-review passes `--reference <base>`, and `kernel-compat.yml` takes a `reference` input that it hands to homescoop's workflow (homescoop#504), recorded in compat.json. So it uses a dispatched run named `Kernel compat v<version> ref <base>` (kernel-compat has no run of its own after Release). Its `candidate` job checks that npm serves the version, or takes a packed tarball from another run (`run`, `artifact`, `sha256`, for PR builds `x.y.z-pr.N.M`), and prints `@ai-ecoverse/slicc-kernel@<version>`, which compat.mjs matches. It dispatches one when none has finished and waits up to 150 minutes; package reruns carry the same reference.
@@ -948,24 +953,23 @@ Releases are cut by semantic-release on every push to `main`, in two jobs, and o
948
953
  - `negative` (a fix's test fails on the previous dist and passes on the release) or `passes` (a feature's test passes);
949
954
  - otherwise a finding: `no-test`, `no-negative`, `fails-on-release`, `not-run`, `prev-not-run` or `no-titles`.
950
955
 
951
- It also diffs the two suites test by test (`regressed`, `newFailing`, `fixed`, `stillFailing`, `removed`), flags a suite run that didn't finish (`suite-incomplete`), and adds the kernel-compat findings. A release suite that didn't finish is a `missing-evidence` finding that always blocks `certified`, with the remedy `re-run the failed jobs`. That covers a signal, a timeout, a cut-off run, no results, or any test file without a result. Its base tests that didn't run are `unrun`, never `removed`. INCOMPLETE is not FAIL: record writes `cert-ci: INCOMPLETE <name>@<version> (<n> files without results: …; re-run the failed jobs)`, and publish posts the `cert-review` status as `error`, which means re-run, where `failure` means a real defect that rolls forward. A defect found in code (supply chain, a compat crash) still FAILs. base and prev skip `error` releases, as they skip `failure`. A base suite that is missing or didn't finish is a `missing-evidence` finding. It blocks `certified` in code (record FAILs it) unless the release suite is complete and all-pass and the delta removes or renames no test file and drops no test title (`dropped` per row). Either way the line ends with `base suite incomplete (<n> results)`. Claude, on Bedrock with the key only in that step, then reads those artifacts and `instructions.txt` (which has the roll-forward FAIL list verbatim) and writes `verdict.json` and `comment.md`. Its tools are reading files, `gh pr view`, and `gh issue list`/`view`. `gh issue create` is allowed only for a release on `main`; otherwise, and for an issue in another repository, it lists the issue under `elsewhere` for the cert lane to file.
956
+ It also diffs the two suites test by test (`regressed`, `newFailing`, `fixed`, `stillFailing`, `removed`), flags a suite run that didn't finish (`suite-incomplete`), and adds the kernel-compat findings. A release suite that didn't finish is a `missing-evidence` finding that always blocks `certified`, with the remedy `re-run the failed jobs`. That covers a signal, a timeout, a cut-off run, no results, or any test file without a result. Its base tests that didn't run are `unrun`, never `removed`. INCOMPLETE is not FAIL: record writes `cert-ci: INCOMPLETE <name>@<version> (<n> files without results: …; re-run the failed jobs) (ci: …; known: …)`, with the known issues as on a certified line, and the `cert-review` status is `error`, which means re-run, where `failure` means a real defect. A defect found in code (a compat crash) still FAILs. A base suite that is missing or didn't finish is a `missing-evidence` finding. It blocks `certified` in code (record FAILs it) unless the release suite is complete and all-pass and the delta removes or renames no test file and drops no test title (`dropped` per row). Either way the line ends with `base suite incomplete (<n> results)`. Claude, on Bedrock with the key only in that step, then reads those artifacts and `instructions.txt` (which has the roll-forward FAIL list verbatim) and writes `verdict.json` and `comment.md`. Its tools are reading files, `gh pr view`, and `gh issue list`/`view`. `gh issue create` is allowed only for a release on `main`; otherwise, and for an issue in another repository, it lists the issue under `elsewhere` for the cert lane to file.
952
957
  - **record** (`record.mjs`), in code, with read-only permissions:
953
958
  - It re-downloads the tarball and checks `dist.integrity` (sha512) and the sha256, the SLSA v1 provenance (the tarball is its subject; `release.yml`, `refs/heads/main`, a GitHub-hosted builder; commit = `gitHead` = the tag), `npm audit signatures`, and that the commit is on `main`. A problem there, a `compat-crash`, or a planted build is a FAIL whatever the review says.
954
- - It adds the **known issues as of the release**: every issue labelled `known-issue` (open or closed) and every one in `known.json`, unless it was fixed before the release. That holds when `known.json`'s `fixedIn` names an earlier or equal version, or the merged pull request that closed it is an ancestor of the tag, or it was closed (with no closing pull request) before the release commit. An issue closed as not planned or as a duplicate is dropped.
959
+ - It adds the **known issues as of the release**: every issue labelled `known-issue` (open or closed) and every one in `known.json`, unless it was fixed before the release. An issue in another repository, whether listed or named by the reviewer, counts only while it is open. That holds when `known.json`'s `fixedIn` names an earlier or equal version, or the merged pull request that closed it is an ancestor of the tag, or it was closed (with no closing pull request) before the release commit. An issue closed as not planned or as a duplicate is dropped.
955
960
  - The line is `certified @ai-ecoverse/slicc-kernel@<v> sha256=<…> (ci: <run>; known: …)`. Any test gaps from the findings (`no-test`, `no-negative`, `prev-not-run`, `no-titles`, `not-run`) are appended in code as `test gaps: #<pr> <kind>`, so a fix without a NEGATIVE is never certified silently.
956
- - **publish** runs only for a release on `main`, never for a dry run. It puts the line into a `cert-review` commit status on the tag's commit and into the GitHub release's notes, between `<!-- cert-review -->` markers.
957
-
958
- Moving the `certified` dist-tag stays manual. Automating it needs an npm token that may change dist-tags, in a step that re-hashes the tarball and tags only on a `certified` line.
961
+ - **verify** (`verify.mjs`) is the `release / verify on npm` job above.
959
962
 
960
- ### Certification in the pull request (`cert-review-pr`)
963
+ ### Certification in the pull request (`cert-review`)
961
964
 
962
- `.github/workflows/cert-review-pr.yml` certifies a pull request before it merges, so `main`, and with it npm's `latest`, only gets certified changes. For now it reports as the `cert-review-pr` commit status and isn't a required check. It becomes the required `cert-review` once it has proved itself on real pull requests; the post-publish review above stays until then.
965
+ `.github/workflows/cert-review-pr.yml` certifies a pull request before it merges, so `main`, and with it npm's `latest`, only gets certified changes. It reports as the `cert-review` commit status, a required check of `main`'s merge queue. Every same-repo pull request gets one (`not behaviour`, `waits for merge-ok`, the review's verdict, or the self-guard's hold). **A pull request from a fork gets none**, because fork code never reaches the pools or the key, so it can't enter the queue: only Lars merges one, after reading it.
963
966
 
964
- - **Which pull requests:** those that change shipped behaviour, by a conservative path filter (`behaviourFiles` in `lib.mjs`): `src/`, `build.mjs`, `package.json` and its lock, `tsconfig*.json`, `.releaserc*`, `tools/release-recover.mjs`, the workflows that build or publish (`release.yml`, `ci.yml`), and the reviewer's own code (`tools/cert-review/`, `cert-review*.yml`, `.github/runner.mjs`). **Main's code decides:** every job checks out the pull request's base and fetches only the trees under test, so the path filter, pack, suite, findings, the instructions and record all run from `main`. A pull request that changes the reviewer is reviewed by `main`'s reviewer. The workflow file of a `pull_request` run still comes from the pull request, so **pull requests that touch the reviewer always go to the Kernel Coordinator before they are queued.** The pull request that adds this workflow can't pass its own check, because its base has no tools yet. Any other pull request gets a green status that says `not behaviour: <files>`, so the skip is recorded.
967
+ - **Which pull requests:** those that change shipped behaviour, by a conservative path filter (`behaviourFiles` in `lib.mjs`): `src/`, `build.mjs`, `package.json` and its lock, `tsconfig*.json`, `.releaserc*`, `tools/release-recover.mjs`, the workflows that build or publish (`release.yml`, `ci.yml`), and the reviewer's own code (`tools/cert-review/`, `cert-review*.yml`, `.github/runner.mjs`). **Main's code decides:** every job checks out the pull request's base and fetches only the trees under test, so the path filter, pack, suite, findings, the instructions and record all run from `main`. A pull request that changes the reviewer is reviewed by `main`'s reviewer. The workflow file of a `pull_request` run still comes from the pull request, so a change to the reviewer is held by the **certification self-guard**, the shared composite action `slicc-shared-web/.github/actions/cert-self-guard` at a pinned sha. Its light `self-guard` job runs on every pull request event and every merge group. When the diff touches the reviewer (`.github/workflows/cert*.yml`, `tools/cert-review/**`, `.github/runner.mjs` and the guard's own paths), `cert-review` is red with `reviewer change: needs coordinator review (cert-review-approved on this head)` and no review runs. The label is the Kernel Coordinator's alone: they add `cert-review-approved` after reading the change, and the review then starts with `merge-ok`. The approval is bound to the head's arrival on GitHub, and a push removes the label. A merge group that holds an unapproved reviewer change is red too. If the guard doesn't finish, the status is an error that asks to re-run the failed jobs. **The residual:** the pull request's YAML still decides what runs, so the guard catches an accidental edit, not a deliberate rewrite of this workflow. The pull request that adds this workflow can't pass its own check, because its base has no tools yet. Any other pull request gets a green status that says `not behaviour: <files>`, so the skip is recorded.
968
+ - **Decide by what ships:** when a pull request's behaviour files are only `package.json` and/or `package-lock.json` (a Renovate devDependency bump, say), both sides are packed as usual, and then the `same dist` job (`same.mjs`, in code) compares the two tarballs before anything else runs. It checks the same paths, entry types (a symlink isn't a file) and modes. `package.json` must be equal once `devDependencies`, `version` and `gitHead` are removed, so `scripts` (they ship, a consumer's install runs `postinstall`, and `prepack`/`prepublishOnly` can make the published build differ from the packed one), runtime dependencies, `exports`, `bin`, `files` and `engines` must all match. Every other file must be byte-identical once each side's **exact** version string (build.mjs bakes it into `dist`) is replaced by one placeholder. A version string in one more place, or any looser match, is a difference. Source maps are compared as JSON without their `mappings`, whose columns the longer or shorter version string shifts; the code itself is compared byte for byte. If all of that matches, `cert-review` is green with `not behaviour (identical dist): <candidate sha256> = <base sha256> after normalising <v1>/<v2>`, and the suites, kernel-compat, the review and the record are skipped. `same.json` (uploaded as an artifact) names the rule and both tarballs' full sha256. Any difference, or a `same dist` job that doesn't finish, means the full review. The kernel has no runtime `dependencies`, since esbuild bundles everything into `dist`, so a devDependency that changes the product shows up as a byte difference. A test-only devDependency is checked by `ci / test`, on the pull request and on the merge group.
965
969
  - **When:** only for a same-repo pull request with `merge-ok`, when the label is added and on every push while it is set. Each pull request has its own concurrency group, and a newer push cancels the older review. A behaviour change without `merge-ok` gets a red status, `waits for merge-ok to be reviewed`. Other labels don't start or cancel anything. Fork code never reaches the pools or the key.
966
970
  - **What:** the checkout is the event's default `refs/pull/N/merge`, the pull request merged onto `main` at review time, so a review is of what would land, without pushing to the branch. Both sides are packed in the same run, on a GitHub-hosted runner, with no secrets: the candidate (the merge ref, `x.y.z-pr.N.<run>`) and the base (its first parent, `main`). Then:
967
971
  - the candidate's whole suite, and the base's;
968
- - the pull request's new or changed test files on the base build (the NEGATIVE: a fix's test must fail on `main` and pass on the candidate);
972
+ - the pull request's new or changed test files on `main`'s tree, with the pull request's `test/**` applied, and `main`'s build (the NEGATIVE: a fix's test must fail on `main` and pass on the candidate);
969
973
  - kernel-compat on the candidate's artifact (#812), against npm's `latest`;
970
974
  - the review (Claude through `claude-code-action/base-action`, with the Bedrock key in that step only, no issue filing);
971
975
  - and record, which re-hashes the candidate's artifact against the pack job's sha256.
@@ -1100,6 +1104,16 @@ A threaded WASIX C program for `test/integration/thread-signal.test.mjs` (#520).
1100
1104
 
1101
1105
  **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 (`-pthread`) and binaryen's `wasm-opt` on `PATH`. `bin/sigthread.wasm` is 44,788 bytes, sha256 `5781fa39d0a0032cd9b214b71de1f15f589b40e4cecd6862362048dd27f0db56`.
1102
1106
 
1107
+ ### `spintest`
1108
+
1109
+ A WASIX C program and a JS command for `test/integration/transport-spin.test.mjs`, which guards the sync transport's paths from #856 (#864).
1110
+ - `spintest rounds N` does N 1-byte write+read rounds on a pipe, a different byte each round, and prints `N rounds in order` if every byte came back as sent. These are fast replies, which take the transport's spin.
1111
+ - `spintest wait` prints `waiting` and blocks in a 1-byte `read` of its terminal, a reply that comes only when the test types. The test measures its own process's CPU (all threads) while the read blocks.
1112
+ - `spintest big M` writes an M MiB file and reads it back in 1 MiB `read` calls, each of which must be whole and match. A 1 MiB read fills the transport window exactly.
1113
+ - `spinread PATH` (`bin/spinread.mjs`, a JS command) reads a whole file through the synchronous `ctx.sync.fs.readFile` and prints its length and a 31-hash. A file over 1 MiB is a reply larger than the window, so it comes in several chunks.
1114
+
1115
+ **Build:** `WASIX_SYSROOT_28=/path/to/wasix-sysroot ./build.sh`, with `wasixcc` 0.4.7 and binaryen's `wasm-opt` on `PATH`. `bin/spintest.wasm` is 50,095 bytes, sha256 `be7659422f187a97fac412ac1dd2ec9d412e8dcd067da88157960352ded6178b`.
1116
+
1103
1117
  ### `longwait`
1104
1118
 
1105
1119
  A WASIX C program for `test/integration/long-timers.test.mjs` (#767).
@@ -219,13 +219,22 @@ var IndexedDbMeta = class {
219
219
  this.cache.set(path, next3 ?? null);
220
220
  }
221
221
  async updateMany(changes) {
222
+ if (changes.length === 0) return;
223
+ this.settleRemoves();
222
224
  const [transaction, store] = this.transaction("readwrite");
225
+ const next3 = /* @__PURE__ */ new Map();
223
226
  for (const { path, change } of changes) {
224
227
  const request2 = store.get(path);
225
- request2.onsuccess = () => store.put(change(request2.result));
228
+ request2.onsuccess = () => {
229
+ const entry = change(request2.result);
230
+ next3.set(path, entry ?? null);
231
+ if (entry) store.put(entry);
232
+ else store.delete(path);
233
+ };
226
234
  }
227
235
  await settle(transaction, () => void 0);
228
236
  this.changed({ paths: changes.map(({ path }) => path) });
237
+ for (const [path, entry] of next3) this.cache.set(path, entry);
229
238
  }
230
239
  close() {
231
240
  this.channel.close();
@@ -1021,6 +1030,12 @@ function sidecar(meta) {
1021
1030
  });
1022
1031
  const names = meta.fresh ? [...META_OPS, "fresh"] : META_OPS;
1023
1032
  const ops = Object.fromEntries(names.map((op) => [op, guarded2(op)]));
1033
+ const { updateMany } = meta;
1034
+ ops.updateMany = updateMany ? (changes) => updateMany.call(meta, changes).catch((err) => {
1035
+ throw translate(err, changes[0]?.path ?? "/", "sidecar", false);
1036
+ }) : async (changes) => {
1037
+ for (const { path, change } of changes) await ops.update(path, change);
1038
+ };
1024
1039
  if (meta.absent) ops.absent = meta.absent.bind(meta);
1025
1040
  const { removeFile } = meta;
1026
1041
  if (removeFile)
@@ -1503,14 +1518,20 @@ var OpfsFs = class {
1503
1518
  this.flushing.file.unref?.();
1504
1519
  }
1505
1520
  async flushFileTimes() {
1506
- for (const [path, ms] of [...this.fileTimes]) {
1507
- const mtimeFor = await this.modified(path).catch(() => 0);
1508
- await this.meta.update(
1521
+ const pending = [...this.fileTimes];
1522
+ const stamps = [];
1523
+ for (const [path] of pending) stamps.push(await this.modified(path).catch(() => 0));
1524
+ await this.batched(
1525
+ pending.map(([path, ms], i) => ({
1509
1526
  path,
1510
- (entry) => this.fileTimes.get(path) !== ms || this.explicitWins(entry, ms) ? entry : { ...entry, path, mtimeMs: ms, mtimeFor, ctimeMs: ms }
1511
- );
1527
+ change: (entry) => this.fileTimes.get(path) !== ms || this.explicitWins(entry, ms) ? entry : { ...entry, path, mtimeMs: ms, mtimeFor: stamps[i], ctimeMs: ms }
1528
+ }))
1529
+ );
1530
+ for (const [path, ms] of pending)
1512
1531
  if (this.fileTimes.get(path) === ms) this.fileTimes.delete(path);
1513
- }
1532
+ }
1533
+ batched(changes) {
1534
+ return this.meta.updateMany(changes);
1514
1535
  }
1515
1536
  timesOf(path) {
1516
1537
  return this.dirTimes.get(path) ?? this.fileTimes.get(path);
@@ -1542,15 +1563,21 @@ var OpfsFs = class {
1542
1563
  });
1543
1564
  }
1544
1565
  async flushDirTimes() {
1545
- for (const path of this.dirDirty) {
1546
- this.dirDirty.delete(path);
1547
- const ms = this.dirTimes.get(path);
1548
- if (ms === void 0) continue;
1549
- await this.meta.update(
1550
- path,
1551
- (entry) => (entry?.timesAt ?? -1) >= ms ? entry : { ...entry, path, mtimeMs: ms, mtimeFor: 0, ctimeMs: ms }
1566
+ while (this.dirDirty.size > 0) {
1567
+ const pending = [];
1568
+ for (const path of this.dirDirty) {
1569
+ const ms = this.dirTimes.get(path);
1570
+ if (ms !== void 0) pending.push([path, ms]);
1571
+ }
1572
+ this.dirDirty.clear();
1573
+ await this.batched(
1574
+ pending.map(([path, ms]) => ({
1575
+ path,
1576
+ change: (entry) => (entry?.timesAt ?? -1) >= ms ? entry : { ...entry, path, mtimeMs: ms, mtimeFor: 0, ctimeMs: ms }
1577
+ }))
1552
1578
  );
1553
- if (this.dirTimes.get(path) === ms) this.dirTimes.delete(path);
1579
+ for (const [path, ms] of pending)
1580
+ if (this.dirTimes.get(path) === ms) this.dirTimes.delete(path);
1554
1581
  }
1555
1582
  }
1556
1583
  async snapshot(handle) {
@@ -2089,6 +2116,9 @@ var OpfsFs = class {
2089
2116
  async chmod(path, mode) {
2090
2117
  const found = await this.locate(path, true);
2091
2118
  this.refuse(found.path);
2119
+ const [known] = await this.meta.get([found.path]);
2120
+ const directory = found.handle?.kind === "directory";
2121
+ if ((this.mode(found.path, directory, known) & PERM_MASK) === (mode & PERM_MASK)) return;
2092
2122
  const ctimeMs = Date.now();
2093
2123
  await this.meta.update(found.path, (entry) => ({
2094
2124
  ...entry,
@@ -6146,7 +6176,7 @@ function satisfies(version, range2) {
6146
6176
  }
6147
6177
 
6148
6178
  // src/version.ts
6149
- var KERNEL_VERSION = true ? "1.110.1" : "0.0.0-dev";
6179
+ var KERNEL_VERSION = true ? "1.110.3" : "0.0.0-dev";
6150
6180
  var OS_RELEASE = "6.0.0-slicc";
6151
6181
  function procVersion() {
6152
6182
  return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC