@nimbus-sh/core 0.4.0 → 0.6.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.
- package/README.md +35 -7
- package/dist/_shared/cache-stats.d.ts +23 -1
- package/dist/_shared/cache-stats.d.ts.map +1 -1
- package/dist/_shared/cache-stats.js +13 -1
- package/dist/_shared/error-text.d.ts +15 -0
- package/dist/_shared/error-text.d.ts.map +1 -0
- package/dist/_shared/error-text.js +21 -0
- package/dist/_shared/exports-resolver.d.ts +19 -7
- package/dist/_shared/exports-resolver.d.ts.map +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/retry.d.ts.map +1 -1
- package/dist/_shared/retry.js +4 -2
- package/dist/constants.d.ts +5 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +23 -49
- package/dist/runtime/assets-loader.js +1 -1
- package/dist/runtime/esbuild-service.d.ts +4 -3
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +9 -4
- package/dist/runtime/installed-runtimes.d.ts +4 -3
- package/dist/runtime/installed-runtimes.d.ts.map +1 -1
- package/dist/runtime/long-running-handle.d.ts +3 -3
- package/dist/runtime/long-running-handle.d.ts.map +1 -1
- package/dist/runtime/os-contracts.d.ts +7 -5
- package/dist/runtime/os-contracts.d.ts.map +1 -1
- package/dist/runtime/port-registry.d.ts +28 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +79 -9
- package/dist/runtime/process-logs.d.ts +9 -1
- package/dist/runtime/process-logs.d.ts.map +1 -1
- package/dist/runtime/project-detect.d.ts +6 -1
- package/dist/runtime/project-detect.d.ts.map +1 -1
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +3 -4
- package/dist/runtime/runtime-registry.d.ts +4 -3
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +2 -1
- package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +2 -2
- package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +3 -3
- package/dist/runtime/virtual-socket-kernel.js +10 -5
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +22 -14
- package/dist/shell/features.d.ts.map +1 -1
- package/dist/shell/features.js +5 -4
- package/dist/shell/unix-commands.d.ts +14 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +115 -69
- package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
- package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
- package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
- package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/index.js +3 -2
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
- package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
- package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/process.js +6 -3
- package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
- package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/Shell.js +86 -75
- package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
- package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/lexer.js +40 -0
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +9 -3
- package/dist/vfs/sqlite-vfs.d.ts +63 -31
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +206 -41
- package/package.json +3 -2
- package/src/_shared/cache-stats.ts +27 -1
- package/src/_shared/error-text.ts +21 -0
- package/src/_shared/exports-resolver.ts +24 -4
- package/src/_shared/retry.ts +5 -3
- package/src/constants.ts +37 -53
- package/src/runtime/assets-loader.ts +1 -1
- package/src/runtime/esbuild-service.ts +16 -11
- package/src/runtime/installed-runtimes.ts +4 -3
- package/src/runtime/long-running-handle.ts +3 -3
- package/src/runtime/os-contracts.ts +7 -5
- package/src/runtime/port-registry.ts +97 -9
- package/src/runtime/process-logs.ts +12 -3
- package/src/runtime/project-detect.ts +7 -1
- package/src/runtime/require-resolver.ts +7 -7
- package/src/runtime/runtime-registry.ts +9 -7
- package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
- package/src/runtime/virtual-socket-kernel.generated.ts +3 -3
- package/src/runtime/virtual-socket-kernel.ts +10 -5
- package/src/runtime/wasm-runner.ts +55 -29
- package/src/shell/features.ts +5 -4
- package/src/shell/unix-commands.ts +273 -128
- package/src/substrate/lifo/commands/net/ip.ts +15 -15
- package/src/substrate/lifo/commands/system/ps.ts +2 -1
- package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
- package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
- package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
- package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
- package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
- package/src/substrate/lifo/node-compat/index.ts +3 -2
- package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
- package/src/substrate/lifo/node-compat/process.ts +5 -2
- package/src/substrate/lifo/shell/Shell.ts +92 -70
- package/src/substrate/lifo/shell/lexer.ts +38 -0
- package/src/vfs/seed-project.ts +13 -6
- package/src/vfs/sqlite-vfs.ts +233 -77
- package/dist/_shared/install-phase.d.ts +0 -47
- package/dist/_shared/install-phase.d.ts.map +0 -1
- package/dist/_shared/install-phase.js +0 -1
- package/dist/_shared/rpc-dispose.d.ts +0 -4
- package/dist/_shared/rpc-dispose.d.ts.map +0 -1
- package/dist/_shared/rpc-dispose.js +0 -30
- package/dist/_shared/w7-frame.d.ts +0 -79
- package/dist/_shared/w7-frame.d.ts.map +0 -1
- package/dist/_shared/w7-frame.js +0 -839
- package/dist/_shared/weighted-credit-pool.d.ts +0 -61
- package/dist/_shared/weighted-credit-pool.d.ts.map +0 -1
- package/dist/_shared/weighted-credit-pool.js +0 -196
- package/dist/observability/diag-counters.d.ts +0 -236
- package/dist/observability/diag-counters.d.ts.map +0 -1
- package/dist/observability/diag-counters.js +0 -195
- package/dist/observability/heap-estimate.d.ts +0 -225
- package/dist/observability/heap-estimate.d.ts.map +0 -1
- package/dist/observability/heap-estimate.js +0 -185
- package/dist/observability/heavy-alloc-coord.d.ts +0 -93
- package/dist/observability/heavy-alloc-coord.d.ts.map +0 -1
- package/dist/observability/heavy-alloc-coord.js +0 -181
- package/dist/observability/oom-classify.d.ts +0 -138
- package/dist/observability/oom-classify.d.ts.map +0 -1
- package/dist/observability/oom-classify.js +0 -261
- package/dist/observability/oom-discriminator.d.ts +0 -176
- package/dist/observability/oom-discriminator.d.ts.map +0 -1
- package/dist/observability/oom-discriminator.js +0 -272
- package/src/_shared/install-phase.ts +0 -56
- package/src/_shared/rpc-dispose.ts +0 -31
- package/src/_shared/w7-frame.ts +0 -1012
- package/src/_shared/weighted-credit-pool.ts +0 -244
- package/src/observability/diag-counters.ts +0 -343
- package/src/observability/heap-estimate.ts +0 -331
- package/src/observability/heavy-alloc-coord.ts +0 -241
- package/src/observability/oom-classify.ts +0 -253
- package/src/observability/oom-discriminator.ts +0 -405
package/dist/vfs/sqlite-vfs.js
CHANGED
|
@@ -41,21 +41,33 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import { VfsEventEmitter } from './events.js';
|
|
43
43
|
import { normalizeVfsPath } from './path.js';
|
|
44
|
-
import {
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
44
|
+
import { LRU_MAX_ENTRIES, FS_LIST_PAGE_LIMIT, } from '../constants.js';
|
|
45
|
+
import { CHUNK_SIZE, MAX_TX_BLOB_BYTES, MAX_TX_LOGICAL_ROWS, MAX_TX_SQL_EXECS, MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES, } from '@nimbus-sh/platform/limits.js';
|
|
46
|
+
import { recordFailure } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
47
|
+
import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
48
|
+
import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
|
|
48
49
|
import { enc, dec } from '../_shared/bytes.js';
|
|
49
|
-
import { decodeWriteBatchStream, } from '
|
|
50
|
-
import { WeightedCreditPool, } from '
|
|
50
|
+
import { decodeWriteBatchStream, } from '@nimbus-sh/platform/w7-frame.js';
|
|
51
|
+
import { WeightedCreditPool, } from '@nimbus-sh/platform/weighted-credit-pool.js';
|
|
51
52
|
import { LEGACY_SYMLINK_REGISTRY_PATH } from './symlink-registry.js';
|
|
52
53
|
import { CRED_KERNEL, } from '../runtime/os-contracts.js';
|
|
53
54
|
const CONTENT_ID_ALLOCATION_ATTEMPTS = 8;
|
|
55
|
+
/**
|
|
56
|
+
* Live view of the global object. `process` is not in the Workers lib, so its
|
|
57
|
+
* shape is declared here rather than assumed present.
|
|
58
|
+
*/
|
|
59
|
+
const nodeHost = globalThis;
|
|
60
|
+
/** Single gate for the W2.5b install-pipeline diagnostics below. */
|
|
61
|
+
function installPipelineDiagEnabled() {
|
|
62
|
+
return nodeHost.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1';
|
|
63
|
+
}
|
|
54
64
|
const INODE_ROWS_PER_SQL_EXEC = 9;
|
|
55
65
|
const CHUNK_ROWS_PER_SQL_EXEC = 33;
|
|
56
66
|
const CONTENT_IDS_PER_SQL_EXEC = 50;
|
|
57
67
|
const TRANSACTION_DURATION_SAMPLE_COUNT = 128;
|
|
58
68
|
const CONTENT_SCHEMA_MIGRATION = 'content_generations_v1';
|
|
69
|
+
/** Storage key of the shared scratch tree. `normalizeVfsPath` drops the slash. */
|
|
70
|
+
const TMP_ROOT = 'tmp';
|
|
59
71
|
export const VFS_APPEND_RECEIPT_LIMIT = 2048;
|
|
60
72
|
const INODE_KIND_FILE = 0;
|
|
61
73
|
const INODE_KIND_DIRECTORY = 1;
|
|
@@ -357,6 +369,9 @@ export class SqliteVFS {
|
|
|
357
369
|
activeStagingContentIds = new Set();
|
|
358
370
|
/** True only while durable GC work or a known abandoned staging row exists. */
|
|
359
371
|
maintenancePending = false;
|
|
372
|
+
// Keyset cursor for the orphan scan: each maintenance call reads one bounded
|
|
373
|
+
// page of distinct content ids past this bound, wrapping to '' at the end.
|
|
374
|
+
orphanScanCursor = '';
|
|
360
375
|
// Stage 2 transaction/phase telemetry. Scalar writes stay cheap; the
|
|
361
376
|
// percentile is computed from the fixed ring only when diagnostics read it.
|
|
362
377
|
_activeTransaction = null;
|
|
@@ -939,6 +954,85 @@ export class SqliteVFS {
|
|
|
939
954
|
return null;
|
|
940
955
|
return this.blobToUint8Array(rows[0].data);
|
|
941
956
|
}
|
|
957
|
+
/**
|
|
958
|
+
* Principals whose `/tmp` is private, keyed by uid, valued by the storage
|
|
959
|
+
* root their `/tmp` resolves to.
|
|
960
|
+
*
|
|
961
|
+
* `/tmp` keeps its path in every view and only the bytes behind it differ,
|
|
962
|
+
* so nothing has to be told which principal it is. The credential is the
|
|
963
|
+
* only per-process state visible where paths are RESOLVED — there is no pid
|
|
964
|
+
* and no cwd down here — so it is what the private view is keyed on.
|
|
965
|
+
*
|
|
966
|
+
* Resolution rather than a mount, because a mount diverges the two planes:
|
|
967
|
+
* the shell writing `/tmp/a` and the file API writing `/tmp/b` landed in
|
|
968
|
+
* different trees under the same name. `resolvePath` already takes `cred`,
|
|
969
|
+
* and every plane goes through it.
|
|
970
|
+
*
|
|
971
|
+
* Registration is also what makes a principal CONFINED for `chmod`. The two
|
|
972
|
+
* properties travel together because they answer one question: is this
|
|
973
|
+
* principal a guest in this filesystem. Nothing here applies to an
|
|
974
|
+
* unregistered credential, so the ordinary session user is untouched.
|
|
975
|
+
*/
|
|
976
|
+
confinedTmpRoots = new Map();
|
|
977
|
+
/**
|
|
978
|
+
* Confine a principal. `tmpRoot` is a storage key, not a logical path — the
|
|
979
|
+
* caller owns creating and chowning it, because a per-principal `chown` is
|
|
980
|
+
* uid-0 only and a guest cannot provision its own.
|
|
981
|
+
*/
|
|
982
|
+
confinePrincipal(uid, tmpRoot) {
|
|
983
|
+
const root = normalizeVfsPath(tmpRoot);
|
|
984
|
+
if (root === '')
|
|
985
|
+
throw vfsError('EINVAL', 'a private /tmp root cannot be the filesystem root');
|
|
986
|
+
this.confinedTmpRoots.set(uid, root);
|
|
987
|
+
}
|
|
988
|
+
/** Drop a confinement. A principal's `/tmp` dies with it; its home does not. */
|
|
989
|
+
releasePrincipal(uid) {
|
|
990
|
+
this.confinedTmpRoots.delete(uid);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Logical path -> storage key, for one credential.
|
|
994
|
+
*
|
|
995
|
+
* Everything under `/tmp` belongs to the caller's own private root, which is
|
|
996
|
+
* what makes the same path mean different bytes per principal. Idempotent: a
|
|
997
|
+
* key already inside that root is returned untouched, so the several methods
|
|
998
|
+
* that derive a key before handing it on cannot stack the rewrite.
|
|
999
|
+
*/
|
|
1000
|
+
storageKey(path, cred) {
|
|
1001
|
+
const key = normalizeVfsPath(path);
|
|
1002
|
+
const root = this.confinedTmpRoots.get(cred.uid);
|
|
1003
|
+
if (root === undefined)
|
|
1004
|
+
return key;
|
|
1005
|
+
if (key === root || key.startsWith(`${root}/`))
|
|
1006
|
+
return key;
|
|
1007
|
+
if (key === TMP_ROOT)
|
|
1008
|
+
return root;
|
|
1009
|
+
if (!key.startsWith(`${TMP_ROOT}/`))
|
|
1010
|
+
return key;
|
|
1011
|
+
return `${root}/${key.slice(TMP_ROOT.length + 1)}`;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Storage key -> the name this credential knows it by, or `null` when it has
|
|
1015
|
+
* none. The inverse of {@link storageKey}, for the one surface that reports
|
|
1016
|
+
* paths it was not asked about: {@link list}.
|
|
1017
|
+
*
|
|
1018
|
+
* A confined caller has no name for the shared scratch tree — `/tmp` is its
|
|
1019
|
+
* own root — nor for another principal's, so both answer `null` and are
|
|
1020
|
+
* omitted. An unconfined caller sees storage as it is, which is what the
|
|
1021
|
+
* kernel and the session user need.
|
|
1022
|
+
*/
|
|
1023
|
+
logicalPath(key, cred) {
|
|
1024
|
+
const root = this.confinedTmpRoots.get(cred.uid);
|
|
1025
|
+
if (root === undefined)
|
|
1026
|
+
return key;
|
|
1027
|
+
if (key === root)
|
|
1028
|
+
return TMP_ROOT;
|
|
1029
|
+
if (key.startsWith(`${root}/`))
|
|
1030
|
+
return `${TMP_ROOT}/${key.slice(root.length + 1)}`;
|
|
1031
|
+
// The SHARED scratch root has no name here — this caller's `/tmp` is its
|
|
1032
|
+
// own root, which already supplied that entry. Returning it too would
|
|
1033
|
+
// enumerate one name twice, for two different directories.
|
|
1034
|
+
return key === TMP_ROOT || key.startsWith(`${TMP_ROOT}/`) ? null : key;
|
|
1035
|
+
}
|
|
942
1036
|
// ── Filesystem operations ─────────────────────────────────────────────
|
|
943
1037
|
as(cred) {
|
|
944
1038
|
const bound = Object.freeze({
|
|
@@ -983,7 +1077,7 @@ export class SqliteVFS {
|
|
|
983
1077
|
writeBatch: (payload) => this.writeBatch(payload, bound),
|
|
984
1078
|
writeStream: (stream, options) => this.writeStream(stream, options, bound),
|
|
985
1079
|
mkdirBatch: (paths) => this.mkdirBatch(paths, bound),
|
|
986
|
-
revision: (path) => this.revision(path),
|
|
1080
|
+
revision: (path) => this.revision(path, bound),
|
|
987
1081
|
epoch: this._epoch,
|
|
988
1082
|
};
|
|
989
1083
|
}
|
|
@@ -1007,7 +1101,7 @@ export class SqliteVFS {
|
|
|
1007
1101
|
return (granted & requested) === requested;
|
|
1008
1102
|
}
|
|
1009
1103
|
resolvePath(path, cred, followLeaf, allowMissing) {
|
|
1010
|
-
let current =
|
|
1104
|
+
let current = this.storageKey(path, cred);
|
|
1011
1105
|
const seen = new Set();
|
|
1012
1106
|
for (let hops = 0; hops <= 40; hops++) {
|
|
1013
1107
|
const parts = current.split('/').filter(Boolean);
|
|
@@ -1028,8 +1122,13 @@ export class SqliteVFS {
|
|
|
1028
1122
|
seen.add(prefix);
|
|
1029
1123
|
const target = dec.decode(this.readInodeBytes(prefix, inode));
|
|
1030
1124
|
const suffix = parts.slice(index + 1).join('/');
|
|
1125
|
+
// An ABSOLUTE target is a logical path the same way the caller's
|
|
1126
|
+
// was, so it goes through the same rewrite: otherwise a symlink to
|
|
1127
|
+
// /tmp/y stored inside a private tree would read the shared one,
|
|
1128
|
+
// which is the one way out of the confinement. A RELATIVE target
|
|
1129
|
+
// resolves against a key that is already private, so it stays there.
|
|
1031
1130
|
const resolvedTarget = target.startsWith('/')
|
|
1032
|
-
?
|
|
1131
|
+
? this.storageKey(target, cred)
|
|
1033
1132
|
: normalizeVfsPath(`${this.parentPath(prefix)}/${target}`);
|
|
1034
1133
|
current = suffix ? normalizeVfsPath(`${resolvedTarget}/${suffix}`) : resolvedTarget;
|
|
1035
1134
|
restarted = true;
|
|
@@ -1068,6 +1167,29 @@ export class SqliteVFS {
|
|
|
1068
1167
|
if (resolved.inode?.kind !== 'directory')
|
|
1069
1168
|
throw vfsError('ENOTDIR', parent);
|
|
1070
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* POSIX sticky-bit restriction on a shared directory.
|
|
1172
|
+
*
|
|
1173
|
+
* Write permission on a directory is normally enough to remove or rename
|
|
1174
|
+
* anything inside it, which is why `/tmp` is `1777` and not `0777`: the
|
|
1175
|
+
* sticky bit narrows that to the entry's owner, the directory's owner, and
|
|
1176
|
+
* root. Nothing enforced it here, so a world-writable shared directory gave
|
|
1177
|
+
* every principal the ability to delete every other principal's files —
|
|
1178
|
+
* the mode said one thing and the filesystem did another.
|
|
1179
|
+
*/
|
|
1180
|
+
checkStickyParentMutation(path, inode, cred) {
|
|
1181
|
+
if (cred.uid === 0)
|
|
1182
|
+
return;
|
|
1183
|
+
const parent = this.parentPath(normalizeVfsPath(path));
|
|
1184
|
+
if (parent === '')
|
|
1185
|
+
return;
|
|
1186
|
+
const parentInode = this.inodes.get(parent);
|
|
1187
|
+
if (!parentInode || (parentInode.mode & 0o1000) === 0)
|
|
1188
|
+
return;
|
|
1189
|
+
if (cred.uid !== parentInode.uid && cred.uid !== inode.uid) {
|
|
1190
|
+
throw vfsError('EPERM', normalizeVfsPath(path));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1071
1193
|
/**
|
|
1072
1194
|
* Shared resolver for the boolean probes (exists/isDirectory/isFile/
|
|
1073
1195
|
* isSymlink). Resolution-structure failures — a missing or non-directory
|
|
@@ -1105,10 +1227,12 @@ export class SqliteVFS {
|
|
|
1105
1227
|
* under it changed in this DO lifetime). `revision('')` equals the
|
|
1106
1228
|
* global clock by construction (every mutation stamps all ancestors).
|
|
1107
1229
|
*/
|
|
1108
|
-
revision(path) {
|
|
1230
|
+
revision(path, cred) {
|
|
1109
1231
|
if (path === undefined)
|
|
1110
1232
|
return this._revision;
|
|
1111
|
-
|
|
1233
|
+
// A credentialed caller asks about its own view; a confined one asking
|
|
1234
|
+
// after /tmp/x means its own file, so the counter must be that file's.
|
|
1235
|
+
const p = cred === undefined ? normalizeVfsPath(path) : this.storageKey(path, cred);
|
|
1112
1236
|
if (p === '')
|
|
1113
1237
|
return this._revision;
|
|
1114
1238
|
return this._pathRevisions.get(p) ?? 0;
|
|
@@ -1288,7 +1412,7 @@ export class SqliteVFS {
|
|
|
1288
1412
|
}
|
|
1289
1413
|
}
|
|
1290
1414
|
mkdir(path, options, cred) {
|
|
1291
|
-
const normalized =
|
|
1415
|
+
const normalized = this.storageKey(path, cred);
|
|
1292
1416
|
this.assertMutationsAllowed([normalized]);
|
|
1293
1417
|
if (this.exists(normalized, cred))
|
|
1294
1418
|
return;
|
|
@@ -1389,7 +1513,7 @@ export class SqliteVFS {
|
|
|
1389
1513
|
}
|
|
1390
1514
|
symlink(target, path, cred) {
|
|
1391
1515
|
this.assertMutationsAllowed([path]);
|
|
1392
|
-
const normalized =
|
|
1516
|
+
const normalized = this.storageKey(path, cred);
|
|
1393
1517
|
const prior = this.checkAccess(normalized, 0, cred, { followLeaf: false, allowMissingLeaf: true });
|
|
1394
1518
|
if (prior.inode)
|
|
1395
1519
|
throw vfsError('EEXIST', normalized);
|
|
@@ -2124,6 +2248,22 @@ export class SqliteVFS {
|
|
|
2124
2248
|
throw vfsError('ENOENT', path);
|
|
2125
2249
|
if (cred.uid !== 0 && cred.uid !== inode.uid)
|
|
2126
2250
|
throw vfsError('EPERM', resolved.path);
|
|
2251
|
+
// A confined principal owns its own triad and nothing else. Refusing chmod
|
|
2252
|
+
// outright would be simpler and wrong: execution is gated on the x bit, so
|
|
2253
|
+
// a guest that writes build.sh and cannot chmod it cannot run it. What
|
|
2254
|
+
// must not happen is WIDENING past its own principal, so the group, other
|
|
2255
|
+
// and setuid/setgid/sticky bits keep the value they were provisioned with
|
|
2256
|
+
// while the owner triad moves freely. `u+x` works; `+x` and `777` do not.
|
|
2257
|
+
//
|
|
2258
|
+
// Refused, never clamped. Quietly narrowing a mutation to the part that
|
|
2259
|
+
// was allowed reports success for something other than what was asked, so
|
|
2260
|
+
// the refusal names the spelling that works instead.
|
|
2261
|
+
if (cred.uid !== 0 && this.confinedTmpRoots.has(cred.uid)) {
|
|
2262
|
+
const beyondOwner = (mode & 0o7777) & ~0o700;
|
|
2263
|
+
if (beyondOwner !== ((inode.mode & 0o7777) & ~0o700)) {
|
|
2264
|
+
throw vfsError('EPERM', `${resolved.path}: mode change would grant permission outside your own principal; use u+x`);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2127
2267
|
this.assertMutationsAllowed([inode.path]);
|
|
2128
2268
|
const full = inodeTypeBits(inode.kind) | (mode & 0o7777);
|
|
2129
2269
|
inode.mode = full;
|
|
@@ -2183,7 +2323,7 @@ export class SqliteVFS {
|
|
|
2183
2323
|
// Before the walk, deliberately — see VfsListPage.
|
|
2184
2324
|
const epoch = this._epoch;
|
|
2185
2325
|
const rev = this._revision;
|
|
2186
|
-
const from = after === null || after === undefined ? '' :
|
|
2326
|
+
const from = after === null || after === undefined ? '' : this.storageKey(after, cred);
|
|
2187
2327
|
// this.inodes is insertion-ordered, not path-ordered, so the sort is what
|
|
2188
2328
|
// makes `after` a resume key at all. Paid once per page against a map the
|
|
2189
2329
|
// DO already holds whole.
|
|
@@ -2210,8 +2350,16 @@ export class SqliteVFS {
|
|
|
2210
2350
|
catch {
|
|
2211
2351
|
continue;
|
|
2212
2352
|
}
|
|
2353
|
+
// Reported in the caller's OWN path space. Enumerating raw storage keys
|
|
2354
|
+
// would name a private root the caller cannot address and does not know
|
|
2355
|
+
// it has, and a caller feeding such a path back would be asking about
|
|
2356
|
+
// someone else's tree. `null` means the path has no name for this
|
|
2357
|
+
// caller, which is the same OMIT the access check above performs.
|
|
2358
|
+
const logical = this.logicalPath(path, cred);
|
|
2359
|
+
if (logical === null)
|
|
2360
|
+
continue;
|
|
2213
2361
|
entries.push({
|
|
2214
|
-
path,
|
|
2362
|
+
path: logical,
|
|
2215
2363
|
kind: inode.kind,
|
|
2216
2364
|
size: inode.size,
|
|
2217
2365
|
rev: this._pathRevisions.get(path) ?? 0,
|
|
@@ -2220,7 +2368,7 @@ export class SqliteVFS {
|
|
|
2220
2368
|
return { epoch, rev, entries, next };
|
|
2221
2369
|
}
|
|
2222
2370
|
readdir(path, cred) {
|
|
2223
|
-
const np =
|
|
2371
|
+
const np = this.storageKey(path, cred);
|
|
2224
2372
|
const resolved = np ? this.checkAccess(np, 0o4, cred) : { path: '', inode: undefined };
|
|
2225
2373
|
const inode = resolved.inode;
|
|
2226
2374
|
if (inode && inode.kind !== 'directory')
|
|
@@ -2229,7 +2377,7 @@ export class SqliteVFS {
|
|
|
2229
2377
|
if (!kids) {
|
|
2230
2378
|
// W2.5b diagnostic: empty children-set for a directory we expected
|
|
2231
2379
|
// to be populated.
|
|
2232
|
-
if (
|
|
2380
|
+
if (installPipelineDiagEnabled()) {
|
|
2233
2381
|
// eslint-disable-next-line no-console
|
|
2234
2382
|
console.warn('[sqlite-vfs/W2.5b] readdir miss path=' + np +
|
|
2235
2383
|
' kidsUndefined=true inodeExists=' + this.inodes.has(np));
|
|
@@ -2248,7 +2396,7 @@ export class SqliteVFS {
|
|
|
2248
2396
|
// fewer (some entries' inodes are missing from this.inodes), log it.
|
|
2249
2397
|
// This distinguishes (a) "children index broken" from (b) "inodes
|
|
2250
2398
|
// map lost entries".
|
|
2251
|
-
if (
|
|
2399
|
+
if (installPipelineDiagEnabled() &&
|
|
2252
2400
|
kids.size !== results.length) {
|
|
2253
2401
|
// eslint-disable-next-line no-console
|
|
2254
2402
|
console.warn('[sqlite-vfs/W2.5b] readdir size mismatch path=' + np +
|
|
@@ -2273,23 +2421,25 @@ export class SqliteVFS {
|
|
|
2273
2421
|
if (!inode)
|
|
2274
2422
|
throw vfsError('ENOENT', path);
|
|
2275
2423
|
this.checkParentAccess(resolved.path, cred);
|
|
2424
|
+
this.checkStickyParentMutation(resolved.path, inode, cred);
|
|
2276
2425
|
if (inode.isDir)
|
|
2277
2426
|
throw vfsError('EISDIR', resolved.path);
|
|
2278
2427
|
this.writeBatch({ inodes: [], chunks: [], deletePaths: [resolved.path] }, cred);
|
|
2279
2428
|
}
|
|
2280
2429
|
rmdir(path, cred) {
|
|
2281
2430
|
this.assertMutationsAllowed([path]);
|
|
2282
|
-
const np =
|
|
2431
|
+
const np = this.storageKey(path, cred);
|
|
2283
2432
|
const resolved = this.checkAccess(np, 0, cred, { followLeaf: false });
|
|
2284
2433
|
this.checkParentAccess(resolved.path, cred);
|
|
2434
|
+
const inode = this.inodes.get(np);
|
|
2435
|
+
if (!inode)
|
|
2436
|
+
throw vfsError('ENOENT', path);
|
|
2437
|
+
this.checkStickyParentMutation(resolved.path, inode, cred);
|
|
2285
2438
|
// Check if empty using children index (O(1) instead of O(N))
|
|
2286
2439
|
const kids = this.children.get(np);
|
|
2287
2440
|
if (kids && kids.size > 0) {
|
|
2288
2441
|
throw vfsError('ENOTEMPTY', path);
|
|
2289
2442
|
}
|
|
2290
|
-
const inode = this.inodes.get(np);
|
|
2291
|
-
if (!inode)
|
|
2292
|
-
throw vfsError('ENOENT', path);
|
|
2293
2443
|
if (!inode.isDir)
|
|
2294
2444
|
throw vfsError('ENOTDIR', path);
|
|
2295
2445
|
this.writeBatch({ inodes: [], chunks: [], deletePaths: [np] }, cred);
|
|
@@ -2316,6 +2466,7 @@ export class SqliteVFS {
|
|
|
2316
2466
|
if (!resolved.inode)
|
|
2317
2467
|
throw vfsError('ENOENT', normalizeVfsPath(path));
|
|
2318
2468
|
this.checkParentAccess(resolved.path, cred);
|
|
2469
|
+
this.checkStickyParentMutation(resolved.path, resolved.inode, cred);
|
|
2319
2470
|
const removable = this.collectSubtreeInodes([resolved.path]);
|
|
2320
2471
|
// The directories the recursive walk used to enumerate are exactly the
|
|
2321
2472
|
// directory inodes of the subtree, and enumerating one needed read
|
|
@@ -2367,6 +2518,7 @@ export class SqliteVFS {
|
|
|
2367
2518
|
const inode = this.inodes.get(oldPath);
|
|
2368
2519
|
if (!inode)
|
|
2369
2520
|
throw new Error("ENOENT: " + oldPath);
|
|
2521
|
+
this.checkStickyParentMutation(oldPath, inode, cred);
|
|
2370
2522
|
// W-3 (WASI filesystem WASI): if newPath already exists, unlink it first so the
|
|
2371
2523
|
// SQL UPDATE doesn't conflict on inodes.path uniqueness. POSIX rename(2)
|
|
2372
2524
|
// overwrites the destination atomically; clang's atomic-write pattern
|
|
@@ -2376,6 +2528,7 @@ export class SqliteVFS {
|
|
|
2376
2528
|
// (the latter is rare but POSIX permits it for empty dirs).
|
|
2377
2529
|
const destInode = this.inodes.get(newPath);
|
|
2378
2530
|
if (destInode) {
|
|
2531
|
+
this.checkStickyParentMutation(newPath, destInode, cred);
|
|
2379
2532
|
if (destInode.isDir) {
|
|
2380
2533
|
// POSIX semantics: rename onto a non-empty dir is an error
|
|
2381
2534
|
// (ENOTEMPTY); rename onto an empty dir is allowed. We surface
|
|
@@ -2590,14 +2743,14 @@ export class SqliteVFS {
|
|
|
2590
2743
|
}
|
|
2591
2744
|
// ── Batch write (npm install fast path) ───────────────────────────────
|
|
2592
2745
|
normalizeBatchInode(entry, cred) {
|
|
2593
|
-
const path =
|
|
2746
|
+
const path = this.storageKey(entry.path, cred);
|
|
2594
2747
|
const prior = this.inodes.get(path);
|
|
2595
2748
|
const newUid = cred.uid === 0 ? (entry.uid ?? 1000) : cred.uid;
|
|
2596
2749
|
const newGid = cred.uid === 0 ? (entry.gid ?? 1000) : cred.gid;
|
|
2597
2750
|
return {
|
|
2598
2751
|
...entry,
|
|
2599
2752
|
path,
|
|
2600
|
-
parentPath:
|
|
2753
|
+
parentPath: this.storageKey(entry.parentPath, cred),
|
|
2601
2754
|
mode: prior
|
|
2602
2755
|
? prior.mode
|
|
2603
2756
|
: inodeKind(entry) === 'symlink'
|
|
@@ -2632,7 +2785,7 @@ export class SqliteVFS {
|
|
|
2632
2785
|
}
|
|
2633
2786
|
};
|
|
2634
2787
|
for (const path of payload.deletePaths ?? []) {
|
|
2635
|
-
const normalized =
|
|
2788
|
+
const normalized = this.storageKey(path, cred);
|
|
2636
2789
|
const existing = this.checkAccess(normalized, 0, cred, {
|
|
2637
2790
|
followLeaf: false,
|
|
2638
2791
|
allowMissingLeaf: true,
|
|
@@ -2648,14 +2801,14 @@ export class SqliteVFS {
|
|
|
2648
2801
|
checkParent(entry.path);
|
|
2649
2802
|
}
|
|
2650
2803
|
for (const chunk of payload.chunks) {
|
|
2651
|
-
const path =
|
|
2804
|
+
const path = this.storageKey(chunk.path, cred);
|
|
2652
2805
|
if (!pending.has(path))
|
|
2653
2806
|
this.checkAccess(path, 0o2, cred, { followLeaf: false });
|
|
2654
2807
|
}
|
|
2655
2808
|
return {
|
|
2656
2809
|
...payload,
|
|
2657
2810
|
inodes,
|
|
2658
|
-
deletePaths: payload.deletePaths?.map(
|
|
2811
|
+
deletePaths: payload.deletePaths?.map((path) => this.storageKey(path, cred)),
|
|
2659
2812
|
};
|
|
2660
2813
|
}
|
|
2661
2814
|
/**
|
|
@@ -3307,18 +3460,30 @@ export class SqliteVFS {
|
|
|
3307
3460
|
let orphanScanComplete = true;
|
|
3308
3461
|
let stagingScanComplete = true;
|
|
3309
3462
|
if (transactions < maximum) {
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3463
|
+
// The page is selected before the orphan predicates so a scan that finds
|
|
3464
|
+
// nothing still reads only one page, not the whole covering index.
|
|
3465
|
+
const pageRows = [...this.sql.exec(`SELECT page.content_id AS content_id,
|
|
3466
|
+
(NOT EXISTS (
|
|
3467
|
+
SELECT 1 FROM content_lifecycle AS lifecycle
|
|
3468
|
+
WHERE lifecycle.content_id = page.content_id
|
|
3469
|
+
)
|
|
3470
|
+
AND ${sqlNoInodeContentReference('page.content_id')}) AS orphaned
|
|
3471
|
+
FROM (
|
|
3472
|
+
SELECT chunks.content_id
|
|
3473
|
+
FROM file_chunks AS chunks
|
|
3474
|
+
WHERE chunks.content_id > ?
|
|
3475
|
+
GROUP BY chunks.content_id
|
|
3476
|
+
ORDER BY chunks.content_id
|
|
3477
|
+
LIMIT ?
|
|
3478
|
+
) AS page
|
|
3479
|
+
ORDER BY page.content_id`, this.orphanScanCursor, CONTENT_IDS_PER_SQL_EXEC)];
|
|
3480
|
+
orphanScanComplete = pageRows.length < CONTENT_IDS_PER_SQL_EXEC;
|
|
3481
|
+
this.orphanScanCursor = orphanScanComplete
|
|
3482
|
+
? ''
|
|
3483
|
+
: String(pageRows[pageRows.length - 1].content_id);
|
|
3484
|
+
const contentIds = pageRows
|
|
3485
|
+
.filter((row) => Number(row.orphaned) === 1)
|
|
3486
|
+
.map((row) => String(row.content_id));
|
|
3322
3487
|
if (contentIds.length > 0) {
|
|
3323
3488
|
const candidates = contentIds
|
|
3324
3489
|
.map((_, index) => index === 0 ? 'SELECT ? AS content_id' : 'SELECT ?')
|
|
@@ -3480,7 +3645,7 @@ export class SqliteVFS {
|
|
|
3480
3645
|
/** Best-effort process.memoryUsage().heapUsed; 0 in DO contexts. */
|
|
3481
3646
|
_safeHeapUsed() {
|
|
3482
3647
|
try {
|
|
3483
|
-
const mu =
|
|
3648
|
+
const mu = nodeHost.process?.memoryUsage?.();
|
|
3484
3649
|
return Number(mu?.heapUsed) || 0;
|
|
3485
3650
|
}
|
|
3486
3651
|
catch {
|
|
@@ -3658,7 +3823,7 @@ export class SqliteVFS {
|
|
|
3658
3823
|
// `_addToChildrenIndex` uses Set.add so repeated calls are idempotent;
|
|
3659
3824
|
// gating it on `prior === undefined` was the bug. Counters remain
|
|
3660
3825
|
// gated correctly so they don't double-count.
|
|
3661
|
-
const __diag = (
|
|
3826
|
+
const __diag = installPipelineDiagEnabled();
|
|
3662
3827
|
const replacedPaths = new Set();
|
|
3663
3828
|
for (const entry of plan.inodes) {
|
|
3664
3829
|
const prior = this.inodes.get(entry.path);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Backend-agnostic half of Nimbus
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Backend-agnostic half of Nimbus — the durable filesystem, the shell, and the OS/process contracts, over a host-supplied SQL port.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"filesystem",
|
|
7
7
|
"shell",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"typecheck": "tsc --noEmit"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@nimbus-sh/platform": "^0.1.0",
|
|
58
59
|
"@renovatebot/pep440": "^5.0.0",
|
|
59
60
|
"acorn": "^8.15.0",
|
|
60
61
|
"acorn-walk": "^8.3.5",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* L4 — registry.npmjs.org origin ~100-300 ms cross-region
|
|
12
12
|
*
|
|
13
13
|
* Pre-cache metrics support: hit/miss counters lived in
|
|
14
|
-
*
|
|
14
|
+
* @nimbus-sh/platform/diag-counters.js but only tracked the RPC-call
|
|
15
15
|
* boundary, not per-tier. Those flat counters have since been removed.
|
|
16
16
|
*
|
|
17
17
|
* This module adds per-tier × per-kind counters. SAME singleton-per-
|
|
@@ -229,3 +229,29 @@ export function reset(): void {
|
|
|
229
229
|
}
|
|
230
230
|
_lastResetAt = Date.now();
|
|
231
231
|
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* cache-obs-2: fold facet-collected per-tier cache events into the
|
|
235
|
+
* DO-side cache-stats singleton. Called from installer.ts after a
|
|
236
|
+
* batch-facet / resolve-facet returns — mirrors recordR2RaceCounters
|
|
237
|
+
* (the facet collects metrics in its result and the supervisor folds
|
|
238
|
+
* them into the DO isolate).
|
|
239
|
+
*
|
|
240
|
+
* Each event has shape:
|
|
241
|
+
* { kind: 'hit', tier: 'L2'|'L3'|'L4', cacheKind: 'tarball'|'packument'|'asset', bytes: number }
|
|
242
|
+
* { kind: 'miss', tier: ..., cacheKind: ... }
|
|
243
|
+
*/
|
|
244
|
+
export type CacheStatEvent =
|
|
245
|
+
| { kind: 'hit'; tier: CacheTier; cacheKind: CacheKind; bytes: number }
|
|
246
|
+
| { kind: 'miss'; tier: CacheTier; cacheKind: CacheKind };
|
|
247
|
+
|
|
248
|
+
export function recordCacheStatEvents(events: readonly CacheStatEvent[] | undefined): void {
|
|
249
|
+
if (!events || events.length === 0) return;
|
|
250
|
+
for (const e of events) {
|
|
251
|
+
if (e.kind === 'hit') {
|
|
252
|
+
recordHit(e.tier, e.cacheKind, e.bytes);
|
|
253
|
+
} else {
|
|
254
|
+
recordMiss(e.tier, e.cacheKind);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* error-text.ts — the text a caught value carries.
|
|
3
|
+
*
|
|
4
|
+
* A `catch` binding is `unknown`: the throw site may have thrown an Error, a
|
|
5
|
+
* string, or a plain object with a `message`, and the shell and runtime paths
|
|
6
|
+
* report all three the same way. This is that report, in one place, so a
|
|
7
|
+
* command's diagnostics do not depend on which of those a facet threw.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The message a thrown value names, or the value itself when it names none.
|
|
12
|
+
* Empty and other falsy messages fall through to the value, so an
|
|
13
|
+
* `new Error('')` still reports as `Error` rather than as nothing.
|
|
14
|
+
*/
|
|
15
|
+
export function errorText(error: unknown): string {
|
|
16
|
+
if (typeof error === 'object' && error !== null && 'message' in error) {
|
|
17
|
+
const { message } = error;
|
|
18
|
+
if (message) return String(message);
|
|
19
|
+
}
|
|
20
|
+
return String(error);
|
|
21
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 1. Install-time supervisor (TS) — `src/npm-resolver.ts` re-exports the
|
|
8
8
|
* typed functions for tree resolution.
|
|
9
9
|
*
|
|
10
|
-
* 2.
|
|
10
|
+
* 2. IsolatePool isolates (JS string) — `src/loaders/pre-bundle-preamble.ts`
|
|
11
11
|
* embeds `getExportsResolverJS()` as part of the pool preamble so the
|
|
12
12
|
* pre-bundle facet uses identical resolution semantics to the supervisor.
|
|
13
13
|
*
|
|
@@ -37,6 +37,26 @@ export const DEFAULT_ESM_CONDITIONS = ['import', 'module', 'browser', 'default']
|
|
|
37
37
|
/** Default conditions for CJS runtime resolution (user-shell node). */
|
|
38
38
|
export const DEFAULT_CJS_CONDITIONS = ['require', 'node', 'default'];
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* A `package.json#exports` / `#imports` value: a target path, an ordered list
|
|
42
|
+
* of fallbacks to try in turn, or a map keyed by subpath (`"./client"`) or by
|
|
43
|
+
* condition (`"import"`) whose values are the same shape again. `null` is the
|
|
44
|
+
* spec's "this subpath is not exported" marker, and blocks fallback.
|
|
45
|
+
*/
|
|
46
|
+
export type ExportsField =
|
|
47
|
+
| string
|
|
48
|
+
| null
|
|
49
|
+
| ExportsField[]
|
|
50
|
+
| { [key: string]: ExportsField };
|
|
51
|
+
|
|
52
|
+
/** The package.json fields entry-point resolution reads. */
|
|
53
|
+
export interface ResolvablePackageJson {
|
|
54
|
+
exports?: ExportsField;
|
|
55
|
+
imports?: ExportsField;
|
|
56
|
+
main?: string;
|
|
57
|
+
module?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
/**
|
|
41
61
|
* Resolve `package.json#exports` (or `#imports`) per Node spec.
|
|
42
62
|
*
|
|
@@ -46,7 +66,7 @@ export const DEFAULT_CJS_CONDITIONS = ['require', 'node', 'default'];
|
|
|
46
66
|
* @returns Relative path target string, or null if not found / forbidden
|
|
47
67
|
*/
|
|
48
68
|
export function resolveExports(
|
|
49
|
-
exportsField:
|
|
69
|
+
exportsField: ExportsField | undefined,
|
|
50
70
|
subpath: string = '.',
|
|
51
71
|
conditions: string[] = DEFAULT_ESM_CONDITIONS,
|
|
52
72
|
): string | null {
|
|
@@ -123,7 +143,7 @@ export function resolveExports(
|
|
|
123
143
|
* array (Node spec).
|
|
124
144
|
*/
|
|
125
145
|
function resolveConditionValue(
|
|
126
|
-
target:
|
|
146
|
+
target: ExportsField | undefined,
|
|
127
147
|
conditions: string[],
|
|
128
148
|
): string | null {
|
|
129
149
|
if (target === null || target === undefined) return null;
|
|
@@ -162,7 +182,7 @@ function resolveConditionValue(
|
|
|
162
182
|
* itself (caller probes filesystem with extension-list).
|
|
163
183
|
*/
|
|
164
184
|
export function resolvePackageEntry(
|
|
165
|
-
pkg:
|
|
185
|
+
pkg: ResolvablePackageJson,
|
|
166
186
|
subpath: string = '.',
|
|
167
187
|
conditions: string[] = DEFAULT_ESM_CONDITIONS,
|
|
168
188
|
): string | null {
|
package/src/_shared/retry.ts
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* equivalent retry loops inside their isolated function bodies.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import { disposeRpcResource } from '
|
|
26
|
+
import { disposeRpcResource } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
27
|
+
import { errorText } from './error-text.js';
|
|
27
28
|
|
|
28
29
|
/** Default retry count AFTER the first attempt (3 = up to 4 total attempts). */
|
|
29
30
|
export const DEFAULT_RETRIES = 3;
|
|
@@ -179,7 +180,7 @@ export async function retryableFetch(
|
|
|
179
180
|
const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
|
|
180
181
|
opts?.onRetry?.(attempt + 1, totalRetries, delayMs, `HTTP ${resp.status}`);
|
|
181
182
|
await new Promise<void>((r) => setTimeout(r, delayMs));
|
|
182
|
-
} catch (e
|
|
183
|
+
} catch (e) {
|
|
183
184
|
if (timer) clearTimeout(timer);
|
|
184
185
|
// Network-level failure (fetch rejection, timeout via AbortError,
|
|
185
186
|
// DNS, connection reset). Retry the same way as a 5xx.
|
|
@@ -189,7 +190,8 @@ export async function retryableFetch(
|
|
|
189
190
|
throw e;
|
|
190
191
|
}
|
|
191
192
|
const delayMs = jittered(BACKOFF_MS[Math.min(attempt, BACKOFF_MS.length - 1)]);
|
|
192
|
-
const
|
|
193
|
+
const isAbort = typeof e === 'object' && e !== null && 'name' in e && e.name === 'AbortError';
|
|
194
|
+
const reason = isAbort ? 'timeout' : errorText(e);
|
|
193
195
|
opts?.onRetry?.(attempt + 1, totalRetries, delayMs, reason);
|
|
194
196
|
await new Promise<void>((r) => setTimeout(r, delayMs));
|
|
195
197
|
}
|