@ai-ecoverse/slicc-kernel 1.117.0 → 1.117.2
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 +2 -2
- package/dist/kernel-worker.js +125 -47
- package/dist/kernel-worker.js.map +3 -3
- package/dist/node-process-worker.js +1 -1
- package/dist/node.js +125 -47
- package/dist/node.js.map +3 -3
- package/dist/process-worker.js +1 -1
- package/dist/types/fs/opfs.d.ts +6 -0
- package/dist/types/fs/unlinked.d.ts +2 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -701,7 +701,7 @@ Programs mount and unmount with `mount(2)` and `umount2(2)`, so a `mount`/`umoun
|
|
|
701
701
|
|
|
702
702
|
### Exec
|
|
703
703
|
|
|
704
|
-
An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
|
|
704
|
+
An exec'd program takes over the pid of the process it replaces, as on Linux: `getpid()` and `$$` in the new image, `/proc`, `ps`, `kill` and the parent's `waitpid` all agree on that pid, and the image's parent pid is the replaced process's. Its own children see that pid as their parent. A file removed or renamed over while programs hold it open keeps its bytes for them, as on Linux, whether the descriptors live in the kernel or a program opened the file in its own worker: every open, read-only ones included, is known to the kernel, and all holders share the one file (writes through one are seen through the others). The kernel does not copy it: as NFS does with its "silly rename", it moves the file into a hidden directory, `.slicc-unlinked/`, at the root of the mount it lives on (or of the root file system), and serves the holders' reads and writes from there (a worker's I/O on the old path while the path is gone). No file data is held in memory for it. The file is deleted when the last holder closes it or exits, and the directory goes as soon as it is empty. A file created at the path afterwards is the new file for everyone, also for the holder; a holder whose file another process renames (rather than removes) still uses the old path. A rename over a held file stays atomic for everyone else, as `rename(2)` is: moving the held file aside and the new one into its place are two steps, so a lookup of that name (`open`, `stat`, a read) that comes while they run waits for both, and one that started before them and missed the name in between looks again (also when it spelled the name through a symlinked parent, which the wait doesn't match by path: a miss is retried whenever any replacement finished meanwhile, up to three tries). Every open finds the old file or the new one, never no file (#903). The OPFS root does the same for its own two steps: a rename onto an existing name replaces the file with `move()` (or, in a browser without it, a copy into the name and a removal of the source), and a lookup that loses that race to a stale handle looks again. A `tmpfs` mount replaces the name in one step; on `fsa` and `hostfs` mounts it is the real file system's `rename`. Listings in the kernel leave `.slicc-unlinked` out, but on an `fsa` or `hostfs` mount it is visible on the real machine while a removed file is held (add `.slicc-unlinked/` to a `.gitignore` there). Each kernel names its entries after itself and holds a Web Lock for that name while it runs; when a mount is set up (and the root at start), the kernel removes the entries whose owner holds no lock, so a tab that died while holding a file leaves nothing behind for long, and entries of other live tabs on the same origin are left alone. Under the Node entry, which has no Web Locks, entries are named after the kernel's process id instead, and an entry is removed when that process is gone (`kill(pid, 0)` fails with `ESRCH`); a reused pid leaves the entry alone. Signals a program ignores stay ignored in the image it execs and in the children it spawns or forks, as POSIX has it, so `nohup cmd` and `trap '' TERM; exec cmd` keep `cmd` safe from them; caught signals go back to their default. The kernel holds what was handed down, so a new image that sets one of those signals back to the default with `signal(SIG_DFL)` still ignores it, until it installs a handler.
|
|
705
705
|
|
|
706
706
|
- **Emscripten:** `Module.sliccKernel.execve(file, argv, env, cwd)` runs `file` as this process's new image on its fds 0, 1 and 2, waits for it, and returns its wait status, or a negative errno when it cannot start. `env` and `cwd` may be `null` for this process's own. An exec shim detects it with `typeof Module.sliccKernel.execve === 'function'` and exits with the status it returns, as `execve` never returns on success. Older shims spawn the program and then call `Module.sliccKernel.execWait(pid)`; that works the same way: a new program asks the kernel for its pid before `main`, and the kernel answers once its parent has made its next call, so an image an older shim execs also reports the pid it replaced (the wait starts once the program is loaded; should the parent make no call within 100 ms after that, the kernel answers anyway, and the image may then report its own pid; `launcher.identities` counts the programs that asked and those answered by that timeout).
|
|
707
707
|
- **WASIX:** `proc_exec` does this by itself.
|
|
@@ -1007,7 +1007,7 @@ The tools in `tools/cert-review/`:
|
|
|
1007
1007
|
|
|
1008
1008
|
`.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.
|
|
1009
1009
|
|
|
1010
|
-
- **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.
|
|
1010
|
+
- **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`. The merge group's lookup loads its tools from the protected branch itself (`merge_group.base_ref`), not from the group's `base_sha`, which can be the speculative commit of the group ahead and carry the reviewer changes of the pull requests queued before it; `base_sha` is only the lookup's input. 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.
|
|
1011
1011
|
- **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.
|
|
1012
1012
|
- **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.
|
|
1013
1013
|
- **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:
|
package/dist/kernel-worker.js
CHANGED
|
@@ -998,6 +998,7 @@ var ABSENT_MS = 2;
|
|
|
998
998
|
var ABSENT_NAMES = 4096;
|
|
999
999
|
var FILE_HANDLES = 4096;
|
|
1000
1000
|
var STALE = /* @__PURE__ */ new Set(["NotFoundError", "TypeMismatchError"]);
|
|
1001
|
+
var REPLACED_ATTEMPTS = 4;
|
|
1001
1002
|
var EXECUTABLE = /\/node_modules\/(?:@[^/]+\/)?[^/]+\/bin\//;
|
|
1002
1003
|
var ERRNO_BY_DOM_ERROR = {
|
|
1003
1004
|
NotFoundError: "ENOENT",
|
|
@@ -1132,6 +1133,8 @@ var OpfsFs = class {
|
|
|
1132
1133
|
files = new PathMap();
|
|
1133
1134
|
absent = new PathMap();
|
|
1134
1135
|
changes = 0;
|
|
1136
|
+
replacing = Promise.resolve();
|
|
1137
|
+
replacements = 0;
|
|
1135
1138
|
snapshots = /* @__PURE__ */ new WeakMap();
|
|
1136
1139
|
found = /* @__PURE__ */ new WeakMap();
|
|
1137
1140
|
checked = /* @__PURE__ */ new WeakMap();
|
|
@@ -1466,7 +1469,22 @@ var OpfsFs = class {
|
|
|
1466
1469
|
}
|
|
1467
1470
|
return void 0;
|
|
1468
1471
|
}
|
|
1469
|
-
|
|
1472
|
+
locate(path, follow2) {
|
|
1473
|
+
return this.unreplaced(() => this.located(path, follow2));
|
|
1474
|
+
}
|
|
1475
|
+
async unreplaced(op) {
|
|
1476
|
+
for (let attempt = 1; ; attempt++) {
|
|
1477
|
+
const seen = this.replacements;
|
|
1478
|
+
try {
|
|
1479
|
+
return await op();
|
|
1480
|
+
} catch (err) {
|
|
1481
|
+
if (attempt >= REPLACED_ATTEMPTS || !missing2(err)) throw err;
|
|
1482
|
+
await this.replacing;
|
|
1483
|
+
if (this.replacements === seen) throw err;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async located(path, follow2) {
|
|
1470
1488
|
let current = path;
|
|
1471
1489
|
for (let hops = 0; hops <= MAX_LINKS2; hops++) {
|
|
1472
1490
|
try {
|
|
@@ -1664,11 +1682,11 @@ var OpfsFs = class {
|
|
|
1664
1682
|
const stat = await this.statOf(found.path, found.handle, meta, touched);
|
|
1665
1683
|
return { ...stat, ...ownOf(meta) ?? inherit(entries) };
|
|
1666
1684
|
}
|
|
1667
|
-
|
|
1668
|
-
return this.describe(await this.
|
|
1685
|
+
stat(path) {
|
|
1686
|
+
return this.unreplaced(async () => this.describe(await this.located(path, true)));
|
|
1669
1687
|
}
|
|
1670
|
-
|
|
1671
|
-
return this.describe(await this.
|
|
1688
|
+
lstat(path) {
|
|
1689
|
+
return this.unreplaced(async () => this.describe(await this.located(path, false)));
|
|
1672
1690
|
}
|
|
1673
1691
|
async exists(path) {
|
|
1674
1692
|
return this.locate(path, true).then(
|
|
@@ -1692,6 +1710,12 @@ var OpfsFs = class {
|
|
|
1692
1710
|
}
|
|
1693
1711
|
}
|
|
1694
1712
|
}
|
|
1713
|
+
reading(path, op) {
|
|
1714
|
+
return this.unreplaced(async () => {
|
|
1715
|
+
const handle = await this.existing(path);
|
|
1716
|
+
return this.retrying(path, () => op(handle), false);
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1695
1719
|
async existing(path, writing = false) {
|
|
1696
1720
|
const found = await this.locate(path, true);
|
|
1697
1721
|
if (writing) this.refuse(found.path);
|
|
@@ -1705,52 +1729,35 @@ var OpfsFs = class {
|
|
|
1705
1729
|
if (entry?.fifo || entry?.socket) throw fsError("ENXIO", path);
|
|
1706
1730
|
}
|
|
1707
1731
|
async readFileBuffer(path) {
|
|
1708
|
-
const
|
|
1709
|
-
const bytes = await this.retrying(
|
|
1732
|
+
const bytes = await this.reading(
|
|
1710
1733
|
path,
|
|
1711
|
-
async () => new Uint8Array(await (await handle.getFile()).arrayBuffer())
|
|
1712
|
-
false
|
|
1734
|
+
async (handle) => new Uint8Array(await (await handle.getFile()).arrayBuffer())
|
|
1713
1735
|
);
|
|
1714
1736
|
await this.refuseNode(path, bytes.length === 0);
|
|
1715
1737
|
return bytes;
|
|
1716
1738
|
}
|
|
1717
1739
|
async readVersioned(path, cached = false) {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
await this.refuseNode(path, bytes.length === 0);
|
|
1726
|
-
return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
|
|
1727
|
-
},
|
|
1728
|
-
false
|
|
1729
|
-
);
|
|
1740
|
+
return this.reading(path, async (handle) => {
|
|
1741
|
+
const stamp = await this.stamp(path, cached);
|
|
1742
|
+
const file = await handle.getFile();
|
|
1743
|
+
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
1744
|
+
await this.refuseNode(path, bytes.length === 0);
|
|
1745
|
+
return { bytes, version: `${file.lastModified}:${file.size}:${stamp}` };
|
|
1746
|
+
});
|
|
1730
1747
|
}
|
|
1731
1748
|
async version(path, cached = false) {
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
const file = await handle.getFile();
|
|
1738
|
-
return `${file.lastModified}:${file.size}:${stamp}`;
|
|
1739
|
-
},
|
|
1740
|
-
false
|
|
1741
|
-
);
|
|
1749
|
+
return this.reading(path, async (handle) => {
|
|
1750
|
+
const stamp = await this.stamp(path, cached);
|
|
1751
|
+
const file = await handle.getFile();
|
|
1752
|
+
return `${file.lastModified}:${file.size}:${stamp}`;
|
|
1753
|
+
});
|
|
1742
1754
|
}
|
|
1743
1755
|
async pread(path, offset, length2) {
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
path,
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
await this.refuseNode(path, file.size === 0);
|
|
1750
|
-
return new Uint8Array(await file.slice(offset, offset + length2).arrayBuffer());
|
|
1751
|
-
},
|
|
1752
|
-
false
|
|
1753
|
-
);
|
|
1756
|
+
return this.reading(path, async (handle) => {
|
|
1757
|
+
const file = await handle.getFile();
|
|
1758
|
+
await this.refuseNode(path, file.size === 0);
|
|
1759
|
+
return new Uint8Array(await file.slice(offset, offset + length2).arrayBuffer());
|
|
1760
|
+
});
|
|
1754
1761
|
}
|
|
1755
1762
|
async pwrite(path, offset, bytes) {
|
|
1756
1763
|
const [handle, , target] = await this.fileFor(path);
|
|
@@ -2063,7 +2070,10 @@ var OpfsFs = class {
|
|
|
2063
2070
|
const staging = swap ? `.${name2}.${crypto.randomUUID()}` : name2;
|
|
2064
2071
|
this.forget(destination);
|
|
2065
2072
|
const timed2 = await this.explicitTimes(source.path);
|
|
2066
|
-
const moved2 = await this.
|
|
2073
|
+
const moved2 = await this.replacingTarget(
|
|
2074
|
+
targetStat !== null,
|
|
2075
|
+
() => this.place(source.handle, dir, staging)
|
|
2076
|
+
);
|
|
2067
2077
|
if (swap) await this.rm(destination, { recursive: true });
|
|
2068
2078
|
await this.meta.move(source.path, destination, carry);
|
|
2069
2079
|
this.movePending(source.path, destination);
|
|
@@ -2079,6 +2089,21 @@ var OpfsFs = class {
|
|
|
2079
2089
|
this.changed(source.path, destination);
|
|
2080
2090
|
this.touch(Date.now(), parentOf3(source.path), parent.path);
|
|
2081
2091
|
}
|
|
2092
|
+
async replacingTarget(replaces, op) {
|
|
2093
|
+
if (!replaces) return op();
|
|
2094
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
2095
|
+
const before = this.replacing;
|
|
2096
|
+
this.replacing = Promise.all([before, promise]).then(() => void 0);
|
|
2097
|
+
this.replacements++;
|
|
2098
|
+
this.changes++;
|
|
2099
|
+
try {
|
|
2100
|
+
return await op();
|
|
2101
|
+
} finally {
|
|
2102
|
+
this.replacements++;
|
|
2103
|
+
this.changes++;
|
|
2104
|
+
resolve2();
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2082
2107
|
async modified(path) {
|
|
2083
2108
|
const handle = await this.handle(path);
|
|
2084
2109
|
return handle.kind === "file" ? (await handle.getFile()).lastModified : 0;
|
|
@@ -6260,7 +6285,7 @@ function satisfies(version, range2) {
|
|
|
6260
6285
|
}
|
|
6261
6286
|
|
|
6262
6287
|
// src/version.ts
|
|
6263
|
-
var KERNEL_VERSION = true ? "1.117.
|
|
6288
|
+
var KERNEL_VERSION = true ? "1.117.2" : "0.0.0-dev";
|
|
6264
6289
|
var OS_RELEASE = "6.0.0-slicc";
|
|
6265
6290
|
function procVersion() {
|
|
6266
6291
|
return `Linux version ${OS_RELEASE} (slicc-kernel ${KERNEL_VERSION}) #1 SMP PREEMPT_DYNAMIC
|
|
@@ -7089,6 +7114,16 @@ var UNLINKED_DIR = ".slicc-unlinked";
|
|
|
7089
7114
|
function webLocks() {
|
|
7090
7115
|
return globalThis.navigator?.locks;
|
|
7091
7116
|
}
|
|
7117
|
+
var LOOKUP_OPS = [
|
|
7118
|
+
"exists",
|
|
7119
|
+
"readFile",
|
|
7120
|
+
"readFileBuffer",
|
|
7121
|
+
"readVersioned",
|
|
7122
|
+
"version",
|
|
7123
|
+
"pread",
|
|
7124
|
+
"readlink",
|
|
7125
|
+
"realpath"
|
|
7126
|
+
];
|
|
7092
7127
|
var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
|
|
7093
7128
|
var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
|
|
7094
7129
|
var lockName = (instance) => `slicc-unlinked:${instance}`;
|
|
@@ -7127,6 +7162,8 @@ var UnlinkedKeeper = class {
|
|
|
7127
7162
|
gate = new PathGate();
|
|
7128
7163
|
queue = Promise.resolve();
|
|
7129
7164
|
count = 0;
|
|
7165
|
+
replacing = /* @__PURE__ */ new Map();
|
|
7166
|
+
replaced = 0;
|
|
7130
7167
|
links = /* @__PURE__ */ new Map();
|
|
7131
7168
|
linked = /* @__PURE__ */ new Map();
|
|
7132
7169
|
dirOwner;
|
|
@@ -7149,6 +7186,44 @@ var UnlinkedKeeper = class {
|
|
|
7149
7186
|
}
|
|
7150
7187
|
wrap(fs = this.fs) {
|
|
7151
7188
|
const keeper = this;
|
|
7189
|
+
const { replacing } = this;
|
|
7190
|
+
const replace = async (target, op) => {
|
|
7191
|
+
const before = replacing.get(target);
|
|
7192
|
+
const { promise, resolve: resolve2 } = Promise.withResolvers();
|
|
7193
|
+
const mine = {
|
|
7194
|
+
done: Promise.all([before?.done, promise]).then(() => void 0)
|
|
7195
|
+
};
|
|
7196
|
+
replacing.set(target, mine);
|
|
7197
|
+
try {
|
|
7198
|
+
await op();
|
|
7199
|
+
} finally {
|
|
7200
|
+
keeper.replaced += 1;
|
|
7201
|
+
resolve2();
|
|
7202
|
+
if (replacing.get(target) === mine) replacing.delete(target);
|
|
7203
|
+
}
|
|
7204
|
+
};
|
|
7205
|
+
const unreplaced = async (path, op) => {
|
|
7206
|
+
const at = fs.resolvePath("/", path);
|
|
7207
|
+
for (let attempt = 1; ; attempt++) {
|
|
7208
|
+
await replacing.get(at)?.done;
|
|
7209
|
+
const seen = keeper.replaced;
|
|
7210
|
+
try {
|
|
7211
|
+
return await op();
|
|
7212
|
+
} catch (err) {
|
|
7213
|
+
const code = err?.code;
|
|
7214
|
+
if (attempt >= 3 || code !== "ENOENT") throw err;
|
|
7215
|
+
await replacing.get(at)?.done;
|
|
7216
|
+
if (keeper.replaced === seen) throw err;
|
|
7217
|
+
}
|
|
7218
|
+
}
|
|
7219
|
+
};
|
|
7220
|
+
const looking = (name2) => {
|
|
7221
|
+
const call = fs[name2];
|
|
7222
|
+
if (!call) return {};
|
|
7223
|
+
return {
|
|
7224
|
+
[name2]: (path, ...rest2) => unreplaced(path, () => call.call(fs, path, ...rest2))
|
|
7225
|
+
};
|
|
7226
|
+
};
|
|
7152
7227
|
const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
|
|
7153
7228
|
const counted2 = (st) => {
|
|
7154
7229
|
const hidden = keeper.links.get(`${st.dev ?? 0}:${st.ino}`) ?? 0;
|
|
@@ -7156,9 +7231,10 @@ var UnlinkedKeeper = class {
|
|
|
7156
7231
|
};
|
|
7157
7232
|
return {
|
|
7158
7233
|
...fs,
|
|
7234
|
+
...LOOKUP_OPS.reduce((all, name2) => Object.assign(all, looking(name2)), {}),
|
|
7159
7235
|
links: (path) => mayLink(fs, path),
|
|
7160
|
-
stat: async (
|
|
7161
|
-
lstat: async (
|
|
7236
|
+
stat: (path) => unreplaced(path, async () => counted2(await fs.stat(path))),
|
|
7237
|
+
lstat: (path) => unreplaced(path, async () => counted2(await fs.lstat(path))),
|
|
7162
7238
|
async rm(path, options) {
|
|
7163
7239
|
const target = fs.resolvePath("/", path);
|
|
7164
7240
|
const pinned = await keeper.pin(target);
|
|
@@ -7168,8 +7244,10 @@ var UnlinkedKeeper = class {
|
|
|
7168
7244
|
async rename(from, to) {
|
|
7169
7245
|
const target = fs.resolvePath("/", to);
|
|
7170
7246
|
if (target === fs.resolvePath("/", from)) return fs.rename(from, to);
|
|
7171
|
-
|
|
7172
|
-
|
|
7247
|
+
await replace(target, async () => {
|
|
7248
|
+
const pinned = await keeper.pin(target);
|
|
7249
|
+
await keeper.settle(pinned, () => fs.rename(from, to));
|
|
7250
|
+
});
|
|
7173
7251
|
},
|
|
7174
7252
|
readdir: async (path) => visible(await fs.readdir(path)),
|
|
7175
7253
|
readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR).map(([name2, st]) => [name2, st && counted2(st)])
|