@volter/browser-wali 0.1.191 → 0.1.193

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.
@@ -0,0 +1,9 @@
1
+ interface SleepHooks {
2
+ prepare?(): void;
3
+ checkFailure?(): void;
4
+ /** Existing root pthread-failure cadence, not a guest deadline. */
5
+ failureCheckInterval?: number;
6
+ }
7
+ export declare function waliClockNanosleep(memory: () => WebAssembly.Memory, clock: number, flags: number, request: number, remaining: number, hooks?: SleepHooks): bigint;
8
+ export {};
9
+ //# sourceMappingURL=nanosleep.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nanosleep.d.ts","sourceRoot":"","sources":["../src/nanosleep.ts"],"names":[],"mappings":"AAQA,UAAU,UAAU;IAClB,OAAO,CAAC,IAAI,IAAI,CAAC;IACjB,YAAY,CAAC,IAAI,IAAI,CAAC;IACtB,mEAAmE;IACnE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAKD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,WAAW,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAChF,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,GAAE,UAAe,GAAG,MAAM,CAsDnF"}
@@ -0,0 +1,74 @@
1
+ // Clock validation and conversion extend the Emscripten clock rules in
2
+ // clocks.ts (MIT/NCSA, 862dc0d48acc4134a0088d27ac8f5dd18baba1cd).
3
+ // Linux nanosleep/clock_nanosleep interruption and remainder rules live here,
4
+ // behind syscalls; programs keep musl. No Linux implementation is copied.
5
+ import { monotonicMilliseconds, realtimeMilliseconds } from "./monotonic-clock.js";
6
+ import { hostWait } from "./host-idle.js";
7
+ import { waliSignalWaitState } from "./signal-interruption.js";
8
+ let sleepWord;
9
+ const nowNanoseconds = (clock) => BigInt(Math.round((clock === 0 ? realtimeMilliseconds() : monotonicMilliseconds()) * 1_000_000));
10
+ export function waliClockNanosleep(memory, clock, flags, request, remaining, hooks = {}) {
11
+ // RAW/coarse clocks do not provide Linux nsleep. CPU, alarm, boottime and
12
+ // TAI clocks require accounting/permissions or clock state we do not have.
13
+ if (clock !== 0 && clock !== 1)
14
+ return clock >= 2 && clock <= 11 && clock !== 3 ? -95n : -22n;
15
+ const range = (pointer) => Number.isSafeInteger(pointer) && pointer > 0
16
+ && pointer + 16 <= memory().buffer.byteLength;
17
+ if (!range(request))
18
+ return -14n;
19
+ const input = new DataView(memory().buffer);
20
+ const seconds = input.getBigInt64(request, true), nanoseconds = input.getBigInt64(request + 8, true);
21
+ if (seconds < 0n || nanoseconds < 0n || nanoseconds >= 1000000000n)
22
+ return -22n;
23
+ const absolute = Boolean(flags & 1); // Linux nsleep interprets TIMER_ABSTIME.
24
+ const time = seconds * 1000000000n + nanoseconds;
25
+ const elapsedClock = absolute ? clock : 1;
26
+ const deadline = absolute ? time : nowNanoseconds(elapsedClock) + time;
27
+ if (deadline <= nowNanoseconds(elapsedClock))
28
+ return 0n;
29
+ // A page cannot implement a synchronous blocking syscall. Never pretend it
30
+ // slept when the runtime is used directly outside a worker.
31
+ if ("document" in globalThis)
32
+ return -38n;
33
+ if (deadline - nowNanoseconds(elapsedClock) >= 1000000n)
34
+ hooks.prepare?.();
35
+ for (;;) {
36
+ hooks.checkFailure?.();
37
+ const left = deadline - nowNanoseconds(elapsedClock);
38
+ if (left <= 0n)
39
+ return 0n;
40
+ let state = waliSignalWaitState();
41
+ if (state.pending) {
42
+ // Delivery distinguishes a caught signal from a default-ignored one.
43
+ // The interval left is sampled before the handler consumes guest time.
44
+ if (state.interruptSleep?.() ?? true) {
45
+ if (!absolute && remaining !== 0) {
46
+ if (!range(remaining))
47
+ return -14n;
48
+ const output = new DataView(memory().buffer);
49
+ output.setBigInt64(remaining, left / 1000000000n, true);
50
+ output.setBigInt64(remaining + 8, left % 1000000000n, true);
51
+ }
52
+ return -4n;
53
+ }
54
+ state = waliSignalWaitState();
55
+ }
56
+ const signal = state.guards[0];
57
+ const word = signal?.words ?? (sleepWord ??= new Int32Array(new SharedArrayBuffer(4)));
58
+ const index = signal?.index ?? 0, generation = signal?.value ?? Atomics.load(word, index);
59
+ const afterDelivery = deadline - nowNanoseconds(elapsedClock);
60
+ if (afterDelivery <= 0n)
61
+ return 0n;
62
+ const duration = Number(afterDelivery) / 1_000_000;
63
+ // Preserve the existing root fatal-thread observation cadence. Pthreads
64
+ // need no such poll and wait directly for signal generation or deadline.
65
+ const wait = Math.min(duration, hooks.failureCheckInterval ?? Infinity);
66
+ hostWait(word, index, generation, wait, undefined, {
67
+ resource: "clock", deadline: Date.now() + duration,
68
+ guards: state.guards, interrupt: { words: word, index },
69
+ });
70
+ // Generation wakes include ignored/masked signals and owner work; only a
71
+ // handler/default termination interrupts. Spurious wakes recheck deadline.
72
+ }
73
+ }
74
+ //# sourceMappingURL=nanosleep.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nanosleep.js","sourceRoot":"","sources":["../src/nanosleep.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"network-host.d.ts","sourceRoot":"","sources":["../src/network-host.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,oBAAoB,EAG1B,MAAM,uBAAuB,CAAC;AAmF/B,8EAA8E;AAC9E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG;IAC3E,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC;CAC1D,CA42CA"}
1
+ {"version":3,"file":"network-host.d.ts","sourceRoot":"","sources":["../src/network-host.ts"],"names":[],"mappings":"AAOA,OAAO,EAKL,KAAK,oBAAoB,EAG1B,MAAM,uBAAuB,CAAC;AAqF/B,8EAA8E;AAC9E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG;IAC3E,OAAO,CAAC,KAAK,EAAE,YAAY,CAAC,oBAAoB,CAAC,GAAG,IAAI,CAAC;CAC1D,CAs7CA"}
@@ -5,7 +5,7 @@
5
5
  // process ownership and port arbitration remain this kernel's adapters.
