@volter/tabnode 0.4.0 → 0.5.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.
Files changed (80) hide show
  1. package/dist/__sw__.js +259 -5
  2. package/dist/assets/{runtime-worker-DoV8lqBn.js → runtime-worker-B9buriZs.js} +4021 -2961
  3. package/dist/fetch-transport.d.ts +20 -0
  4. package/dist/guest-timers.d.ts +15 -0
  5. package/dist/http-client-transport.d.ts +19 -0
  6. package/dist/index.d.ts +11 -0
  7. package/dist/index.mjs +5204 -3057
  8. package/dist/native-stream-binding.d.ts +57 -0
  9. package/dist/native-stream-owner.d.ts +121 -0
  10. package/dist/node-lib/binding/duplex_stream.d.ts +26 -0
  11. package/dist/node-lib/binding/fds.d.ts +8 -0
  12. package/dist/node-lib/binding/fs.d.ts +311 -78
  13. package/dist/node-lib/binding/misc.d.ts +1 -0
  14. package/dist/node-lib/binding/pipe_wrap.d.ts +4 -4
  15. package/dist/node-lib/binding/process_wrap.d.ts +13 -2
  16. package/dist/node-lib/binding/stream_wrap.d.ts +27 -18
  17. package/dist/node-lib/binding/tcp_wrap.d.ts +0 -1
  18. package/dist/node-lib/binding/tty_wrap.d.ts +43 -2
  19. package/dist/node-lib/binding/util.d.ts +83 -0
  20. package/dist/node-lib/binding/uv.d.ts +4 -0
  21. package/dist/node-lib/fs-module.d.ts +1 -0
  22. package/dist/node-lib/internals/buffer-and-streams.d.ts +30 -17
  23. package/dist/node-lib/internals/index.d.ts +1 -1
  24. package/dist/node-lib/load.d.ts +17 -3
  25. package/dist/node-lib/module-hooks.d.ts +1 -0
  26. package/dist/node-lib/public-modules.d.ts +1 -1
  27. package/dist/node-lib/timers.d.ts +38 -0
  28. package/dist/node-process-host.d.ts +41 -0
  29. package/dist/process-registry.d.ts +39 -0
  30. package/dist/process-tokens.d.ts +16 -0
  31. package/dist/promise-ownership.d.ts +3 -0
  32. package/dist/runtime.d.ts +10 -9
  33. package/dist/shims/child_process.d.ts +2 -0
  34. package/dist/shims/crypto.d.ts +660 -119
  35. package/dist/shims/dns.d.ts +76 -2
  36. package/dist/shims/process.d.ts +1 -0
  37. package/dist/shims/string_decoder.d.ts +19 -3
  38. package/dist/shims/tls.d.ts +853 -31
  39. package/dist/source-edits.d.ts +7 -0
  40. package/package.json +1 -1
  41. package/src/code-transforms.ts +2 -3
  42. package/src/fetch-transport.ts +98 -0
  43. package/src/guest-timers.ts +89 -0
  44. package/src/http-client-transport.ts +169 -0
  45. package/src/index.ts +12 -0
  46. package/src/native-stream-binding.ts +393 -0
  47. package/src/native-stream-owner.ts +317 -0
  48. package/src/node-lib/binding/duplex_stream.ts +95 -0
  49. package/src/node-lib/binding/fds.ts +18 -1
  50. package/src/node-lib/binding/fs.ts +45 -17
  51. package/src/node-lib/binding/misc.ts +2 -0
  52. package/src/node-lib/binding/pipe_wrap.ts +35 -12
  53. package/src/node-lib/binding/process_wrap.ts +72 -18
  54. package/src/node-lib/binding/stream_wrap.ts +196 -72
  55. package/src/node-lib/binding/tcp_wrap.ts +43 -8
  56. package/src/node-lib/binding/tty_wrap.ts +135 -8
  57. package/src/node-lib/binding/util.ts +24 -15
  58. package/src/node-lib/binding/uv.ts +4 -0
  59. package/src/node-lib/fs-module.ts +2 -18
  60. package/src/node-lib/internal/navigator.js +149 -0
  61. package/src/node-lib/internals/buffer-and-streams.ts +50 -11
  62. package/src/node-lib/internals/events-util.ts +5 -0
  63. package/src/node-lib/internals/index.ts +12 -2
  64. package/src/node-lib/load.ts +170 -62
  65. package/src/node-lib/module-hooks.ts +5 -2
  66. package/src/node-lib/public-modules.ts +12 -48
  67. package/src/node-lib/sources.ts +4 -0
  68. package/src/node-lib/timers.ts +93 -0
  69. package/src/node-process-host.ts +124 -0
  70. package/src/process-registry.ts +144 -0
  71. package/src/process-tokens.ts +60 -6
  72. package/src/promise-ownership.ts +28 -0
  73. package/src/runtime.ts +90 -214
  74. package/src/shims/child_process.ts +313 -80
  75. package/src/shims/crypto.ts +115 -100
  76. package/src/shims/dns.ts +17 -1
  77. package/src/shims/process.ts +33 -11
  78. package/src/shims/string_decoder.ts +10 -6
  79. package/src/shims/tls.ts +49 -19
  80. package/src/source-edits.ts +20 -0
