@nimbus-sh/core 0.5.0 → 0.7.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 +47 -26
- package/dist/_shared/byte-stream.d.ts +5 -4
- package/dist/_shared/byte-stream.d.ts.map +1 -1
- 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/exports-resolver.d.ts +1 -1
- package/dist/_shared/exports-resolver.js +1 -1
- package/dist/_shared/real-node-imports.d.ts +7 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -1
- package/dist/_shared/real-node-imports.js +8 -0
- package/dist/_shared/retry.js +1 -1
- package/dist/_shared/tarball.d.ts +23 -5
- package/dist/_shared/tarball.d.ts.map +1 -1
- package/dist/_shared/tarball.js +38 -5
- package/dist/_shared/vfs-write-ledger.d.ts.map +1 -1
- package/dist/_shared/vfs-write-ledger.js +22 -16
- package/dist/constants.d.ts +5 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +16 -56
- package/dist/runtime/assets-loader.js +1 -1
- package/dist/runtime/comment-strip.d.ts +0 -35
- package/dist/runtime/comment-strip.d.ts.map +1 -1
- package/dist/runtime/comment-strip.js +48 -20
- package/dist/runtime/esbuild-service.d.ts +38 -42
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +494 -197
- package/dist/runtime/javascript-ast.d.ts.map +1 -1
- package/dist/runtime/javascript-ast.js +45 -7
- package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-backend.js +18 -16
- package/dist/runtime/port-registry.d.ts +4 -2
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +60 -8
- package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
- package/dist/runtime/virtual-socket-kernel.generated.js +1 -1
- package/dist/shell/stdin-adapter.d.ts +11 -0
- package/dist/shell/stdin-adapter.d.ts.map +1 -0
- package/dist/shell/stdin-adapter.js +59 -0
- package/dist/shell/unix-commands.d.ts +2 -1
- package/dist/shell/unix-commands.d.ts.map +1 -1
- package/dist/shell/unix-commands.js +1078 -50
- package/dist/substrate/lifo/commands/io/dd.js +1 -1
- package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/net/curl.js +270 -54
- package/dist/substrate/lifo/commands/system/npm.d.ts +18 -1
- package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/system/npm.js +26 -43
- package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/head.js +2 -1
- package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -1
- package/dist/substrate/lifo/commands/text/sed.js +499 -117
- package/dist/substrate/lifo/commands/types.d.ts +8 -4
- package/dist/substrate/lifo/commands/types.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.d.ts +37 -18
- package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -1
- package/dist/substrate/lifo/node-compat/zlib.js +122 -23
- package/dist/substrate/lifo/shell/interpreter.d.ts +9 -1
- package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/interpreter.js +79 -29
- package/dist/substrate/lifo/shell/pipe.d.ts +16 -0
- package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/pipe.js +62 -18
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts +20 -3
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -1
- package/dist/substrate/lifo/shell/terminal-stdin.js +60 -24
- package/dist/vfs/seed-project.d.ts.map +1 -1
- package/dist/vfs/seed-project.js +1 -1
- package/dist/vfs/sqlite-vfs.d.ts +21 -30
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +113 -29
- package/dist/workspace/nimbus-workspace.d.ts +15 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
- package/dist/workspace/nimbus-workspace.js +20 -3
- package/dist/workspace/supervisor-op.d.ts +23 -0
- package/dist/workspace/supervisor-op.d.ts.map +1 -0
- package/dist/workspace/supervisor-op.js +92 -0
- package/package.json +3 -2
- package/src/_shared/byte-stream.ts +5 -4
- package/src/_shared/cache-stats.ts +27 -1
- package/src/_shared/exports-resolver.ts +1 -1
- package/src/_shared/real-node-imports.ts +8 -0
- package/src/_shared/retry.ts +1 -1
- package/src/_shared/tarball.ts +56 -5
- package/src/_shared/vfs-write-ledger.ts +22 -16
- package/src/constants.ts +30 -60
- package/src/runtime/assets-loader.ts +1 -1
- package/src/runtime/comment-strip.ts +42 -23
- package/src/runtime/esbuild-service.ts +566 -217
- package/src/runtime/javascript-ast.ts +40 -9
- package/src/runtime/opentui-wasm-backend.ts +19 -17
- package/src/runtime/port-registry.ts +66 -8
- package/src/runtime/virtual-socket-kernel.generated.ts +1 -1
- package/src/shell/stdin-adapter.ts +58 -0
- package/src/shell/unix-commands.ts +1054 -50
- package/src/substrate/lifo/commands/io/dd.ts +2 -2
- package/src/substrate/lifo/commands/net/curl.ts +310 -58
- package/src/substrate/lifo/commands/system/npm.ts +58 -38
- package/src/substrate/lifo/commands/text/head.ts +2 -1
- package/src/substrate/lifo/commands/text/sed.ts +519 -109
- package/src/substrate/lifo/commands/types.ts +8 -5
- package/src/substrate/lifo/node-compat/zlib.ts +155 -26
- package/src/substrate/lifo/shell/interpreter.ts +74 -27
- package/src/substrate/lifo/shell/pipe.ts +65 -24
- package/src/substrate/lifo/shell/terminal-stdin.ts +66 -30
- package/src/vfs/seed-project.ts +3 -3
- package/src/vfs/sqlite-vfs.ts +128 -64
- package/src/workspace/nimbus-workspace.ts +26 -2
- package/src/workspace/supervisor-op.ts +117 -0
- 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 -840
- 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 -268
- 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 -1013
- 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 -260
- package/src/observability/oom-discriminator.ts +0 -405
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import { encode } from '../utils/encoding.js';
|
|
1
2
|
import type { TerminalInputStream } from '../commands/types.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
* A bridge between terminal keyboard input and command stdin.
|
|
5
|
-
*
|
|
5
|
+
* A bridge between terminal keyboard input and command stdin. The Shell
|
|
6
|
+
* feeds lines in via feed(); commands consume bytes or text. Bytes are the
|
|
7
|
+
* storage format, so a byte read always makes progress even when maxLength
|
|
8
|
+
* splits a multi-byte code point — `dd bs=1` over `é` yields c3, then a9.
|
|
6
9
|
*/
|
|
7
10
|
export class TerminalStdin implements TerminalInputStream {
|
|
8
|
-
private buffer:
|
|
11
|
+
private buffer: Uint8Array[] = [];
|
|
9
12
|
private closed = false;
|
|
10
|
-
private resolvers: Array<(value:
|
|
13
|
+
private resolvers: Array<(value: Uint8Array | null) => void> = [];
|
|
11
14
|
private _rawMode = false;
|
|
15
|
+
private decoder = new TextDecoder('utf-8');
|
|
12
16
|
|
|
13
17
|
/** True when a command has called read() and is waiting for input. */
|
|
14
18
|
get isWaiting(): boolean {
|
|
@@ -26,16 +30,13 @@ export class TerminalStdin implements TerminalInputStream {
|
|
|
26
30
|
|
|
27
31
|
/** Shell calls this on Enter (with line + '\n'). */
|
|
28
32
|
feed(text: string): void {
|
|
29
|
-
if (this.closed) return;
|
|
30
|
-
|
|
31
|
-
this.deliver(text, 'back');
|
|
33
|
+
if (this.closed || text === '') return;
|
|
34
|
+
this.deliver(encode(text), 'back');
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
/** Shell calls this on Ctrl+D to signal EOF. */
|
|
35
38
|
close(): void {
|
|
36
|
-
if (this.closed) return;
|
|
37
39
|
this.closed = true;
|
|
38
|
-
|
|
39
40
|
while (this.resolvers.length > 0) {
|
|
40
41
|
const resolve = this.resolvers.shift()!;
|
|
41
42
|
resolve(null);
|
|
@@ -44,15 +45,15 @@ export class TerminalStdin implements TerminalInputStream {
|
|
|
44
45
|
|
|
45
46
|
/** Commands consume input. Returns null on EOF. */
|
|
46
47
|
async read(): Promise<string | null> {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
while (true) {
|
|
49
|
+
const bytes = await this.pull();
|
|
50
|
+
if (bytes === null) {
|
|
51
|
+
const tail = this.decoder.decode();
|
|
52
|
+
return tail.length > 0 ? tail : null;
|
|
53
|
+
}
|
|
54
|
+
const text = this.decoder.decode(bytes, { stream: true });
|
|
55
|
+
if (text.length > 0) return text;
|
|
52
56
|
}
|
|
53
|
-
return new Promise<string | null>((resolve) => {
|
|
54
|
-
this.resolvers.push(resolve);
|
|
55
|
-
});
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
async readLine(): Promise<string | null> {
|
|
@@ -64,7 +65,7 @@ export class TerminalStdin implements TerminalInputStream {
|
|
|
64
65
|
const newline = chunk.indexOf('\n');
|
|
65
66
|
if (newline >= 0) {
|
|
66
67
|
const rest = chunk.slice(newline + 1);
|
|
67
|
-
if (rest.length > 0) this.
|
|
68
|
+
if (rest.length > 0) this.deliverBackText(rest);
|
|
68
69
|
return line + chunk.slice(0, newline);
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -72,15 +73,21 @@ export class TerminalStdin implements TerminalInputStream {
|
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Bounded byte read: returns whatever the user has already typed, capped
|
|
78
|
+
* at maxLength. maxLength bounds the result, it is never a fill target —
|
|
79
|
+
* a command reading bytes must not stall until maxLength arrive. A larger
|
|
80
|
+
* queued chunk keeps only its first maxLength bytes; the remainder stays
|
|
81
|
+
* queued, so `dd bs=1` over `é` still yields c3, then a9.
|
|
82
|
+
*/
|
|
83
|
+
async readBytes(maxLength: number): Promise<Uint8Array | null> {
|
|
84
|
+
if (maxLength <= 0) return new Uint8Array(0);
|
|
85
|
+
|
|
86
|
+
const chunk = await this.pull();
|
|
79
87
|
if (chunk === null) return null;
|
|
80
88
|
if (chunk.length <= maxLength) return chunk;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return chunk.slice(0, maxLength);
|
|
89
|
+
this.buffer.unshift(chunk.subarray(maxLength));
|
|
90
|
+
return chunk.subarray(0, maxLength);
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
/** Read all remaining input until EOF, joined together. */
|
|
@@ -94,16 +101,45 @@ export class TerminalStdin implements TerminalInputStream {
|
|
|
94
101
|
return parts.join('');
|
|
95
102
|
}
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Text snapshot of everything queued but not yet consumed. The shell's
|
|
106
|
+
* wrap layer uses this for commands that want drained terminal input.
|
|
107
|
+
*/
|
|
108
|
+
drainBuffered(): string {
|
|
109
|
+
let text = '';
|
|
110
|
+
while (this.buffer.length > 0) {
|
|
111
|
+
text += this.decoder.decode(this.buffer.shift()!, { stream: true });
|
|
112
|
+
}
|
|
113
|
+
text += this.decoder.decode();
|
|
114
|
+
return text;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private pull(): Promise<Uint8Array | null> {
|
|
118
|
+
if (this.buffer.length > 0) {
|
|
119
|
+
return Promise.resolve(this.buffer.shift() ?? null);
|
|
120
|
+
}
|
|
121
|
+
if (this.closed) {
|
|
122
|
+
return Promise.resolve(null);
|
|
123
|
+
}
|
|
124
|
+
return new Promise<Uint8Array | null>((resolve) => {
|
|
125
|
+
this.resolvers.push(resolve);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private deliver(bytes: Uint8Array, position: 'front' | 'back'): void {
|
|
130
|
+
if (bytes.length === 0) return;
|
|
99
131
|
|
|
100
132
|
if (this.resolvers.length > 0) {
|
|
101
133
|
const resolve = this.resolvers.shift()!;
|
|
102
|
-
resolve(
|
|
134
|
+
resolve(bytes);
|
|
103
135
|
return;
|
|
104
136
|
}
|
|
105
137
|
|
|
106
|
-
if (position === 'front') this.buffer.unshift(
|
|
107
|
-
else this.buffer.push(
|
|
138
|
+
if (position === 'front') this.buffer.unshift(bytes);
|
|
139
|
+
else this.buffer.push(bytes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private deliverBackText(text: string): void {
|
|
143
|
+
this.deliver(encode(text), 'front');
|
|
108
144
|
}
|
|
109
145
|
}
|
package/src/vfs/seed-project.ts
CHANGED
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
* - Basename injection (commit 2) handles the /preview/ basepath wiring.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import type { SqliteVFS } from './sqlite-vfs.js';
|
|
21
22
|
import type {
|
|
22
|
-
SqliteVFS,
|
|
23
23
|
BatchInodeEntry,
|
|
24
24
|
BatchChunkEntry,
|
|
25
|
-
} from '
|
|
26
|
-
import { CHUNK_SIZE } from '
|
|
25
|
+
} from '@nimbus-sh/platform/w7-frame.js';
|
|
26
|
+
import { CHUNK_SIZE } from '@nimbus-sh/platform/limits.js';
|
|
27
27
|
import { enc } from '../_shared/bytes.js';
|
|
28
28
|
import { errorText } from '../_shared/error-text.js';
|
|
29
29
|
import { CRED_KERNEL } from '../runtime/os-contracts.js';
|
package/src/vfs/sqlite-vfs.ts
CHANGED
|
@@ -43,27 +43,33 @@
|
|
|
43
43
|
import { VfsEventEmitter, type VfsEventType } from './events.js';
|
|
44
44
|
import { normalizeVfsPath } from './path.js';
|
|
45
45
|
import {
|
|
46
|
-
CHUNK_SIZE,
|
|
47
46
|
LRU_MAX_ENTRIES,
|
|
48
47
|
BATCH_SIZE,
|
|
48
|
+
FS_LIST_PAGE_LIMIT,
|
|
49
|
+
} from '../constants.js';
|
|
50
|
+
import {
|
|
51
|
+
CHUNK_SIZE,
|
|
49
52
|
MAX_TX_BLOB_BYTES,
|
|
50
53
|
MAX_TX_LOGICAL_ROWS,
|
|
51
54
|
MAX_TX_SQL_EXECS,
|
|
52
55
|
MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES,
|
|
53
|
-
|
|
54
|
-
} from '
|
|
55
|
-
import {
|
|
56
|
-
import {
|
|
57
|
-
import { acquireSupervisorAllocation } from '../observability/heavy-alloc-coord.js';
|
|
56
|
+
} from '@nimbus-sh/platform/limits.js';
|
|
57
|
+
import { recordFailure } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
58
|
+
import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
59
|
+
import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
|
|
58
60
|
import { enc, dec } from '../_shared/bytes.js';
|
|
59
61
|
import {
|
|
60
62
|
decodeWriteBatchStream,
|
|
63
|
+
type BatchChunkEntry,
|
|
64
|
+
type BatchInodeEntry,
|
|
65
|
+
type BatchWritePayload,
|
|
66
|
+
type VfsInodeKind,
|
|
61
67
|
type W7DecodedRecord,
|
|
62
|
-
} from '
|
|
68
|
+
} from '@nimbus-sh/platform/w7-frame.js';
|
|
63
69
|
import {
|
|
64
70
|
WeightedCreditPool,
|
|
65
71
|
type CreditLease,
|
|
66
|
-
} from '
|
|
72
|
+
} from '@nimbus-sh/platform/weighted-credit-pool.js';
|
|
67
73
|
import { LEGACY_SYMLINK_REGISTRY_PATH } from './symlink-registry.js';
|
|
68
74
|
import {
|
|
69
75
|
CRED_KERNEL,
|
|
@@ -104,8 +110,16 @@ function installPipelineDiagEnabled(): boolean {
|
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
// ── Types ───────────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
// VfsInodeKind and the writeBatch payload types (BatchInodeEntry,
|
|
114
|
+
// BatchChunkEntry, BatchWritePayload) live with the wire format that encodes
|
|
115
|
+
// them: @nimbus-sh/platform/w7-frame.js. Forwarded here because core@0.5.0
|
|
116
|
+
// published them from this module; platform stays the definition.
|
|
117
|
+
export type {
|
|
118
|
+
BatchChunkEntry,
|
|
119
|
+
BatchInodeEntry,
|
|
120
|
+
BatchWritePayload,
|
|
121
|
+
VfsInodeKind,
|
|
122
|
+
} from '@nimbus-sh/platform/w7-frame.js';
|
|
109
123
|
|
|
110
124
|
export interface ExclusiveMutationLease {
|
|
111
125
|
readonly root: string;
|
|
@@ -133,22 +147,6 @@ interface INode {
|
|
|
133
147
|
contentId: string | null;
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
/** Entry for bulk inode creation via writeBatch(). */
|
|
137
|
-
export interface BatchInodeEntry {
|
|
138
|
-
path: string;
|
|
139
|
-
parentPath: string;
|
|
140
|
-
/** Defaults to isDir ? directory : file for legacy/non-symlink producers. */
|
|
141
|
-
kind?: VfsInodeKind;
|
|
142
|
-
isDir: boolean;
|
|
143
|
-
size: number;
|
|
144
|
-
atime?: number;
|
|
145
|
-
mtime: number;
|
|
146
|
-
mode: number;
|
|
147
|
-
uid?: number;
|
|
148
|
-
gid?: number;
|
|
149
|
-
chunkCount: number;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
150
|
export interface VfsStat {
|
|
153
151
|
type: VfsInodeKind;
|
|
154
152
|
size: number;
|
|
@@ -233,21 +231,6 @@ export interface CredentialedVfs {
|
|
|
233
231
|
readonly epoch: string;
|
|
234
232
|
}
|
|
235
233
|
|
|
236
|
-
/** Entry for bulk chunk creation via writeBatch(). */
|
|
237
|
-
export interface BatchChunkEntry {
|
|
238
|
-
path: string;
|
|
239
|
-
chunkId: number;
|
|
240
|
-
data: Uint8Array;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** Payload for writeBatch() — all inodes + chunks written in ONE transactionSync(). */
|
|
244
|
-
export interface BatchWritePayload {
|
|
245
|
-
inodes: BatchInodeEntry[];
|
|
246
|
-
chunks: BatchChunkEntry[];
|
|
247
|
-
/** Paths to delete before writing (for clean reinstall). */
|
|
248
|
-
deletePaths?: string[];
|
|
249
|
-
}
|
|
250
|
-
|
|
251
234
|
export interface WriteBatchStreamProgress {
|
|
252
235
|
/** 1-based sequence of the last durable publish group; zero means none. */
|
|
253
236
|
committedGroupSequence: number;
|
|
@@ -631,10 +614,9 @@ export class SqliteVFS {
|
|
|
631
614
|
|
|
632
615
|
// ── Running counters for O(1) getStats() (B3 / AUDIT M10 / M-S8) ──
|
|
633
616
|
// Replaces the triple scan of this.inodes on every /api/stats poll.
|
|
634
|
-
// Bootstrapped in loadInodes(); maintained at
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
// walk of this.inodes. Unit-tested in the A5/B3 runtime tests.
|
|
617
|
+
// Bootstrapped in loadInodes(); maintained at committed publication by
|
|
618
|
+
// mkdir, batch writes/deletes and rename. These match a fresh O(N)
|
|
619
|
+
// walk of this.inodes; rollback reloads them from the durable rows.
|
|
638
620
|
private _totalFiles = 0;
|
|
639
621
|
private _totalDirs = 0;
|
|
640
622
|
private _usedBytes = 0;
|
|
@@ -649,6 +631,10 @@ export class SqliteVFS {
|
|
|
649
631
|
// so unrelated writes no longer invalidate them. In-memory only — the
|
|
650
632
|
// clock resets with the DO lifetime, exactly like the caches keyed on it.
|
|
651
633
|
private _pathRevisions = new Map<string, number>();
|
|
634
|
+
private transactionPublication: {
|
|
635
|
+
paths: Set<string>;
|
|
636
|
+
events: { type: VfsEventType; path: string; oldPath?: string }[];
|
|
637
|
+
} | null = null;
|
|
652
638
|
|
|
653
639
|
// ── Invalidation log (facet cache coherence) ──────────────────────────
|
|
654
640
|
// A facet's resident set is a cache of this VFS, and it learns what to
|
|
@@ -1690,6 +1676,10 @@ export class SqliteVFS {
|
|
|
1690
1676
|
* additional coverage, since no facet view keys on a grandparent.
|
|
1691
1677
|
*/
|
|
1692
1678
|
private bumpRevision(paths: readonly string[]): void {
|
|
1679
|
+
if (this.transactionPublication) {
|
|
1680
|
+
for (const path of paths) this.transactionPublication.paths.add(path);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1693
1683
|
const rev = ++this._revision;
|
|
1694
1684
|
for (const path of paths) {
|
|
1695
1685
|
let p = normalizeVfsPath(path);
|
|
@@ -1901,7 +1891,7 @@ export class SqliteVFS {
|
|
|
1901
1891
|
this._addToChildrenIndex(pp, path);
|
|
1902
1892
|
this._totalDirs++; // B3
|
|
1903
1893
|
this.bumpRevision([path]);
|
|
1904
|
-
this.
|
|
1894
|
+
this.emitMutation('addDir', path);
|
|
1905
1895
|
}
|
|
1906
1896
|
|
|
1907
1897
|
private writeFile(
|
|
@@ -2922,11 +2912,11 @@ export class SqliteVFS {
|
|
|
2922
2912
|
}
|
|
2923
2913
|
const atime = atimeMs !== null && Number.isFinite(atimeMs) ? Math.trunc(atimeMs) : this.now();
|
|
2924
2914
|
const mtime = mtimeMs !== null && Number.isFinite(mtimeMs) ? Math.trunc(mtimeMs) : this.now();
|
|
2915
|
+
this.sql.exec("UPDATE inodes SET atime = ?, mtime = ? WHERE path = ?", atime, mtime, inode.path);
|
|
2925
2916
|
inode.atime = atime;
|
|
2926
2917
|
inode.mtime = mtime;
|
|
2927
|
-
this.sql.exec("UPDATE inodes SET atime = ?, mtime = ? WHERE path = ?", atime, mtime, inode.path);
|
|
2928
2918
|
this.bumpRevision([inode.path]);
|
|
2929
|
-
this.
|
|
2919
|
+
this.emitMutation('change', inode.path);
|
|
2930
2920
|
}
|
|
2931
2921
|
|
|
2932
2922
|
/**
|
|
@@ -2966,10 +2956,10 @@ export class SqliteVFS {
|
|
|
2966
2956
|
}
|
|
2967
2957
|
this.assertMutationsAllowed([inode.path]);
|
|
2968
2958
|
const full = inodeTypeBits(inode.kind) | (mode & 0o7777);
|
|
2969
|
-
inode.mode = full;
|
|
2970
2959
|
this.sql.exec("UPDATE inodes SET mode = ? WHERE path = ?", full, inode.path);
|
|
2960
|
+
inode.mode = full;
|
|
2971
2961
|
this.bumpRevision([inode.path]);
|
|
2972
|
-
this.
|
|
2962
|
+
this.emitMutation('change', inode.path);
|
|
2973
2963
|
}
|
|
2974
2964
|
|
|
2975
2965
|
private chown(
|
|
@@ -2992,18 +2982,21 @@ export class SqliteVFS {
|
|
|
2992
2982
|
}
|
|
2993
2983
|
if (gid !== null && gid === inode.gid && cred.uid !== 0 && !owner) throw vfsError('EPERM', resolved.path);
|
|
2994
2984
|
this.assertMutationsAllowed([inode.path]);
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2985
|
+
const nextUid = uid ?? inode.uid;
|
|
2986
|
+
const nextGid = gid ?? inode.gid;
|
|
2987
|
+
const nextMode = cred.uid === 0 ? inode.mode : inode.mode & ~0o6000;
|
|
2998
2988
|
this.sql.exec(
|
|
2999
2989
|
'UPDATE inodes SET uid = ?, gid = ?, mode = ? WHERE path = ?',
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
2990
|
+
nextUid,
|
|
2991
|
+
nextGid,
|
|
2992
|
+
nextMode,
|
|
3003
2993
|
inode.path,
|
|
3004
2994
|
);
|
|
2995
|
+
inode.uid = nextUid;
|
|
2996
|
+
inode.gid = nextGid;
|
|
2997
|
+
inode.mode = nextMode;
|
|
3005
2998
|
this.bumpRevision([inode.path]);
|
|
3006
|
-
this.
|
|
2999
|
+
this.emitMutation('change', inode.path);
|
|
3007
3000
|
}
|
|
3008
3001
|
|
|
3009
3002
|
/**
|
|
@@ -3216,15 +3209,19 @@ export class SqliteVFS {
|
|
|
3216
3209
|
|
|
3217
3210
|
private rename(oldPath: string, newPath: string, cred: VfsCred): void {
|
|
3218
3211
|
this.assertMutationsAllowed([oldPath, newPath]);
|
|
3219
|
-
|
|
3212
|
+
// Resolve private /tmp names to storage keys before reading or mutating.
|
|
3213
|
+
const source = this.checkAccess(oldPath, 0, cred, { followLeaf: false });
|
|
3214
|
+
const inode = source.inode;
|
|
3215
|
+
if (!inode) throw vfsError('ENOENT', oldPath);
|
|
3216
|
+
const target = this.checkAccess(newPath, 0, cred, { followLeaf: false, allowMissingLeaf: true });
|
|
3217
|
+
oldPath = source.path;
|
|
3218
|
+
newPath = target.path;
|
|
3220
3219
|
this.checkParentAccess(oldPath, cred);
|
|
3221
3220
|
this.checkParentAccess(newPath, cred);
|
|
3222
3221
|
if (oldPath === newPath) return;
|
|
3223
3222
|
if (newPath.startsWith(`${oldPath}/`)) {
|
|
3224
3223
|
throw new Error(`EINVAL: cannot move ${oldPath} inside itself`);
|
|
3225
3224
|
}
|
|
3226
|
-
const inode = this.inodes.get(oldPath);
|
|
3227
|
-
if (!inode) throw new Error("ENOENT: " + oldPath);
|
|
3228
3225
|
this.checkStickyParentMutation(oldPath, inode, cred);
|
|
3229
3226
|
|
|
3230
3227
|
// W-3 (WASI filesystem WASI): if newPath already exists, unlink it first so the
|
|
@@ -3234,7 +3231,7 @@ export class SqliteVFS {
|
|
|
3234
3231
|
// a 2nd `make` after a prior successful build throws on UPDATE
|
|
3235
3232
|
// failure. Pre-unlink covers both file-over-file and file-over-dir
|
|
3236
3233
|
// (the latter is rare but POSIX permits it for empty dirs).
|
|
3237
|
-
const destInode =
|
|
3234
|
+
const destInode = target.inode;
|
|
3238
3235
|
if (destInode) {
|
|
3239
3236
|
this.checkStickyParentMutation(newPath, destInode, cred);
|
|
3240
3237
|
if (destInode.isDir) {
|
|
@@ -3398,7 +3395,7 @@ export class SqliteVFS {
|
|
|
3398
3395
|
}
|
|
3399
3396
|
|
|
3400
3397
|
this.bumpRevision([...touchedPaths]);
|
|
3401
|
-
this.
|
|
3398
|
+
this.emitMutation('rename', newPath, oldPath);
|
|
3402
3399
|
this.runContentMaintenanceSafely(1);
|
|
3403
3400
|
}
|
|
3404
3401
|
|
|
@@ -4097,6 +4094,72 @@ export class SqliteVFS {
|
|
|
4097
4094
|
return this.errorMessage(error).toUpperCase().includes('SQLITE_NOMEM');
|
|
4098
4095
|
}
|
|
4099
4096
|
|
|
4097
|
+
/**
|
|
4098
|
+
* Participate in an embedder's synchronous transaction: VFS writes and SQL
|
|
4099
|
+
* issued by callback commit together; on rollback the in-memory mirror
|
|
4100
|
+
* returns to the committed state before the error is rethrown with cause.
|
|
4101
|
+
*
|
|
4102
|
+
* This method MUST own the outermost transaction on this VFS's SQL host;
|
|
4103
|
+
* use it instead of wrapping VFS calls in storage.transactionSync. Do not
|
|
4104
|
+
* nest it, return a Promise/thenable, or start asynchronous work inside it.
|
|
4105
|
+
* The callback may read its writes. Revisions and events publish only on
|
|
4106
|
+
* commit, once for the combined mutation. Existing credential checks apply.
|
|
4107
|
+
*
|
|
4108
|
+
* Inodes are an always-resident cache (absence means ENOENT), so rollback
|
|
4109
|
+
* discards cached chunks and rebuilds metadata/children/counters from SQL.
|
|
4110
|
+
* No inode snapshot or undo log is retained. Recovery failure is surfaced
|
|
4111
|
+
* with both errors; the embedder must discard this VFS in that case.
|
|
4112
|
+
*/
|
|
4113
|
+
withTransaction<T>(callback: () => T): T {
|
|
4114
|
+
if (this.transactionPublication !== null) {
|
|
4115
|
+
throw new Error('[sqlite-vfs] nested embedder transactions are not supported');
|
|
4116
|
+
}
|
|
4117
|
+
const storage = this.ctx.storage;
|
|
4118
|
+
if (!storage) throw new Error('[sqlite-vfs] atomic storage operation requires transactionSync');
|
|
4119
|
+
const publication = {
|
|
4120
|
+
paths: new Set<string>(),
|
|
4121
|
+
events: new Array<{ type: VfsEventType; path: string; oldPath?: string }>(),
|
|
4122
|
+
};
|
|
4123
|
+
const maintenancePending = this.maintenancePending;
|
|
4124
|
+
this.transactionPublication = publication;
|
|
4125
|
+
let result: T;
|
|
4126
|
+
try {
|
|
4127
|
+
result = storage.transactionSync(() => {
|
|
4128
|
+
const value = callback();
|
|
4129
|
+
if (value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
4130
|
+
&& 'then' in value && typeof value.then === 'function') {
|
|
4131
|
+
throw new TypeError('[sqlite-vfs] transaction callback must be synchronous');
|
|
4132
|
+
}
|
|
4133
|
+
return value;
|
|
4134
|
+
});
|
|
4135
|
+
} catch (error) {
|
|
4136
|
+
this.evictAll();
|
|
4137
|
+
this.maintenancePending = maintenancePending;
|
|
4138
|
+
try {
|
|
4139
|
+
this.loadInodes();
|
|
4140
|
+
} catch (reloadError) {
|
|
4141
|
+
throw new AggregateError([error, reloadError], '[sqlite-vfs] transaction rollback reload failed', { cause: error });
|
|
4142
|
+
}
|
|
4143
|
+
throw new Error('[sqlite-vfs] embedder transaction rolled back', { cause: error });
|
|
4144
|
+
} finally {
|
|
4145
|
+
this.transactionPublication = null;
|
|
4146
|
+
}
|
|
4147
|
+
if (publication.paths.size > 0) this.bumpRevision([...publication.paths]);
|
|
4148
|
+
for (const event of publication.events) {
|
|
4149
|
+
this.events.emit(event.type, event.path, event.oldPath);
|
|
4150
|
+
}
|
|
4151
|
+
this.runContentMaintenanceSafely(1);
|
|
4152
|
+
return result;
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
private emitMutation(type: VfsEventType, path: string, oldPath?: string): void {
|
|
4156
|
+
if (this.transactionPublication) {
|
|
4157
|
+
this.transactionPublication.events.push({ type, path, oldPath });
|
|
4158
|
+
} else {
|
|
4159
|
+
this.events.emit(type, path, oldPath);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4100
4163
|
private transactionSync(callback: () => void): void {
|
|
4101
4164
|
if (!this.ctx?.storage?.transactionSync) {
|
|
4102
4165
|
throw new Error('[sqlite-vfs] atomic storage operation requires transactionSync');
|
|
@@ -4465,6 +4528,7 @@ export class SqliteVFS {
|
|
|
4465
4528
|
}
|
|
4466
4529
|
|
|
4467
4530
|
private runContentMaintenanceSafely(maxTransactions: number, force = false): void {
|
|
4531
|
+
if (this.transactionPublication) return;
|
|
4468
4532
|
if (!force && !this.maintenancePending) return;
|
|
4469
4533
|
const startedAt = performance.now();
|
|
4470
4534
|
try {
|
|
@@ -4790,10 +4854,10 @@ export class SqliteVFS {
|
|
|
4790
4854
|
|
|
4791
4855
|
// 5. Events observe the already-published metadata and revision.
|
|
4792
4856
|
for (const inode of deletedInodes) {
|
|
4793
|
-
this.
|
|
4857
|
+
this.emitMutation(inode.isDir ? 'unlinkDir' : 'unlink', inode.path);
|
|
4794
4858
|
}
|
|
4795
4859
|
for (const entry of plan.inodes) {
|
|
4796
|
-
this.
|
|
4860
|
+
this.emitMutation(entry.isDir ? 'addDir' : replacedPaths.has(entry.path) ? 'change' : 'add', entry.path);
|
|
4797
4861
|
}
|
|
4798
4862
|
|
|
4799
4863
|
this.recordDuration(this._postCommitDuration, performance.now() - postCommitStartedAt);
|
|
@@ -48,6 +48,8 @@ import { seedRuntimePackage, type RuntimePackage } from '../runtime/runtime-pack
|
|
|
48
48
|
import type { EsbuildService } from '../runtime/esbuild-service.js';
|
|
49
49
|
import { registerUnixCommands } from '../shell/unix-commands.js';
|
|
50
50
|
import { installPathExecResolver } from '../shell/exec-dispatch.js';
|
|
51
|
+
import { adoptCtxExports, composeFabric, type CtxExports, type FabricComposition } from '@nimbus-sh/platform/composition.js';
|
|
52
|
+
import { createSupervisorOpHandler, type SupervisorOpEnvelope, type SupervisorOpHandler } from './supervisor-op.js';
|
|
51
53
|
|
|
52
54
|
export interface NimbusWorkspaceOptions {
|
|
53
55
|
/** The host's SQLite. In a Durable Object: `ctx.storage.sql`. */
|
|
@@ -126,6 +128,15 @@ export interface NimbusWorkspaceOptions {
|
|
|
126
128
|
* carry REPLs and a resident-process substrate this cannot reach.
|
|
127
129
|
*/
|
|
128
130
|
readonly facets?: FacetHost;
|
|
131
|
+
/** Isolate-wide fabric composition, including the hosting namespace. */
|
|
132
|
+
readonly fabric?: FabricComposition;
|
|
133
|
+
/** Explicit exports override the bag on the transaction host. */
|
|
134
|
+
readonly ctxExports?: CtxExports;
|
|
135
|
+
/** The process table that allocated supervisor-binding pids. */
|
|
136
|
+
readonly processes?: SessionProcessSupervisor;
|
|
137
|
+
readonly processOutput?: (stream: 'stdout' | 'stderr', pid: number, data: string) => void;
|
|
138
|
+
/** Host operations, including overrides for host-specific accounting. */
|
|
139
|
+
readonly supervisorOps?: Readonly<Record<string, SupervisorOpHandler>>;
|
|
129
140
|
}
|
|
130
141
|
|
|
131
142
|
/**
|
|
@@ -168,6 +179,7 @@ export class NimbusWorkspace {
|
|
|
168
179
|
registry: CommandRegistry,
|
|
169
180
|
env: Record<string, string>,
|
|
170
181
|
private readonly sql: SqlDatabase,
|
|
182
|
+
private readonly supervisorOps: (envelope: SupervisorOpEnvelope) => Promise<unknown>,
|
|
171
183
|
) {
|
|
172
184
|
this.vfs = vfs;
|
|
173
185
|
this.kernel = kernel;
|
|
@@ -182,6 +194,10 @@ export class NimbusWorkspace {
|
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
static async create(options: NimbusWorkspaceOptions): Promise<NimbusWorkspace> {
|
|
197
|
+
if (options.fabric) composeFabric(options.fabric);
|
|
198
|
+
const exports = options.ctxExports
|
|
199
|
+
?? (options.transactions as (TransactionHost & { exports?: CtxExports }) | undefined)?.exports;
|
|
200
|
+
if (exports) adoptCtxExports(exports);
|
|
185
201
|
const vfs = options.vfs ?? openFilesystem(options);
|
|
186
202
|
const mounts = options.mounts ?? DEFAULT_MOUNT_POINTS;
|
|
187
203
|
seedBaseFilesystem(vfs, mounts);
|
|
@@ -234,13 +250,21 @@ export class NimbusWorkspace {
|
|
|
234
250
|
});
|
|
235
251
|
}
|
|
236
252
|
|
|
237
|
-
|
|
253
|
+
const supervisorOps = createSupervisorOpHandler({
|
|
254
|
+
vfs, processes: options.processes, output: options.processOutput, extend: options.supervisorOps,
|
|
255
|
+
});
|
|
256
|
+
return new NimbusWorkspace(vfs, kernel, shell, registry, env, options.sql, supervisorOps);
|
|
238
257
|
}
|
|
239
258
|
|
|
240
259
|
exec(command: string, options?: RunOptions): Promise<CommandResult> {
|
|
241
260
|
return this.commands.run(command, options);
|
|
242
261
|
}
|
|
243
262
|
|
|
263
|
+
/** The hosting object forwards its supervisorOp RPC to this method. */
|
|
264
|
+
supervisorOp(envelope: SupervisorOpEnvelope): Promise<unknown> {
|
|
265
|
+
return this.supervisorOps(envelope);
|
|
266
|
+
}
|
|
267
|
+
|
|
244
268
|
/**
|
|
245
269
|
* Apply the login files, and begin reading the terminal when there is one.
|
|
246
270
|
*
|
|
@@ -383,7 +407,7 @@ async function registerWasmRuntimes(deps: {
|
|
|
383
407
|
getEsbuild: () => {
|
|
384
408
|
if (!esbuild) {
|
|
385
409
|
esbuild = import('../runtime/esbuild-service.js')
|
|
386
|
-
.then((module) => new module.EsbuildService(deps.vfs));
|
|
410
|
+
.then((module) => new module.EsbuildService(deps.vfs.as(CRED_KERNEL)));
|
|
387
411
|
}
|
|
388
412
|
return esbuild;
|
|
389
413
|
},
|