6
6
  import { WaliEpollSet } from "./epoll-ready.js";
7
7
  import { waliEpollNestingError } from "./epoll-nesting.js";
8
- import { NETWORK_FD_MIN, NETWORK_PAYLOAD_BYTES, NetworkOperation, } from "./network-protocol.js";
8
+ import { NETWORK_FD_MIN, NETWORK_PAYLOAD_BYTES, NETWORK_RESPONSE_NO_RESTART, NetworkOperation, } from "./network-protocol.js";
9
9
  import { WaliChildProcesses } from "./child-processes.js";
10
10
  import { WaliSignals } from "./signals.js";
11
11
  import { pollWaliSharedPipeState, waliSharedPipeGeneration } from "./shared-pipes.js";
@@ -157,11 +157,12 @@ export function createWaliNetworkSession(reply) {
157
157
  // Reaping and completed I/O are irreversible. Claim blocking requests
158
158
  // before it starts, so transport cancellation cannot lose status or bytes.
159
159
  const operation = Atomics.load(control, 1);
160
- const claimed = [NetworkOperation.ChildWait, NetworkOperation.PollMany, NetworkOperation.EpollWait, NetworkOperation.Receive].includes(operation)
160
+ const claimed = [NetworkOperation.ChildWait, NetworkOperation.PollMany, NetworkOperation.EpollWait, NetworkOperation.Receive, NetworkOperation.Accept].includes(operation)
161
161
  || (operation === NetworkOperation.Send && sockets.get(Atomics.load(control, 5))?.kind === "event");
162
162
  if (claimed && Atomics.compareExchange(control, 0, requestId, -requestId) !== requestId)
163
163
  return;
164
164
  let result = -38;
165
+ let restartable = true;
165
166
  let output = new Uint8Array();
166
167
  let checkedActivity = activity ? Atomics.load(activity, 0) : 0;
167
168
  const parked = (state) => {
@@ -178,7 +179,9 @@ export function createWaliNetworkSession(reply) {
178
179
  const inputLength = Atomics.load(control, 3);
179
180
  const args = Array.from({ length: 10 }, (_, index) => Atomics.load(control, 5 + index));
180
181
  const input = payload.slice(0, inputLength);
181
- ({ result, output } = await operate(operation, args, input, process, parked));
182
+ const response = await operate(operation, args, input, process, parked);
183
+ ({ result, output } = response);
184
+ restartable = response.restartable !== false;
182
185
  if ([NetworkOperation.EpollControl, NetworkOperation.Send, NetworkOperation.Receive,
183
186
  NetworkOperation.Close, NetworkOperation.Shutdown, NetworkOperation.TimerFdSetTime].includes(operation))
184
187
  notifyDescriptors();
@@ -202,6 +205,7 @@ export function createWaliNetworkSession(reply) {
202
205
  payload.set(output);
203
206
  Atomics.store(control, 2, result);
204
207
  Atomics.store(control, 4, output.byteLength);
208
+ Atomics.store(control, NETWORK_RESPONSE_NO_RESTART, restartable ? 0 : 1);
205
209
  Atomics.store(control, 0, requestId + 1);
206
210
  Atomics.notify(control, 0);
207
211
  }
@@ -293,7 +297,7 @@ export function createWaliNetworkSession(reply) {
293
297
  if (operation === NetworkOperation.Listen)
294
298
  return scalar(await listenSocket(socket, args[1]));
295
299
  if (operation === NetworkOperation.Accept)
296
- return acceptSocket(socket, args[1]);
300
+ return acceptSocket(socket, args[1], signalMask(args[2] ?? 0, args[3] ?? 0), parked);
297
301
  if (operation === NetworkOperation.Send) {
298
302
  if ((args[1] & ~(0x40 | 0x4000)) !== 0)
299
303
  return scalar(-95);
@@ -320,8 +324,7 @@ export function createWaliNetworkSession(reply) {
320
324
  if (operation === NetworkOperation.GetSocketOption)
321
325
  return socketOption(socket, args[1], args[2]);
322
326
  if (operation === NetworkOperation.SetSocketOption) {
323
- // Never claim to apply an option that the relay transport cannot honor.
324
- return scalar(socket.kind === "event" || socket.kind === "timer" ? -88 : -92);
327
+ return scalar(setSocketOption(socket, args[1], args[2], input));
325
328
  }
326
329
  if (operation === NetworkOperation.Duplicate)
327
330
  return scalar(duplicateSocket(socket));
@@ -392,7 +395,9 @@ export function createWaliNetworkSession(reply) {
392
395
  const done = () => { if (timer !== undefined)
393
396
  clearTimeout(timer); descriptorWaiters.delete(done); resolve(); };
394
397
  descriptorWaiters.add(done);
395
- const timer = Number.isFinite(delay) ? setTimeout(done, Math.max(0, delay)) : undefined;
398
+ // Browser timers use signed-int32 milliseconds. A long guest deadline is
399
+ // rechecked after each interval; this cap is not an I/O watchdog.
400
+ const timer = Number.isFinite(delay) ? setTimeout(done, Math.min(0x7fff_ffff, Math.max(0, delay))) : undefined;
396
401
  });
397
402
  }
398
403
  function duplicateEpoll(fd, minimum, closeOnExec = false) {
@@ -715,7 +720,6 @@ export function createWaliNetworkSession(reply) {
715
720
  listening: false,
716
721
  backlog: 0,
717
722
  pending: [],
718
- acceptWaiters: [],
719
723
  peerClosed: false,
720
724
  };
721
725
  }
@@ -951,32 +955,48 @@ export function createWaliNetworkSession(reply) {
951
955
  listenersByPort.set(socket.bound.port, socket);
952
956
  return 0;
953
957
  }
954
- async function acceptSocket(socket, flags) {
958
+ async function acceptSocket(socket, flags, mask, parked) {
955
959
  if (socket.kind !== "tcp" || !socket.listening)
956
960
  return scalar(-22);
957
961
  if ((flags & ~0x8_0800) !== 0)
958
962
  return scalar(-22);
959
- let accepted = socket.pending.shift();
960
- if (!accepted && socket.nonblocking)
961
- return scalar(-11);
962
- if (!accepted) {
963
- accepted = await new Promise((resolve) => socket.acceptWaiters.push(resolve));
964
- }
965
- if (!accepted)
966
- return scalar(-9);
967
- accepted.nonblocking = Boolean(flags & 0x800);
968
- let fd;
963
+ const deadline = receiveDeadline(socket);
964
+ activeIO.set(socket, (activeIO.get(socket) ?? 0) + 1);
969
965
  try {
970
- fd = takeFd();
966
+ for (;;) {
967
+ if (disposed || socket.closed)
968
+ return scalar(-9);
969
+ const accepted = socket.pending.shift();
970
+ if (accepted) {
971
+ accepted.nonblocking = Boolean(flags & 0x800);
972
+ let fd;
973
+ try {
974
+ fd = takeFd();
975
+ }
976
+ catch (error) {
977
+ socket.pending.unshift(accepted);
978
+ throw error;
979
+ }
980
+ sockets.set(fd, accepted);
981
+ socketDescriptorFlags.set(fd, flags & 0x80000 ? 1 : 0);
982
+ return { result: fd, output: socketAddress(accepted.domain, accepted.domain === 10 ? "::" : "0.0.0.0", 0) };
983
+ }
984
+ if (socket.nonblocking || monotonicMilliseconds() >= deadline)
985
+ return scalar(-11);
986
+ if (signals && (signals.pending() & ~mask) !== 0n)
987
+ return { ...scalar(-4), restartable: deadline === Infinity };
988
+ parked(2); // listener ingress or configured deadline can wake the call
989
+ try {
990
+ await waitForDescriptorChange(deadline - monotonicMilliseconds());
991
+ }
992
+ finally {
993
+ parked(0);
994
+ }
995
+ }
971
996
  }
972
- catch (error) {
973
- socket.pending.unshift(accepted);
974
- throw error;
997
+ finally {
998
+ releaseActiveIO(socket);
975
999
  }
976
- sockets.set(fd, accepted);
977
- socketDescriptorFlags.set(fd, flags & 0x80000 ? 1 : 0);
978
- // The ingress wire does not yet carry the remote endpoint. Do not invent it.
979
- return { result: fd, output: socketAddress(accepted.domain, accepted.domain === 10 ? "::" : "0.0.0.0", 0) };
980
1000
  }
981
1001
  function requestLoopbackListen(port) {
982
1002
  if (!loopbackPort)
@@ -1058,17 +1078,14 @@ export function createWaliNetworkSession(reply) {
1058
1078
  listening: false,
1059
1079
  backlog: 0,
1060
1080
  pending: [],
1061
- acceptWaiters: [],
1062
1081
  loopbackConnectionId: message.connectionId,
1063
1082
  peerClosed: false,
1064
1083
  ...(listener.bound ? { bound: listener.bound } : {}),
1084
+ // Clone listener options when the connection is created, not at accept.
1085
+ ...(listener.receiveTimeoutMicros !== undefined ? { receiveTimeoutMicros: listener.receiveTimeoutMicros } : {}),
1065
1086
  };
1066
1087
  loopbackConnections.set(message.connectionId, accepted);
1067
- const waiter = listener.acceptWaiters.shift();
1068
- if (waiter)
1069
- waiter(accepted);
1070
- else
1071
- listener.pending.push(accepted);
1088
+ listener.pending.push(accepted);
1072
1089
  notifyDescriptors();
1073
1090
  }
1074
1091
  function postLoopback(message) {
@@ -1183,35 +1200,90 @@ export function createWaliNetworkSession(reply) {
1183
1200
  }
1184
1201
  }
1185
1202
  }
1186
- /** A readiness wait, not a polling loop or a transport watchdog. */
1203
+ /** Linux recv(2) WAITALL supplements the Emscripten socket rules (L/M).
1204
+ * A readiness wait, not a polling loop or a transport watchdog. Only one
1205
+ * pinned description participates, including while an alias is closed.
1206
+ */
1187
1207
  async function receiveSocketBlocking(socket, maximum, flags, mask, parked) {
1208
+ const MSG_PEEK = 2, MSG_DONTWAIT = 0x40, MSG_WAITALL = 0x100;
1188
1209
  if (maximum < 0)
1189
1210
  return scalar(-22);
1190
- // Receive only supported flags; no false claim of WAITALL/ancillary/OOB.
1191
- if (flags & ~(2 | 0x40))
1211
+ if (flags & ~(MSG_PEEK | MSG_DONTWAIT | MSG_WAITALL))
1212
+ return scalar(-95);
1213
+ const waitAll = Boolean(flags & MSG_WAITALL) && (socket.kind === "tcp" || socket.kind === "local");
1214
+ if ((flags & MSG_WAITALL) && socket.kind !== "dns" && !waitAll)
1192
1215
  return scalar(-95);
1193
- // Linux I/O pins the open description, even if another thread closes
1194
- // the descriptor while the call is waiting. Close transport only at last use.
1216
+ if (waitAll && maximum > NETWORK_PAYLOAD_BYTES)
1217
+ return scalar(-95);
1218
+ const target = Math.min(maximum, NETWORK_PAYLOAD_BYTES);
1219
+ const peek = Boolean(flags & MSG_PEEK);
1220
+ const chunks = waitAll && !peek ? [] : undefined;
1221
+ let collected = 0;
1222
+ let peeked;
1223
+ const partial = (fallback) => {
1224
+ if (peeked)
1225
+ return peeked;
1226
+ if (!collected)
1227
+ return fallback;
1228
+ if (chunks.length === 1)
1229
+ return { result: collected, output: chunks[0] };
1230
+ const output = new Uint8Array(collected);
1231
+ let offset = 0;
1232
+ for (const chunk of chunks) {
1233
+ output.set(chunk, offset);
1234
+ offset += chunk.byteLength;
1235
+ }
1236
+ return { result: collected, output };
1237
+ };
1238
+ const deadline = receiveDeadline(socket);
1195
1239
  activeIO.set(socket, (activeIO.get(socket) ?? 0) + 1);
1196
1240
  try {
1197
1241
  for (;;) {
1198
1242
  if (disposed)
1199
- return scalar(-9);
1200
- // A stream may return fewer bytes than requested. Bound consumption to
1201
- // this channel's output capacity so a large read cannot lose queued bytes.
1202
- const response = receiveSocket(socket, Math.min(maximum, NETWORK_PAYLOAD_BYTES), flags);
1203
- if (response.result !== -11 || socket.nonblocking || Boolean(flags & 0x40))
1204
- return response;
1205
- if (signals && (signals.pending() & ~mask) !== 0n)
1206
- return scalar(-4);
1207
- let delay = Infinity;
1243
+ return partial(scalar(-9));
1244
+ // Return bytes already read without clearing a later socket error.
1245
+ // The next receive/SO_ERROR must still be able to observe that error.
1246
+ if (collected > 0 && socket.kind === "tcp" && socket.error)
1247
+ return partial(scalar(0));
1248
+ const response = receiveSocket(socket, target - collected, flags);
1249
+ if (response.result > 0) {
1250
+ if (!waitAll)
1251
+ return response;
1252
+ const ended = socket.kind === "local" ? socket.closed || socket.readShutdown
1253
+ : socket.kind === "tcp" && (socket.closed || socket.peerClosed || socket.error !== 0);
1254
+ if (peek) {
1255
+ // Resample the queue from its beginning; never concatenate a second
1256
+ // copy of bytes a previous peek deliberately left queued.
1257
+ peeked = response;
1258
+ if (response.result >= target || ended)
1259
+ return response;
1260
+ }
1261
+ else {
1262
+ chunks.push(response.output);
1263
+ collected += response.result;
1264
+ if (collected >= target || ended)
1265
+ return partial(response);
1266
+ }
1267
+ }
1268
+ else if (response.result !== -11)
1269
+ return partial(response);
1270
+ // WAITALL does not turn a nonblocking descriptor into a blocking one.
1271
+ if (socket.nonblocking || Boolean(flags & MSG_DONTWAIT))
1272
+ return partial(response);
1273
+ if (monotonicMilliseconds() >= deadline)
1274
+ return partial(scalar(-11));
1275
+ if (signals && (signals.pending() & ~mask) !== 0n) {
1276
+ const response = partial(scalar(-4));
1277
+ return response.result === -4 ? { ...response, restartable: deadline === Infinity } : response;
1278
+ }
1279
+ let delay = deadline - monotonicMilliseconds();
1208
1280
  if (socket.kind === "timer" && socket.deadlineNanoseconds !== undefined) {
1209
- delay = Number(timerRemaining(socket)) / 1_000_000;
1281
+ delay = Math.min(delay, Number(timerRemaining(socket)) / 1_000_000);
1210
1282
  }
1211
- parked(socket.kind === "tcp" || socket.kind === "dns"
1283
+ parked(deadline !== Infinity || socket.kind === "tcp" || socket.kind === "dns"
1212
1284
  || (socket.kind === "timer" && socket.deadlineNanoseconds !== undefined) ? 2 : 1);
1213
- // The owner installs the wake before yielding; events arriving while the
1214
- // request is being serviced cannot slip between the scan and subscription.
1285
+ // Install the wake before yielding; arrivals cannot slip between the
1286
+ // scan and subscription. Signals use the existing caller-mask guard.
1215
1287
  try {
1216
1288
  await waitForDescriptorChange(delay);
1217
1289
  }
@@ -1387,6 +1459,30 @@ export function createWaliNetworkSession(reply) {
1387
1459
  socketDescriptorFlags.set(fd, closeOnExec ? 1 : 0);
1388
1460
  return fd;
1389
1461
  }
1462
+ /** Linux socket(7) timeouts use the existing owner, not transport watchdogs. */
1463
+ function receiveDeadline(socket) {
1464
+ const timeout = socket.kind === "event" || socket.kind === "timer" ? undefined : socket.receiveTimeoutMicros;
1465
+ return timeout === undefined ? Infinity : monotonicMilliseconds() + Number(timeout) / 1000;
1466
+ }
1467
+ function setSocketOption(socket, level, name, input) {
1468
+ if (socket.kind === "event" || socket.kind === "timer")
1469
+ return -88;
1470
+ if (level !== 1 || (name !== 20 && name !== 66))
1471
+ return -92;
1472
+ if (input.byteLength < 16)
1473
+ return -22;
1474
+ const view = new DataView(input.buffer, input.byteOffset, input.byteLength);
1475
+ const seconds = view.getBigInt64(0, true), micros = view.getBigInt64(8, true);
1476
+ if (micros < 0n || micros >= 1000000n)
1477
+ return -33; // EDOM, sock_set_timeout
1478
+ if (seconds < 0n)
1479
+ socket.receiveTimeoutMicros = 0n;
1480
+ else if (seconds === 0n && micros === 0n)
1481
+ delete socket.receiveTimeoutMicros;
1482
+ else
1483
+ socket.receiveTimeoutMicros = seconds * 1000000n + micros;
1484
+ return 0;
1485
+ }
1390
1486
  // libsyscall.js only supports SO_ERROR. Additional read-only Linux queries
1391
1487
  // below expose actual owner state, never a fabricated successful setting.
1392
1488
  function socketOption(socket, level, name) {
@@ -1394,6 +1490,13 @@ export function createWaliNetworkSession(reply) {
1394
1490
  return scalar(-88);
1395
1491
  if (level !== 1)
1396
1492
  return scalar(-92);
1493
+ if (name === 20 || name === 66) {
1494
+ const duration = socket.receiveTimeoutMicros ?? 0n;
1495
+ const output = new Uint8Array(16), view = new DataView(output.buffer);
1496
+ view.setBigInt64(0, duration / 1000000n, true);
1497
+ view.setBigInt64(8, duration % 1000000n, true);
1498
+ return { result: 0, output };
1499
+ }
1397
1500
  let value;
1398
1501
  switch (name) {
1399
1502
  case 3:
@@ -1460,8 +1563,6 @@ export function createWaliNetworkSession(reply) {
1460
1563
  if (socket.listening && socket.bound) {
1461
1564
  listenersByPort.delete(socket.bound.port);
1462
1565
  postLoopback({ type: "unlisten", port: socket.bound.port });
1463
- for (const resolve of socket.acceptWaiters.splice(0))
1464
- resolve(undefined);
1465
1566
  for (const accepted of socket.pending.splice(0))
1466
1567
  closeSocket(accepted);
1467
1568
  }