@@ -0,0 +1,317 @@
1
+ /** Host-owned native handles. Node's JS socket and IPC libraries stay in the guest. */
2
+ import { TCP, TCPConnectWrap, type SockName } from './node-lib/binding/tcp_wrap';
3
+ import { Pipe, PipeConnectWrap } from './node-lib/binding/pipe_wrap';
4
+ import { LibuvStreamWrap, WriteWrap, ShutdownWrap, streamBaseState, kReadBytesOrError, kArrayBufferOffset } from './node-lib/binding/stream_wrap';
5
+ import { __adoptHandle } from './node-lib/binding/handles';
6
+ import { UV_EBADF, UV_ECANCELED, UV_EINVAL, UV_ENOBUFS, UV_ENOTSUP, UV_EPIPE, UV_EMFILE, UV_EBUSY, UV_EALREADY } from './node-lib/binding/uv';
7
+
8
+ export interface NativeStreamDescriptor { id: number; kind: 'tcp' | 'pipe'; type: number }
9
+ export interface NativeStreamLimits {
10
+ maxHandles: number;
11
+ maxReadChunkBytes: number;
12
+ maxQueuedBytes: number;
13
+ /** Per connection, like maxQueuedBytes; includes unread IPC descriptor copies. */
14
+ maxQueuedHandles: number;
15
+ maxPendingWriteBytes: number;
16
+ }
17
+ export type NativeStreamEvent =
18
+ | { type: 'closed' }
19
+ | { type: 'read'; id: number; status: number; bytes?: Uint8Array; handle?: NativeStreamDescriptor }
20
+ | { type: 'connection'; id: number; status: number; handle?: NativeStreamDescriptor }
21
+ | { type: 'connect' | 'shutdown' | 'close'; id: number; request: number; status: number };
22
+ export type NativeStreamOperation =
23
+ | { operation: 'create'; kind: 'tcp' | 'pipe'; type: number }
24
+ | { operation: 'fdType'; fd: number }
25
+ | { operation: 'pair'; id: number; peer: number }
26
+ | { operation: 'duplicate'; id: number }
27
+ | { operation: 'bind'; id: number; address: string; port?: number; ipv6?: boolean; flags?: number }
28
+ | { operation: 'listen'; id: number; backlog: number }
29
+ | { operation: 'connect'; id: number; request: number; address: string; port?: number; ipv6?: boolean }
30
+ | { operation: 'open'; id: number; fd: number }
31
+ | { operation: 'readStart' | 'readStop' | 'ref' | 'unref' | 'getsockname' | 'getpeername'; id: number }
32
+ | { operation: 'shutdown' | 'close' | 'reset'; id: number; request: number };
33
+ export interface NativeStreamReply { status: number; handle?: NativeStreamDescriptor; handleType?: 'TCP' | 'PIPE'; address?: Partial<SockName> }
34
+ export interface NativeStreamTransport {
35
+ readonly limits: Readonly<NativeStreamLimits>;
36
+ call(operation: NativeStreamOperation): NativeStreamReply;
37
+ write(id: number, bytes: Uint8Array, handle?: number): Promise<number>;
38
+ onEvent(listener: (event: NativeStreamEvent) => void): () => void;
39
+ }
40
+
41
+ type NativeHandle = TCP | Pipe;
42
+ interface Entry { handle: NativeHandle; descriptor: NativeStreamDescriptor; buffer?: Uint8Array }
43
+ const scopes = new WeakMap<LibuvStreamWrap, NativeStreamScope>();
44
+
45
+ /**
46
+ * One capability namespace per worker. Only the trusted host can inherit a
47
+ * descriptor from another scope; wire operations can name this scope's IDs only.
48
+ */
49
+ export class NativeStreamScope {
50
+ private readonly entries = new Map<number, Entry>();
51
+ private readonly descriptors = new Map<number, number>();
52
+ private readonly writes = new Map<number, (status: number) => void>();
53
+ private readonly connects = new Map<number, number>();
54
+ private nextId = 1;
55
+ private pendingWriteBytes = 0;
56
+ private disposed = false;
57
+ private readonly limits: Readonly<NativeStreamLimits>;
58
+
59
+ constructor(limits: NativeStreamLimits, private readonly emit: (event: NativeStreamEvent) => void) {
60
+ if ([limits.maxHandles, limits.maxReadChunkBytes, limits.maxQueuedBytes, limits.maxQueuedHandles, limits.maxPendingWriteBytes].some(value => !Number.isSafeInteger(value) || value < 1)
61
+ || limits.maxReadChunkBytes > 0x7fffffff
62
+ || limits.maxReadChunkBytes > limits.maxQueuedBytes
63
+ || limits.maxReadChunkBytes > limits.maxPendingWriteBytes) throw new Error('Invalid native stream resource limits.');
64
+ this.limits = Object.freeze({ ...limits });
65
+ }
66
+
67
+ private entry(id: number): Entry | undefined {
68
+ return this.disposed ? undefined : this.entries.get(id);
69
+ }
70
+
71
+ private publish(event: NativeStreamEvent): void {
72
+ if (this.disposed) return;
73
+ try { this.emit(event); }
74
+ catch (cause) {
75
+ try { this.dispose(); } catch (cleanup) { throw new AggregateError([cause, cleanup], 'Native stream event delivery and cleanup failed.'); }
76
+ throw cause;
77
+ }
78
+ }
79
+
80
+ private claim(handle: NativeHandle): NativeStreamDescriptor | undefined {
81
+ if (this.disposed || !Number.isSafeInteger(this.nextId) || this.entries.size >= this.limits.maxHandles || scopes.has(handle)) return undefined;
82
+ const descriptor: NativeStreamDescriptor = { id: this.nextId++, kind: handle instanceof TCP ? 'tcp' : 'pipe', type: handle.type };
83
+ const entry: Entry = { handle, descriptor };
84
+ this.entries.set(descriptor.id, entry);
85
+ scopes.set(handle, this);
86
+ // The scope, not the currently executing guest in the owner realm, owns it.
87
+ __adoptHandle(handle, null);
88
+ handle.onconnection = (status: number, accepted: NativeHandle) => {
89
+ const incoming = status === 0 ? this.claim(accepted) : undefined;
90
+ if (status === 0 && !incoming) accepted.close();
91
+ this.publish({ type: 'connection', id: descriptor.id, status: status || (incoming ? 0 : UV_EMFILE), ...(incoming ? { handle: incoming } : {}) });
92
+ };
93
+ handle.onread = buffer => {
94
+ const count = streamBaseState[kReadBytesOrError];
95
+ const offset = streamBaseState[kArrayBufferOffset];
96
+ handle.readStop(); // One read credit; the receiver explicitly replenishes it.
97
+ const sent = handle.pendingHandle;
98
+ handle.pendingHandle = null;
99
+ let incoming: NativeStreamDescriptor | undefined;
100
+ if (sent != null) {
101
+ if ((sent instanceof TCP || sent instanceof Pipe) && !sent.listening) incoming = this.claim(sent);
102
+ if (!incoming) {
103
+ if (sent instanceof LibuvStreamWrap && !scopes.has(sent) && !(sent as NativeHandle).listening) sent.close();
104
+ this.publish({ type: 'read', id: descriptor.id, status: UV_ENOTSUP });
105
+ this.close(descriptor.id);
106
+ return;
107
+ }
108
+ }
109
+ const bytes = count > 0
110
+ ? (buffer ? new Uint8Array(buffer, offset, count) : entry.buffer!.subarray(0, count)).slice()
111
+ : undefined;
112
+ this.publish({ type: 'read', id: descriptor.id, status: count,
113
+ ...(bytes ? { bytes } : {}), ...(incoming ? { handle: incoming } : {}) });
114
+ };
115
+ return { ...descriptor };
116
+ }
117
+
118
+ /** Synchronous operations return libuv status; completions use the event door. */
119
+ call(operation: NativeStreamOperation): NativeStreamReply {
120
+ if (this.disposed) return { status: UV_EBADF };
121
+ if (operation.operation === 'fdType') {
122
+ if (!Number.isInteger(operation.fd) || operation.fd < 0) return { status: UV_EINVAL };
123
+ const id = this.descriptors.get(operation.fd);
124
+ const entry = id === undefined ? undefined : this.entry(id);
125
+ return entry ? { status: 0, handleType: entry.descriptor.kind === 'tcp' ? 'TCP' : 'PIPE' } : { status: UV_EBADF };
126
+ }
127
+ if (operation.operation === 'create') {
128
+ if (this.entries.size >= this.limits.maxHandles) return { status: UV_EMFILE };
129
+ if ((operation.kind !== 'tcp' && operation.kind !== 'pipe')
130
+ || !Number.isInteger(operation.type) || operation.type < 0 || operation.type > (operation.kind === 'pipe' ? 2 : 1)) return { status: UV_EINVAL };
131
+ const handle = operation.kind === 'tcp' ? new TCP(operation.type) : new Pipe(operation.type);
132
+ const descriptor = this.claim(handle);
133
+ if (!descriptor) { handle.close(); return { status: UV_EMFILE }; }
134
+ return { status: 0, handle: descriptor };
135
+ }
136
+ const entry = this.entry(operation.id);
137
+ if (!entry) return { status: UV_EBADF };
138
+ const handle = entry.handle;
139
+ switch (operation.operation) {
140
+ case 'duplicate': {
141
+ if (!handle.peer || handle.listening) return { status: UV_ENOTSUP };
142
+ if (this.entries.size >= this.limits.maxHandles) return { status: UV_EMFILE };
143
+ const copy = handle.duplicate();
144
+ const descriptor = this.claim(copy);
145
+ if (!descriptor) { copy.close(); return { status: UV_EMFILE }; }
146
+ return { status: 0, handle: descriptor };
147
+ }
148
+ case 'pair': {
149
+ const other = this.entry(operation.peer)?.handle;
150
+ if (!other || handle.peer || other.peer || handle.listening || other.listening || other === handle) return { status: UV_EINVAL };
151
+ LibuvStreamWrap.pair(handle, other);
152
+ return { status: 0 };
153
+ }
154
+ case 'bind': {
155
+ if (typeof operation.address !== 'string') return { status: UV_EINVAL };
156
+ if (handle instanceof Pipe) return { status: handle.bind(operation.address) };
157
+ if (!Number.isInteger(operation.port) || operation.port! < 0 || operation.port! > 65535) return { status: UV_EINVAL };
158
+ return { status: operation.ipv6 ? handle.bind6(operation.address, operation.port!, operation.flags) : handle.bind(operation.address, operation.port!, operation.flags) };
159
+ }
160
+ case 'listen': return { status: Number.isInteger(operation.backlog) && operation.backlog >= 0 ? handle.listen(operation.backlog) : UV_EINVAL };
161
+ case 'connect': {
162
+ if (this.connects.has(operation.id)) return { status: UV_EALREADY };
163
+ if (typeof operation.address !== 'string' || !Number.isSafeInteger(operation.request) || operation.request < 1) return { status: UV_EINVAL };
164
+ if (handle instanceof TCP && (!Number.isInteger(operation.port) || operation.port! < 1 || operation.port! > 65535)) return { status: UV_EINVAL };
165
+ this.connects.set(operation.id, operation.request);
166
+ const request = handle instanceof TCP ? new TCPConnectWrap() : new PipeConnectWrap();
167
+ request.oncomplete = status => {
168
+ if (this.connects.get(operation.id) !== operation.request) return;
169
+ this.connects.delete(operation.id);
170
+ this.publish({ type: 'connect', id: operation.id, request: operation.request, status });
171
+ };
172
+ const status = handle instanceof TCP
173
+ ? operation.ipv6 ? handle.connect6(request as TCPConnectWrap, operation.address, operation.port!) : handle.connect(request as TCPConnectWrap, operation.address, operation.port!)
174
+ : handle.connect(request as PipeConnectWrap, operation.address);
175
+ if (status !== 0) this.connects.delete(operation.id);
176
+ return { status };
177
+ }
178
+ case 'open': {
179
+ const inherited = this.descriptors.get(operation.fd);
180
+ const held = inherited === undefined ? undefined : this.entry(inherited);
181
+ if (!held || held.descriptor.kind !== entry.descriptor.kind || handle.peer || handle.listening) return { status: UV_EBADF };
182
+ if (held !== entry) {
183
+ if (handle instanceof TCP && held.handle instanceof TCP) {
184
+ const local: Partial<SockName> = {}, remote: Partial<SockName> = {};
185
+ if (held.handle.getsockname(local) === 0 && held.handle.getpeername(remote) === 0) handle.adoptNames(local as SockName, remote as SockName);
186
+ }
187
+ handle.takeOverFrom(held.handle);
188
+ this.entries.delete(inherited!);
189
+ scopes.delete(held.handle);
190
+ held.handle.close();
191
+ this.descriptors.set(operation.fd, operation.id);
192
+ }
193
+ return { status: 0 };
194
+ }
195
+ case 'readStart':
196
+ entry.buffer ??= new Uint8Array(this.limits.maxReadChunkBytes);
197
+ handle.useUserBuffer(entry.buffer);
198
+ return { status: handle.readStart() };
199
+ case 'readStop': return { status: handle.readStop() };
200
+ case 'ref': handle.ref(); return { status: 0 };
201
+ case 'unref': handle.unref(); return { status: 0 };
202
+ case 'getsockname': case 'getpeername': {
203
+ if (!(handle instanceof TCP)) return { status: UV_ENOTSUP };
204
+ const address: Partial<SockName> = {};
205
+ const status = handle[operation.operation](address);
206
+ return { status, ...(status === 0 ? { address } : {}) };
207
+ }
208
+ case 'shutdown': {
209
+ if (!Number.isSafeInteger(operation.request) || operation.request < 1) return { status: UV_EINVAL };
210
+ if (this.writes.has(operation.id)) return { status: UV_EBUSY };
211
+ const request = new ShutdownWrap();
212
+ request.oncomplete = status => this.publish({ type: 'shutdown', id: operation.id, request: operation.request, status });
213
+ return { status: handle.shutdown(request) };
214
+ }
215
+ case 'reset':
216
+ if (!Number.isSafeInteger(operation.request) || operation.request < 1) return { status: UV_EINVAL };
217
+ if (!(handle instanceof TCP)) return { status: UV_ENOTSUP };
218
+ this.writes.get(operation.id)?.(UV_ECANCELED);
219
+ return this.close(operation.id, operation.request, true);
220
+ case 'close':
221
+ if (!Number.isSafeInteger(operation.request) || operation.request < 1) return { status: UV_EINVAL };
222
+ return this.close(operation.id, operation.request);
223
+ }
224
+ }
225
+
226
+ /**
227
+ * Transport chunks are bounded and serialized per handle. Waiting for native
228
+ * receive capacity retains at most one chunk per writer, within the scope's
229
+ * explicit aggregate ceiling. No polling or copying a whole guest write.
230
+ */
231
+ write(id: number, bytes: Uint8Array, sentId?: number): Promise<number> {
232
+ const handle = this.entry(id)?.handle;
233
+ if (!handle) return Promise.resolve(UV_EBADF);
234
+ if (this.writes.has(id)) return Promise.resolve(UV_EBUSY);
235
+ if (bytes.byteLength > this.limits.maxReadChunkBytes
236
+ || this.pendingWriteBytes + bytes.byteLength > this.limits.maxPendingWriteBytes) return Promise.resolve(UV_ENOBUFS);
237
+ const sent = sentId === undefined ? undefined : this.entry(sentId)?.handle;
238
+ if (sentId !== undefined && !sent) return Promise.resolve(UV_EBADF);
239
+ if (sent && (!(handle instanceof Pipe) || !sent.peer || sent.listening || bytes.byteLength === 0)) return Promise.resolve(UV_ENOTSUP);
240
+ // Retain the descriptor while capacity is unavailable; the sender can close
241
+ // its original after submitting the write without invalidating this copy.
242
+ const owned = new Uint8Array(bytes.byteLength);
243
+ owned.set(bytes);
244
+ const retained = sent?.duplicate();
245
+ if (retained) __adoptHandle(retained, null);
246
+ this.pendingWriteBytes += owned.byteLength;
247
+ return new Promise((resolve, reject) => {
248
+ let ended = false;
249
+ let unsubscribe = (): void => {};
250
+ const finish = (status: number, failure?: unknown): void => {
251
+ if (ended) return;
252
+ ended = true;
253
+ unsubscribe();
254
+ this.writes.delete(id);
255
+ this.pendingWriteBytes -= owned.byteLength;
256
+ try { retained?.close(); }
257
+ catch (cause) { reject(cause); return; }
258
+ if (failure !== undefined) reject(failure);
259
+ else resolve(status);
260
+ };
261
+ const attempt = (): void => {
262
+ if (ended) return;
263
+ if (handle.closed || this.disposed) { finish(UV_ECANCELED); return; }
264
+ if (!handle.peer) { finish(UV_EPIPE); return; }
265
+ if (handle.peerQueuedBytes + owned.byteLength > this.limits.maxQueuedBytes) return;
266
+ // A one-byte IPC message can carry an entire descriptor. Byte credit
267
+ // alone does not meaningfully bound unread SCM_RIGHTS copies.
268
+ if (retained && handle.peerQueuedHandles >= this.limits.maxQueuedHandles) return;
269
+ try { finish(handle.writeBuffer(new WriteWrap(), owned, retained)); }
270
+ catch (cause) { finish(UV_ECANCELED, cause); }
271
+ };
272
+ this.writes.set(id, finish);
273
+ unsubscribe = handle.onPeerReadCapacity(attempt);
274
+ attempt();
275
+ });
276
+ }
277
+
278
+ /** Trusted parent-to-child inheritance; this is deliberately absent from call(). */
279
+ inherit(fd: number, source: NativeStreamScope, id: number): NativeStreamReply {
280
+ if (this.disposed || !Number.isInteger(fd) || fd < 0 || this.descriptors.has(fd)) return { status: UV_EINVAL };
281
+ if (this.entries.size >= this.limits.maxHandles) return { status: UV_EMFILE };
282
+ const handle = source.entry(id)?.handle;
283
+ if (!handle) return { status: UV_EBADF };
284
+ if (handle.listening || !handle.peer) return { status: UV_ENOTSUP };
285
+ const copy = handle.duplicate();
286
+ const descriptor = this.claim(copy);
287
+ if (!descriptor) { copy.close(); return { status: UV_EMFILE }; }
288
+ this.descriptors.set(fd, descriptor.id);
289
+ return { status: 0, handle: descriptor };
290
+ }
291
+
292
+ private close(id: number, request?: number, reset = false): NativeStreamReply {
293
+ const entry = this.entries.get(id);
294
+ if (!entry) return { status: UV_EBADF };
295
+ this.writes.get(id)?.(UV_ECANCELED);
296
+ const connecting = this.connects.get(id);
297
+ this.connects.delete(id);
298
+ if (connecting !== undefined) this.publish({ type: 'connect', id, request: connecting, status: UV_ECANCELED });
299
+ this.entries.delete(id);
300
+ scopes.delete(entry.handle);
301
+ for (const [fd, held] of this.descriptors) if (held === id) this.descriptors.delete(fd);
302
+ const complete = request === undefined ? undefined : () => this.publish({ type: 'close', id, request, status: 0 });
303
+ if (reset) (entry.handle as TCP).reset(complete);
304
+ else entry.handle.close(complete);
305
+ return { status: 0 };
306
+ }
307
+
308
+ dispose(): void {
309
+ if (this.disposed) return;
310
+ this.disposed = true;
311
+ const failures: unknown[] = [];
312
+ for (const id of [...this.entries.keys()]) {
313
+ try { this.close(id); } catch (cause) { failures.push(cause); }
314
+ }
315
+ if (failures.length) throw new AggregateError(failures, 'Native stream scope cleanup failed.');
316
+ }
317
+ }
@@ -0,0 +1,95 @@
1
+ /** A Node Duplex as a libuv stream handle. Node still owns both socket APIs. */
2
+ import { LibuvStreamWrap, type WriteWrap, type ShutdownWrap, streamBaseState, kBytesWritten, kLastWriteWasAsync } from './stream_wrap';
3
+ import { UV_EBADF, UV_ECONNRESET, UV_ENOTSUP } from './uv';
4
+
5
+ interface DuplexTransport {
6
+ write(chunk: Uint8Array, callback: (error?: Error | null) => void): boolean;
7
+ end(callback: () => void): void;
8
+ pause(): void;
9
+ resume(): void;
10
+ destroy(): void;
11
+ on(event: string, listener: (...args: any[]) => void): void;
12
+ }
13
+
14
+ /** No listener or network address is allocated for this internal connection. */
15
+ export class DuplexStreamHandle extends LibuvStreamWrap {
16
+ private pendingWrite?: (status: number) => void;
17
+
18
+ constructor(private readonly transport: DuplexTransport) {
19
+ super();
20
+ transport.pause();
21
+ transport.on('data', (chunk: Uint8Array) => {
22
+ transport.pause();
23
+ this.receive(chunk);
24
+ });
25
+ transport.on('end', () => this.receiveEof());
26
+ transport.on('error', () => {
27
+ this.pendingWrite?.(UV_ECONNRESET);
28
+ this.receiveError(UV_ECONNRESET);
29
+ });
30
+ transport.on('close', () => {
31
+ this.pendingWrite?.(UV_ECONNRESET);
32
+ this.receiveEof();
33
+ });
34
+ }
35
+
36
+ override readStop(): number {
37
+ this.transport.pause();
38
+ return super.readStop();
39
+ }
40
+
41
+ protected override flushInbound(): void {
42
+ super.flushInbound();
43
+ if (this.reading && !this.closed && !this.inboundEof) this.transport.resume();
44
+ }
45
+
46
+ protected override dispatchWrite(req: WriteWrap, buffer: Uint8Array): number {
47
+ if (this.closed) return UV_EBADF;
48
+ streamBaseState[kBytesWritten] = buffer.byteLength;
49
+ streamBaseState[kLastWriteWasAsync] = 1;
50
+ this.writeQueueSize = buffer.byteLength;
51
+ let offset = 0;
52
+ let complete = false;
53
+ const finish = (status: number): void => {
54
+ if (complete) return;
55
+ complete = true;
56
+ this.pendingWrite = undefined;
57
+ this.writeQueueSize = 0;
58
+ queueMicrotask(() => req.oncomplete?.(status));
59
+ };
60
+ this.pendingWrite = finish;
61
+ const next = (): void => {
62
+ if (complete) return;
63
+ if (this.closed) { finish(UV_EBADF); return; }
64
+ if (offset === buffer.byteLength) { finish(0); return; }
65
+ // A caller's large write is retained until its callback, as in Node;
66
+ // only one bounded copy enters the transport at a time.
67
+ const bytes = buffer.slice(offset, offset + 64 * 1024);
68
+ offset += bytes.byteLength;
69
+ this.transport.write(bytes, error => {
70
+ if (error) { finish(UV_ECONNRESET); return; }
71
+ this.bytesWritten += bytes.byteLength;
72
+ this.writeQueueSize -= bytes.byteLength;
73
+ next();
74
+ });
75
+ };
76
+ queueMicrotask(next);
77
+ return 0;
78
+ }
79
+
80
+ override shutdown(req: ShutdownWrap): number {
81
+ if (this.closed) return UV_EBADF;
82
+ this.transport.end(() => req.oncomplete?.(0));
83
+ return 0;
84
+ }
85
+
86
+ protected override onCloseHandle(): void {
87
+ this.pendingWrite?.(UV_EBADF);
88
+ this.transport.destroy();
89
+ }
90
+
91
+ getsockname(): number { return UV_ENOTSUP; }
92
+ getpeername(): number { return UV_ENOTSUP; }
93
+ setNoDelay(): number { return 0; }
94
+ setKeepAlive(): number { return 0; }
95
+ }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import type { ProcessToken } from '../../process-tokens';
17
17
  import { currentOwner } from './handles';
