@volter/tabnode 0.4.1 → 0.5.1
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/dist/__sw__.js +259 -5
- package/dist/assets/{runtime-worker-LWfW6VWE.js → runtime-worker-DBZ2flHK.js} +4000 -2944
- package/dist/fetch-transport.d.ts +20 -0
- package/dist/guest-timers.d.ts +15 -0
- package/dist/http-client-transport.d.ts +19 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.mjs +5196 -3062
- package/dist/native-stream-binding.d.ts +57 -0
- package/dist/native-stream-owner.d.ts +121 -0
- package/dist/node-lib/binding/duplex_stream.d.ts +26 -0
- package/dist/node-lib/binding/fds.d.ts +8 -0
- package/dist/node-lib/binding/fs.d.ts +311 -78
- package/dist/node-lib/binding/misc.d.ts +1 -0
- package/dist/node-lib/binding/pipe_wrap.d.ts +4 -4
- package/dist/node-lib/binding/process_wrap.d.ts +12 -1
- package/dist/node-lib/binding/stream_wrap.d.ts +27 -18
- package/dist/node-lib/binding/tcp_wrap.d.ts +0 -1
- package/dist/node-lib/binding/tty_wrap.d.ts +43 -2
- package/dist/node-lib/binding/util.d.ts +83 -0
- package/dist/node-lib/binding/uv.d.ts +4 -0
- package/dist/node-lib/fs-module.d.ts +1 -0
- package/dist/node-lib/internals/buffer-and-streams.d.ts +30 -17
- package/dist/node-lib/internals/index.d.ts +1 -1
- package/dist/node-lib/load.d.ts +17 -3
- package/dist/node-lib/module-hooks.d.ts +1 -0
- package/dist/node-lib/public-modules.d.ts +1 -1
- package/dist/node-lib/timers.d.ts +38 -0
- package/dist/node-process-host.d.ts +41 -0
- package/dist/process-registry.d.ts +39 -0
- package/dist/process-tokens.d.ts +11 -0
- package/dist/promise-ownership.d.ts +3 -0
- package/dist/runtime.d.ts +10 -9
- package/dist/shims/child_process.d.ts +2 -0
- package/dist/shims/crypto.d.ts +660 -119
- package/dist/shims/dns.d.ts +76 -2
- package/dist/shims/process.d.ts +1 -0
- package/dist/shims/string_decoder.d.ts +19 -3
- package/dist/shims/tls.d.ts +853 -31
- package/dist/source-edits.d.ts +7 -0
- package/package.json +1 -1
- package/src/code-transforms.ts +2 -3
- package/src/fetch-transport.ts +98 -0
- package/src/guest-timers.ts +89 -0
- package/src/http-client-transport.ts +169 -0
- package/src/index.ts +18 -0
- package/src/native-stream-binding.ts +393 -0
- package/src/native-stream-owner.ts +317 -0
- package/src/node-lib/binding/duplex_stream.ts +95 -0
- package/src/node-lib/binding/fds.ts +18 -1
- package/src/node-lib/binding/fs.ts +45 -17
- package/src/node-lib/binding/misc.ts +2 -0
- package/src/node-lib/binding/pipe_wrap.ts +35 -12
- package/src/node-lib/binding/process_wrap.ts +50 -9
- package/src/node-lib/binding/stream_wrap.ts +196 -72
- package/src/node-lib/binding/tcp_wrap.ts +43 -8
- package/src/node-lib/binding/tty_wrap.ts +135 -8
- package/src/node-lib/binding/util.ts +24 -15
- package/src/node-lib/binding/uv.ts +4 -0
- package/src/node-lib/fs-module.ts +2 -18
- package/src/node-lib/internal/navigator.js +149 -0
- package/src/node-lib/internals/buffer-and-streams.ts +50 -11
- package/src/node-lib/internals/events-util.ts +5 -0
- package/src/node-lib/internals/index.ts +12 -2
- package/src/node-lib/load.ts +170 -62
- package/src/node-lib/module-hooks.ts +5 -2
- package/src/node-lib/public-modules.ts +12 -48
- package/src/node-lib/sources.ts +4 -0
- package/src/node-lib/timers.ts +93 -0
- package/src/node-process-host.ts +124 -0
- package/src/process-registry.ts +144 -0
- package/src/process-tokens.ts +55 -6
- package/src/promise-ownership.ts +28 -0
- package/src/runtime.ts +90 -214
- package/src/shims/child_process.ts +310 -80
- package/src/shims/crypto.ts +115 -100
- package/src/shims/dns.ts +17 -1
- package/src/shims/process.ts +33 -11
- package/src/shims/string_decoder.ts +10 -6
- package/src/shims/tls.ts +49 -19
- 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 =
|
|
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 (
|
|
268
|
+
return (Bytes().from(name) as { toString(enc: string): string }).toString(encoding);
|
|
266
269
|
}
|
|
267
270
|
return name;
|
|
268
271
|
}
|
|
269
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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, () => {
|
|
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,
|