18
+ import { nativeInheritedFdType } from '../../native-stream-binding';
18
19
 
19
20
  /** What libuv answers for a descriptor. */
20
21
  export type HandleType = 'TCP' | 'TTY' | 'UDP' | 'FILE' | 'PIPE' | 'UNKNOWN';
@@ -36,6 +37,15 @@ const runFds = new Map<ProcessToken, Map<number, { type: HandleType; handle: unk
36
37
 
37
38
  let nextFd = 20;
38
39
 
40
+ /** Files and streams occupy one descriptor namespace, as on the OS. */
41
+ export function allocateFd(): number {
42
+ // The host can inherit any descriptor number, not just the usual IPC fd 3.
43
+ // Files allocated locally must not shadow one owned by the native channel.
44
+ while (nativeInheritedFdType(nextFd) !== undefined) nextFd += 1;
45
+ return nextFd++;
46
+ }
47
+
48
+
39
49
  /** Record a descriptor a run is started with, under that run's name. */
40
50
  export function registerRunFd(token: ProcessToken, fd: number, type: HandleType, handle: unknown): void {
41
51
  let table = runFds.get(token);
@@ -48,6 +58,11 @@ export function releaseRunFds(token: ProcessToken): void {
48
58
  runFds.delete(token);
49
59
  }
50
60
 
61
+ /** Trusted process admission reads copies of this run's descriptor entries. */
62
+ export function inheritedRunFds(token: ProcessToken): Array<{ fd: number; type: HandleType; handle: unknown }> {
63
+ return Array.from(runFds.get(token) ?? [], ([fd, entry]) => ({ fd, ...entry }));
64
+ }
65
+
51
66
  /** The descriptor table of the run asking, where the asking run has one. */
52
67
  function tableOfAskingRun(): Map<number, { type: HandleType; handle: unknown }> | undefined {
53
68
  const token = currentOwner();
@@ -56,7 +71,7 @@ function tableOfAskingRun(): Map<number, { type: HandleType; handle: unknown }>
56
71
 
57
72
  /** Record a handle under a descriptor of the engine's own, and answer the number. */
58
73
  export function registerFd(type: HandleType, handle: unknown): number {
59
- const fd = nextFd++;
74
+ const fd = allocateFd();
60
75
  openFds.set(fd, { type, handle });
61
76
  return fd;
62
77
  }
@@ -75,6 +90,8 @@ export function releaseFd(fd: number): void {
75
90
  export function guessHandleTypeOfFd(fd: number): HandleType {
76
91
  const open = tableOfAskingRun()?.get(fd) ?? openFds.get(fd);
77
92
  if (open) return open.type;
93
+ const inherited = nativeInheritedFdType(fd);
94
+ if (inherited) return inherited;
78
95
  if (fd === 0 || fd === 1 || fd === 2) return 'PIPE';
79
96
  return 'FILE';
80
97
  }
@@ -30,6 +30,9 @@
30
30
  import { createNodeError as vfsError } from '../../virtual-fs';
31
31
  import { registerHandle, releaseHandle, refHandle, unrefHandle, handleHasRef, currentOwner } from './handles';
32
32
  import { enterRun } from '../../process-tokens';
33
+ import { allocateFd, handleForFd } from './fds';
34
+ import { LibuvStreamWrap, WriteWrap } from './stream_wrap';
35
+ import { errname } from './uv';
33
36
 
34
37
  /**
35
38
  * An errno error, as the filesystem makes one. The tree's own maker knows the
@@ -158,7 +161,6 @@ interface OpenFile {
158
161
 
159
162
  /** Every descriptor this engine has open, and the next number to hand out. */
160
163
  const openFiles = new Map<number, OpenFile>();
161
- let nextFd = 20;
162
164
 
163
165
  function fileFor(fd: number): OpenFile {
164
166
  const file = openFiles.get(fd);
@@ -259,14 +261,15 @@ function answer<T>(req: FSReq | undefined, work: () => T): T | undefined {
259
261
  * onto the path; a string child threw `list[1]`/`list[2]` must be a Buffer,
260
262
  * received `'exthost1'`.
261
263
  */
264
+ export function createFsBindings(Bytes: () => typeof NodeBuffer = () => NodeBuffer) {
262
265
  function encodeFsName(name: string, encoding: unknown): string | Uint8Array {
263
266
  if (encoding !== 'buffer' && encoding !== 6) {
264
267
  if (typeof encoding === 'string' && encoding.length > 0 && encoding !== 'utf8' && encoding !== 'utf-8') {
265
- return (NodeBuffer.from(name) as { toString(enc: string): string }).toString(encoding);
268
+ return (Bytes().from(name) as { toString(enc: string): string }).toString(encoding);
266
269
  }
267
270
  return name;
268
271
  }
269
- return NodeBuffer.from(name);
272
+ return Bytes().from(name);
270
273
  }
271
274
 
272
275
  /** The path as Node handed it, with a Buffer decoded. A symlink's target is this, not resolved. */
@@ -303,21 +306,21 @@ function asPath(path: unknown): string {
303
306
  * the callback and nothing else, because `answer` above is the whole of what
304
307
  * makes a call asynchronous in a realm with one thread.
305
308
  */
306
- export class FSReqCallback {
309
+ class FSReqCallback {
307
310
  oncomplete: ((error: Error | null, ...rest: unknown[]) => void) | undefined;
308
311
  context: unknown;
309
312
  constructor(public bigint = false) {}
310
313
  }
311
314
 
312
315
  /** The symbol `fs.promises` passes where a callback would go. */
313
- export const kUsePromises = Symbol('kUsePromises');
316
+
314
317
 
315
318
  /**
316
319
  * `internalBinding('fs_dir')`'s handle: an open directory, read a batch at a
317
320
  * time. Node's own `Dir` and `Dirent` are built on it; what a handle owes is
318
321
  * a flat list of name and type, and `null` when there is nothing left.
319
322
  */
320
- export class DirHandle {
323
+ class DirHandle {
321
324
  #entries: Array<[string, number]>;
322
325
  #at = 0;
323
326
  #encoding: unknown;
@@ -351,7 +354,7 @@ export class DirHandle {
351
354
  }
352
355
  }
353
356
 
354
- export const fsDirBinding = {
357
+ const fsDirBinding = {
355
358
  opendirSync(path: unknown, encoding?: unknown): DirHandle { return new DirHandle(asPath(path), encoding); },
356
359
  opendir(path: unknown, encoding: string, req?: FSReq): DirHandle | undefined {
357
360
  return answer(req, () => new DirHandle(asPath(path), encoding));
@@ -364,12 +367,12 @@ export const fsDirBinding = {
364
367
  * `FSWatcher` expects around them -- a start that takes a path and answers
365
368
  * an errno, a close, and `onchange(status, event, filename)`.
366
369
  */
367
- export class FSEvent {
368
- onchange: ((status: number, event: string, filename: string) => void) | null = null;
370
+ class FSEvent {
371
+ onchange: ((status: number, event: string, filename: string | Uint8Array) => void) | null = null;
369
372
  #watcher: { close(): void } | null = null;
370
373
  initialized = false;
371
374
 
372
- start(path: unknown, persistent?: boolean, recursive?: boolean, _encoding?: string): number {
375
+ start(path: unknown, persistent?: boolean, recursive?: boolean, encoding?: string): number {
373
376
  const name = asPath(path);
374
377
  const tree = vfs();
375
378
  const cwd = callingCwd();
@@ -379,7 +382,9 @@ export class FSEvent {
379
382
  // libuv names a change `change` and a create or a remove `rename`,
380
383
  // which is what Node's `FSWatcher` turns into its own two events.
381
384
  const notify = () => withFilesystem(tree, () => withCwd(cwd, () =>
382
- this.onchange?.(0, event === 'rename' ? 'rename' : 'change', filename ?? '')));
385
+ // libuv encodes the filename at this binding, including Buffer
386
+ // when requested. Node's FSWatcher forwards it without conversion.
387
+ this.onchange?.(0, event === 'rename' ? 'rename' : 'change', encodeFsName(filename ?? '', encoding))));
383
388
  if (owner === null) notify();
384
389
  else enterRun(owner, notify);
385
390
  }) as never) as unknown as { close(): void };
@@ -410,7 +415,7 @@ export class FSEvent {
410
415
  * `StatWatcher`: what `fs.watchFile` is. It polls, as libuv's does, and the
411
416
  * interval is the one the program asked for.
412
417
  */
413
- export class StatWatcher {
418
+ class StatWatcher {
414
419
  onchange: ((current: Float64Array | BigInt64Array, previous: Float64Array | BigInt64Array) => void) | null = null;
415
420
  #timer: ReturnType<typeof setInterval> | null = null;
416
421
  #previous: Float64Array | BigInt64Array | null = null;
@@ -445,21 +450,21 @@ export class StatWatcher {
445
450
  hasRef(): boolean { return this.#timer !== null; }
446
451
  }
447
452
 
448
- export const fsEventWrapBinding = { FSEvent };
453
+ const fsEventWrapBinding = { FSEvent };
449
454
 
450
455
  /**
451
456
  * What `fs.promises.open` is handed: a descriptor with a close of its own.
452
457
  * Node's is a C++ handle whose destructor closes the file; here it is the
453
458
  * same number this file's table keeps, with the same close.
454
459
  */
455
- export class FileHandle {
460
+ class FileHandle {
456
461
  constructor(public fd: number) {}
457
462
  close(): Promise<void> { openFiles.delete(this.fd); return Promise.resolve(); }
458
463
  release(): void { openFiles.delete(this.fd); }
459
464
  getAsyncId(): number { return this.fd; }
460
465
  }
461
466
 
462
- export const fsBinding = {
467
+ const fsBinding = {
463
468
  FSReqCallback,
464
469
  kUsePromises,
465
470
  StatWatcher,
@@ -490,7 +495,7 @@ export const fsBinding = {
490
495
  } else if ((flags & bits.truncate) !== 0) {
491
496
  tree.writeFileSync(name, '');
492
497
  }
493
- const fd = nextFd++;
498
+ const fd = allocateFd();
494
499
  const position = (flags & bits.append) !== 0 && exists ? (tree.readFileSync(name) as Uint8Array).length : 0;
495
500
  openFiles.set(fd, { path: name, flags, position, directory: false, tree });
496
501
  return fd;
@@ -498,7 +503,11 @@ export const fsBinding = {
498
503
  },
499
504
 
500
505
  close(fd: number, req?: FSReq): undefined {
501
- return answer(req, () => { fileFor(fd); openFiles.delete(fd); return undefined; });
506
+ return answer(req, () => {
507
+ const stream = handleForFd(fd);
508
+ if (stream instanceof LibuvStreamWrap) { stream.close(); return undefined; }
509
+ fileFor(fd); openFiles.delete(fd); return undefined;
510
+ });
502
511
  },
503
512
 
504
513
  // ---- reading ------------------------------------------------------------
@@ -544,6 +553,14 @@ export const fsBinding = {
544
553
  // ---- writing ------------------------------------------------------------
545
554
  writeBuffer(fd: number, buffer: Uint8Array, offset: number, length: number, position: number | null, req?: FSReq): number | undefined {
546
555
  return answer(req, () => {
556
+ const stream = handleForFd(fd);
557
+ if (stream instanceof LibuvStreamWrap) {
558
+ if (position !== null && position !== undefined && position >= 0) throw createNodeError('ESPIPE', 'write', String(fd));
559
+ const bytes = buffer.subarray(offset, offset + length);
560
+ const status = stream.writeBuffer(new WriteWrap(), bytes);
561
+ if (status !== 0) throw createNodeError(errname(status), 'write', String(fd));
562
+ return bytes.length;
563
+ }
547
564
  const file = fileFor(fd);
548
565
  if ((file.flags & 3) === 0) throw createNodeError('EBADF', 'write', file.path);
549
566
  const tree = file.tree;
@@ -872,4 +889,15 @@ export const fsBinding = {
872
889
  },
873
890
  };
874
891
 
892
+ return { fsBinding, fsDirBinding, fsEventWrapBinding, FSReqCallback, DirHandle, FSEvent, StatWatcher, FileHandle };
893
+ }
894
+
895
+ export const kUsePromises = Symbol('kUsePromises');
896
+ const hostBindings = createFsBindings();
897
+ export const { fsBinding, fsDirBinding, fsEventWrapBinding, FSReqCallback, DirHandle, FSEvent, StatWatcher, FileHandle } = hostBindings;
898
+ export type FSReqCallback = InstanceType<typeof FSReqCallback>;
899
+ export type DirHandle = InstanceType<typeof DirHandle>;
900
+ export type FSEvent = InstanceType<typeof FSEvent>;
901
+ export type StatWatcher = InstanceType<typeof StatWatcher>;
902
+ export type FileHandle = InstanceType<typeof FileHandle>;
875
903
  export default fsBinding;
@@ -166,6 +166,8 @@ export const encodingBinding = {
166
166
  * Node without ICU does, and is the one thing `buffer.js` loses here.
167
167
  */
168
168
  export const configBinding = {
169
+ // No ICU: Node navigator supplies its own default for an empty locale.
170
+ getDefaultLocale: (): string => '',
169
171
  hasIntl: false,
170
172
  hasSmallICU: false,
171
173
  hasNodeOptions: true,