@ai-ecoverse/slicc-kernel 1.37.0 → 1.38.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.
@@ -2032,9 +2032,80 @@ var SIGNAL_BY_NAME = {
2032
2032
  // src/body-idle.ts
2033
2033
  var BODY_IDLE_MS = 5 * 60 * 1e3;
2034
2034
 
2035
+ // src/kernel/net/ws-queue.ts
2036
+ var INBOUND_LIMIT = 16 * 1024 * 1024;
2037
+ var MESSAGE_LIMIT = 64 * 1024 * 1024;
2038
+ var TOO_BIG = 1009;
2039
+ function payloadSize(message2) {
2040
+ return typeof message2 === "string" ? new TextEncoder().encode(message2).length : message2.length;
2041
+ }
2042
+ var MessageQueue = class {
2043
+ items = [];
2044
+ limit;
2045
+ waiter;
2046
+ size = 0;
2047
+ ended = false;
2048
+ overflow;
2049
+ taken;
2050
+ closed;
2051
+ settle;
2052
+ constructor(overflow, limit = INBOUND_LIMIT, taken) {
2053
+ this.overflow = overflow;
2054
+ this.limit = limit;
2055
+ this.taken = taken;
2056
+ this.closed = new Promise((resolve) => {
2057
+ this.settle = resolve;
2058
+ });
2059
+ }
2060
+ push(message2) {
2061
+ if (this.ended) return;
2062
+ const size = payloadSize(message2);
2063
+ const huge = size > MESSAGE_LIMIT;
2064
+ if (huge || this.items.length > 0 && this.size + size > this.limit) {
2065
+ const reason = huge ? `a message is over ${MESSAGE_LIMIT} bytes` : `over ${this.limit} bytes of messages are waiting`;
2066
+ this.end({ code: TOO_BIG, reason });
2067
+ this.overflow();
2068
+ return;
2069
+ }
2070
+ this.size += size;
2071
+ this.items.push(message2);
2072
+ this.wake();
2073
+ }
2074
+ end(close) {
2075
+ if (this.ended) return;
2076
+ this.ended = true;
2077
+ this.settle(close);
2078
+ this.wake();
2079
+ }
2080
+ wake() {
2081
+ const waiter = this.waiter;
2082
+ this.waiter = void 0;
2083
+ waiter?.();
2084
+ }
2085
+ async *[Symbol.asyncIterator]() {
2086
+ for (; ; ) {
2087
+ const next = this.items.shift();
2088
+ if (next !== void 0) {
2089
+ const n = payloadSize(next);
2090
+ this.size -= n;
2091
+ this.taken?.(n);
2092
+ yield next;
2093
+ continue;
2094
+ }
2095
+ if (this.ended) return;
2096
+ await new Promise((resolve) => {
2097
+ this.waiter = resolve;
2098
+ });
2099
+ }
2100
+ }
2101
+ };
2102
+
2035
2103
  // src/transport.ts
2104
+ var SEND_BUFFER = 1024 * 1024;
2105
+ var CLOSE_WAIT_MS = 5e3;
2106
+ var NO_WEBSOCKET = "WebSocket not supported by this transport (fetchTransport needs { webSocket: true })";
2036
2107
  var MAX_REQUEST_BODY = 64 * 1024 * 1024;
2037
- function serveTransport(peer, transport) {
2108
+ function serveTransport(peer, transport, { closeWaitMs = CLOSE_WAIT_MS } = {}) {
2038
2109
  const open = /* @__PURE__ */ new Map();
2039
2110
  const fail = (nid, e) => {
2040
2111
  open.delete(nid);
@@ -2075,6 +2146,96 @@ function serveTransport(peer, transport) {
2075
2146
  fail(call.nid, e);
2076
2147
  }
2077
2148
  };
2149
+ const sockets = /* @__PURE__ */ new Map();
2150
+ const credit = /* @__PURE__ */ new Map();
2151
+ const refill = (nid, n, closing = false) => {
2152
+ const flow = credit.get(nid);
2153
+ if (!flow) return;
2154
+ flow.inflight -= n;
2155
+ if (closing && !flow.closing) {
2156
+ flow.closing = true;
2157
+ flow.deadline();
2158
+ }
2159
+ flow.wake?.();
2160
+ };
2161
+ const openSocket = async (call) => {
2162
+ const abort = new AbortController();
2163
+ open.set(call.nid, { abort });
2164
+ try {
2165
+ if (!transport.websocket) throw new Error(NO_WEBSOCKET);
2166
+ const socket = await transport.websocket({
2167
+ url: call.url,
2168
+ protocols: call.protocols,
2169
+ headers: call.headers,
2170
+ signal: abort.signal
2171
+ });
2172
+ if (!open.delete(call.nid)) {
2173
+ socket.close(1e3, "cancelled");
2174
+ return;
2175
+ }
2176
+ sockets.set(call.nid, socket);
2177
+ peer.postMessage({ net: "ws-opened", nid: call.nid, protocol: socket.protocol });
2178
+ void relay(call.nid, socket);
2179
+ } catch (e) {
2180
+ fail(call.nid, e);
2181
+ }
2182
+ };
2183
+ const relay = async (nid, socket) => {
2184
+ let ending;
2185
+ let late;
2186
+ const unanswered = new Promise((resolve) => {
2187
+ late = () => resolve("late");
2188
+ });
2189
+ const flow = {
2190
+ inflight: 0,
2191
+ closing: false,
2192
+ deadline: () => AbortSignal.timeout(closeWaitMs).addEventListener("abort", late, { once: true })
2193
+ };
2194
+ credit.set(nid, flow);
2195
+ const timedOut = { code: 1006, reason: "the far end did not answer the close" };
2196
+ let oversize = false;
2197
+ try {
2198
+ const messages = socket.messages[Symbol.asyncIterator]();
2199
+ for (; ; ) {
2200
+ const next = await Promise.race([messages.next(), unanswered]);
2201
+ if (next === "late" || next.done) break;
2202
+ const data = next.value;
2203
+ if (flow.closing) continue;
2204
+ if (payloadSize(data) > MESSAGE_LIMIT) {
2205
+ oversize = true;
2206
+ break;
2207
+ }
2208
+ if (typeof data === "string") peer.postMessage({ net: "ws-message", nid, data });
2209
+ else {
2210
+ const bytes = data.slice();
2211
+ peer.postMessage({ net: "ws-message", nid, data: bytes }, [bytes.buffer]);
2212
+ }
2213
+ flow.inflight += payloadSize(data);
2214
+ while (flow.inflight > INBOUND_LIMIT && !flow.closing) {
2215
+ await new Promise((resolve) => {
2216
+ flow.wake = resolve;
2217
+ });
2218
+ }
2219
+ }
2220
+ if (oversize) socket.close(1e3, "a message is too big");
2221
+ ending = oversize ? { code: 1009, reason: `a message is over ${MESSAGE_LIMIT} bytes` } : await Promise.race([socket.closed, unanswered.then(() => timedOut)]);
2222
+ } catch (e) {
2223
+ socket.close(1e3, "the relay failed");
2224
+ ending = { code: 1011, reason: e instanceof Error ? e.message : String(e) };
2225
+ }
2226
+ sockets.delete(nid);
2227
+ credit.delete(nid);
2228
+ peer.postMessage({ net: "ws-closed", nid, ...ending });
2229
+ };
2230
+ const sendOn = async (call) => {
2231
+ const socket = sockets.get(call.nid);
2232
+ if (!socket) return;
2233
+ socket.send(call.data);
2234
+ while (socket.buffered > SEND_BUFFER && sockets.has(call.nid)) {
2235
+ await new Promise((resolve) => setTimeout(resolve, 10));
2236
+ }
2237
+ peer.postMessage({ net: "ws-acked", nid: call.nid, n: payloadSize(call.data) });
2238
+ };
2078
2239
  const read = async (nid) => {
2079
2240
  const body = open.get(nid)?.body;
2080
2241
  if (!body) return;
@@ -2092,6 +2253,8 @@ function serveTransport(peer, transport) {
2092
2253
  }
2093
2254
  };
2094
2255
  const drop = (nid) => {
2256
+ sockets.get(nid)?.close(1e3, "cancelled");
2257
+ refill(nid, 0, true);
2095
2258
  const entry = open.get(nid);
2096
2259
  open.delete(nid);
2097
2260
  entry?.abort.abort();
@@ -2101,10 +2264,16 @@ function serveTransport(peer, transport) {
2101
2264
  answer(call) {
2102
2265
  if (call.net === "fetch") void start(call);
2103
2266
  else if (call.net === "read") void read(call.nid);
2267
+ else if (call.net === "ws-open") void openSocket(call);
2268
+ else if (call.net === "ws-send") void sendOn(call);
2269
+ else if (call.net === "ws-close") {
2270
+ sockets.get(call.nid)?.close(call.code, call.reason);
2271
+ refill(call.nid, 0, true);
2272
+ } else if (call.net === "ws-consumed") refill(call.nid, call.n);
2104
2273
  else drop(call.nid);
2105
2274
  },
2106
2275
  close() {
2107
- for (const nid of [...open.keys()]) drop(nid);
2276
+ for (const nid of [...open.keys(), ...sockets.keys()]) drop(nid);
2108
2277
  }
2109
2278
  };
2110
2279
  }
@@ -2494,6 +2663,10 @@ var Incoming = class {
2494
2663
  this.buf = this.buf.subarray(n);
2495
2664
  return out;
2496
2665
  }
2666
+ async peek(signal) {
2667
+ if (this.buf.length === 0) await this.fill(signal);
2668
+ return this.buf[0];
2669
+ }
2497
2670
  async some(max, signal) {
2498
2671
  if (this.buf.length > 0) return this.take(Math.min(max, this.buf.length));
2499
2672
  if (this.eof) return new Uint8Array(0);
@@ -2675,13 +2848,219 @@ function chunk(bytes) {
2675
2848
  }
2676
2849
  var LAST_CHUNK = latin1Bytes("0\r\n\r\n");
2677
2850
 
2851
+ // src/cdp/websocket.ts
2852
+ var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
2853
+ var MAX_MESSAGE = 256 * 1024 * 1024;
2854
+ var CLOSE = {
2855
+ normal: 1e3,
2856
+ protocol: 1002,
2857
+ unsupported: 1003,
2858
+ invalid: 1007,
2859
+ tooBig: 1009,
2860
+ error: 1011
2861
+ };
2862
+ var OP_TEXT = 1;
2863
+ var OP_BINARY = 2;
2864
+ var OP = {
2865
+ continuation: 0,
2866
+ text: OP_TEXT,
2867
+ binary: OP_BINARY,
2868
+ close: 8,
2869
+ ping: 9,
2870
+ pong: 10
2871
+ };
2872
+ var WsError = class extends Error {
2873
+ code;
2874
+ constructor(code, message2) {
2875
+ super(message2);
2876
+ this.code = code;
2877
+ }
2878
+ };
2879
+ function sendableCode(code) {
2880
+ if (code >= 3e3 && code <= 4999) return true;
2881
+ return code >= 1e3 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006;
2882
+ }
2883
+ function field(headers, name2) {
2884
+ return headers.find(([n]) => n.toLowerCase() === name2)?.[1];
2885
+ }
2886
+ async function acceptKey(headers) {
2887
+ const tokens = (name2) => (field(headers, name2) ?? "").split(",").map((t) => t.trim().toLowerCase());
2888
+ if (!tokens("upgrade").includes("websocket") || !tokens("connection").includes("upgrade")) {
2889
+ throw new HttpError(400, "expected a WebSocket upgrade");
2890
+ }
2891
+ if (field(headers, "sec-websocket-version") !== "13") {
2892
+ throw new HttpError(400, "Sec-WebSocket-Version must be 13");
2893
+ }
2894
+ const key = field(headers, "sec-websocket-key") ?? "";
2895
+ if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) throw new HttpError(400, "bad Sec-WebSocket-Key");
2896
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(key + GUID));
2897
+ return btoa(String.fromCharCode(...new Uint8Array(digest)));
2898
+ }
2899
+ function isUpgrade(headers) {
2900
+ return (field(headers, "upgrade") ?? "").toLowerCase().includes("websocket");
2901
+ }
2902
+ function frame(opcode, payload) {
2903
+ const n = payload.length;
2904
+ const size = n < 126 ? 2 : n < 65536 ? 4 : 10;
2905
+ const out = new Uint8Array(size + n);
2906
+ const view = new DataView(out.buffer);
2907
+ out[0] = 128 | opcode;
2908
+ if (n < 126) out[1] = n;
2909
+ else if (n < 65536) {
2910
+ out[1] = 126;
2911
+ view.setUint16(2, n);
2912
+ } else {
2913
+ out[1] = 127;
2914
+ view.setBigUint64(2, BigInt(n));
2915
+ }
2916
+ out.set(payload, size);
2917
+ return out;
2918
+ }
2919
+ function closeFrame(code, reason = "") {
2920
+ let bytes = new TextEncoder().encode(reason);
2921
+ if (bytes.length > 123) {
2922
+ let end = 123;
2923
+ while ((bytes[end] & 192) === 128) end--;
2924
+ bytes = bytes.subarray(0, end);
2925
+ }
2926
+ const payload = new Uint8Array(2 + bytes.length);
2927
+ new DataView(payload.buffer).setUint16(0, code);
2928
+ payload.set(bytes, 2);
2929
+ return frame(OP.close, payload);
2930
+ }
2931
+ function pongFrame(payload) {
2932
+ return frame(OP.pong, payload);
2933
+ }
2934
+ async function fill(incoming, out, signal) {
2935
+ for (let at = 0; at < out.length; ) {
2936
+ const piece = await incoming.some(out.length - at, signal);
2937
+ if (piece.length === 0) throw new WsError(CLOSE.protocol, "the connection closed mid-frame");
2938
+ out.set(piece, at);
2939
+ at += piece.length;
2940
+ }
2941
+ }
2942
+ var fatal = new TextDecoder("utf-8", { fatal: true });
2943
+ function decode(bytes) {
2944
+ try {
2945
+ return fatal.decode(bytes);
2946
+ } catch {
2947
+ throw new WsError(CLOSE.invalid, "a text message is not UTF-8");
2948
+ }
2949
+ }
2950
+ var WsReader = class {
2951
+ incoming;
2952
+ max;
2953
+ parts = [];
2954
+ size = 0;
2955
+ continuing = false;
2956
+ binaryMessage = false;
2957
+ binary;
2958
+ constructor(incoming, max = MAX_MESSAGE, binary) {
2959
+ this.incoming = incoming;
2960
+ this.max = max;
2961
+ this.binary = binary;
2962
+ }
2963
+ async next(signal) {
2964
+ for (; ; ) {
2965
+ const first = await this.incoming.some(1, signal);
2966
+ if (first.length === 0) return null;
2967
+ const head = new Uint8Array(2);
2968
+ head[0] = first[0];
2969
+ await fill(this.incoming, head.subarray(1), signal);
2970
+ const fin = (head[0] & 128) !== 0;
2971
+ const opcode = head[0] & 15;
2972
+ if ((head[0] & 112) !== 0) throw new WsError(CLOSE.protocol, "reserved bits are set");
2973
+ if ((head[1] & 128) === 0) throw new WsError(CLOSE.protocol, "client frames must be masked");
2974
+ const length2 = await this.length(head[1] & 127, signal);
2975
+ const control = opcode >= 8;
2976
+ if (control && (length2 > 125 || !fin)) {
2977
+ throw new WsError(CLOSE.protocol, "a control frame is too long or fragmented");
2978
+ }
2979
+ if (!control && this.size + length2 > this.max) {
2980
+ throw new WsError(CLOSE.tooBig, `a message is over ${this.max} bytes`);
2981
+ }
2982
+ const mask = new Uint8Array(4);
2983
+ await fill(this.incoming, mask, signal);
2984
+ const payload = new Uint8Array(length2);
2985
+ await fill(this.incoming, payload, signal);
2986
+ for (let i = 0; i < length2; i++) payload[i] ^= mask[i & 3];
2987
+ const event = this.take(opcode, fin, payload);
2988
+ if (event) return event;
2989
+ }
2990
+ }
2991
+ data(opcode, fin, payload) {
2992
+ if (opcode === OP.continuation !== this.continuing) {
2993
+ throw new WsError(CLOSE.protocol, "unexpected continuation frame");
2994
+ }
2995
+ if (opcode !== OP.continuation) this.binaryMessage = opcode === OP.binary;
2996
+ this.parts.push(payload);
2997
+ this.size += payload.length;
2998
+ this.continuing = !fin;
2999
+ if (!fin) return void 0;
3000
+ const whole = this.parts.length === 1 ? payload : concat(this.parts, this.size);
3001
+ this.parts = [];
3002
+ this.size = 0;
3003
+ return this.binaryMessage ? { kind: "binary", bytes: whole } : { kind: "text", text: decode(whole) };
3004
+ }
3005
+ async length(short, signal) {
3006
+ if (short < 126) return short;
3007
+ const ext = new Uint8Array(short === 126 ? 2 : 8);
3008
+ await fill(this.incoming, ext, signal);
3009
+ const view = new DataView(ext.buffer);
3010
+ if (short === 126) return view.getUint16(0);
3011
+ const long = view.getBigUint64(0);
3012
+ return long > BigInt(this.max) ? this.max + 1 : Number(long);
3013
+ }
3014
+ take(opcode, fin, payload) {
3015
+ switch (opcode) {
3016
+ case OP.ping:
3017
+ return { kind: "ping", payload };
3018
+ case OP.pong:
3019
+ return void 0;
3020
+ case OP.close: {
3021
+ if (payload.length === 1) throw new WsError(CLOSE.protocol, "a close frame is malformed");
3022
+ const code = payload.length >= 2 ? new DataView(payload.buffer).getUint16(0) : 1005;
3023
+ if (payload.length >= 2 && !sendableCode(code)) {
3024
+ throw new WsError(CLOSE.protocol, `close code ${code} may not be sent`);
3025
+ }
3026
+ return { kind: "close", code, reason: decode(payload.subarray(2)) };
3027
+ }
3028
+ case OP.binary:
3029
+ if (!this.binary) throw new WsError(CLOSE.unsupported, "CDP messages are text");
3030
+ return this.data(opcode, fin, payload);
3031
+ case OP.text:
3032
+ case OP.continuation:
3033
+ return this.data(opcode, fin, payload);
3034
+ default:
3035
+ throw new WsError(CLOSE.protocol, `unknown opcode ${opcode}`);
3036
+ }
3037
+ }
3038
+ };
3039
+ function switchingHead(accept2, protocol = "") {
3040
+ const chosen = protocol ? `Sec-WebSocket-Protocol: ${protocol}\r
3041
+ ` : "";
3042
+ return latin1Bytes(
3043
+ `HTTP/1.1 101 Switching Protocols\r
3044
+ Upgrade: websocket\r
3045
+ Connection: Upgrade\r
3046
+ Sec-WebSocket-Accept: ${accept2}\r
3047
+ ${chosen}\r
3048
+ `
3049
+ );
3050
+ }
3051
+
2678
3052
  // src/kernel/net/proxy-service.ts
3053
+ var SEND_BUFFER2 = 1024 * 1024;
3054
+ var TLS_HANDSHAKE = 22;
3055
+ var TOKEN2 = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
3056
+ var NO_WEBSOCKET2 = "WebSocket not supported by this transport (fetchTransport needs { webSocket: true })";
2679
3057
  var REALM_PROXY_PORT = 3128;
2680
3058
  var DEFAULT_LIMITS = {
2681
3059
  maxConnections: 64,
2682
3060
  maxHead: 64 * 1024,
2683
3061
  idleMs: 12e4,
2684
- bodyBudget: 128 * 1024 * 1024
3062
+ bodyBudget: 128 * 1024 * 1024,
3063
+ closeWaitMs: 5e3
2685
3064
  };
2686
3065
  var HOP_BY_HOP = /* @__PURE__ */ new Set([
2687
3066
  "connection",
@@ -2872,11 +3251,27 @@ function plainResponse(status, message2) {
2872
3251
  cancel: async () => void 0
2873
3252
  };
2874
3253
  }
2875
- function timedSource(conn, idleMs, stop) {
3254
+ function timedSource(conn, idle, stop) {
2876
3255
  return {
2877
- read: (max) => conn.read(max, AbortSignal.any([stop, AbortSignal.timeout(idleMs)]))
3256
+ read: (max, signal) => {
3257
+ const own = signal ? [stop, signal] : [stop];
3258
+ if (Number.isFinite(idle.ms)) own.push(AbortSignal.timeout(idle.ms));
3259
+ return conn.read(max, AbortSignal.any(own));
3260
+ }
2878
3261
  };
2879
3262
  }
3263
+ function webSocketTarget(req) {
3264
+ return { ...req, target: req.target.replace(/^ws(s?):/i, (_, s) => `http${s.toLowerCase()}:`) };
3265
+ }
3266
+ function wireCode(code) {
3267
+ if (code === 1005) return 1e3;
3268
+ return sendableCode(code) ? code : 1011;
3269
+ }
3270
+ function quietly(ms) {
3271
+ return new Promise((resolve) => {
3272
+ AbortSignal.timeout(ms).addEventListener("abort", () => resolve(), { once: true });
3273
+ });
3274
+ }
2880
3275
  var RealmProxy = class {
2881
3276
  options;
2882
3277
  listener;
@@ -2885,6 +3280,7 @@ var RealmProxy = class {
2885
3280
  slots;
2886
3281
  bodies;
2887
3282
  connections = /* @__PURE__ */ new Set();
3283
+ warnedWebSocket = false;
2888
3284
  closed;
2889
3285
  port;
2890
3286
  constructor(options) {
@@ -2933,9 +3329,10 @@ var RealmProxy = class {
2933
3329
  }
2934
3330
  async serve(conn) {
2935
3331
  this.connections.add(conn);
2936
- const incoming = new Incoming(timedSource(conn, this.limits.idleMs, this.stop.signal));
3332
+ const idle = { ms: this.limits.idleMs };
3333
+ const incoming = new Incoming(timedSource(conn, idle, this.stop.signal));
2937
3334
  try {
2938
- await this.requests({ sink: conn, incoming, conn, socket: conn });
3335
+ await this.requests({ sink: conn, incoming, conn, socket: conn, idle });
2939
3336
  } finally {
2940
3337
  this.connections.delete(conn);
2941
3338
  conn.close();
@@ -2962,7 +3359,11 @@ var RealmProxy = class {
2962
3359
  const req = parseRequestHead(head);
2963
3360
  if (req.method === "CONNECT") {
2964
3361
  if (!ctx.conn) throw new HttpError(400, "CONNECT inside a tunnel");
2965
- await this.connect(ctx.conn, incoming, req);
3362
+ await this.connect(ctx.conn, incoming, req, ctx.idle);
3363
+ return false;
3364
+ }
3365
+ if (req.method === "GET" && isUpgrade(req.headers)) {
3366
+ await this.websocket(ctx, req);
2966
3367
  return false;
2967
3368
  }
2968
3369
  const url = ctx.origin ? tunnelRequestUrl(req, ctx.origin) : requestUrl(req);
@@ -3057,33 +3458,149 @@ var RealmProxy = class {
3057
3458
  if (chunked) await conn.write(LAST_CHUNK, signal);
3058
3459
  return keep;
3059
3460
  }
3060
- async connect(conn, incoming, req) {
3461
+ async connect(conn, incoming, req, idle) {
3061
3462
  const target = tunnelTarget(req);
3062
- const tunnel = this.options.tunnel;
3063
- if (!tunnel) {
3064
- throw new HttpError(
3065
- 501,
3066
- `CONNECT ${target.host}:${target.port}: no tunnels through this proxy`
3067
- );
3068
- }
3069
- await this.options.tunnelReady?.().catch((e) => {
3070
- throw new HttpError(
3071
- 501,
3072
- `CONNECT ${target.host}:${target.port}: ${e instanceof Error ? e.message : String(e)}`
3073
- );
3074
- });
3463
+ const refused = target.port === 443 ? await this.tunnelReady(target) : void 0;
3464
+ if (refused) throw refused;
3075
3465
  await conn.write(latin1Bytes("HTTP/1.1 200 Connection Established\r\n\r\n"), this.stop.signal);
3076
3466
  try {
3467
+ const first = await incoming.peek(this.stop.signal);
3468
+ if (first === void 0) return;
3469
+ if (first !== TLS_HANDSHAKE) {
3470
+ const origin = `http://${target.port === 80 ? target.host : `${target.host}:${target.port}`}`;
3471
+ await this.requests({ sink: conn, incoming, origin, socket: conn, idle });
3472
+ return;
3473
+ }
3474
+ const tunnel = this.options.tunnel;
3475
+ if (!tunnel || target.port !== 443 && await this.tunnelReady(target)) return;
3077
3476
  await tunnel(
3078
3477
  conn,
3079
3478
  incoming,
3080
3479
  target,
3081
3480
  this.stop.signal,
3082
- (source, sink, origin) => this.requests({ sink, incoming: new Incoming(source), origin, socket: conn })
3481
+ (source, sink, origin) => this.requests({ sink, incoming: new Incoming(source), origin, socket: conn, idle })
3083
3482
  );
3084
3483
  } catch {
3085
3484
  }
3086
3485
  }
3486
+ async tunnelReady(target) {
3487
+ const name2 = `CONNECT ${target.host}:${target.port}`;
3488
+ if (!this.options.tunnel) return new HttpError(501, `${name2}: no tunnels through this proxy`);
3489
+ try {
3490
+ await this.options.tunnelReady?.();
3491
+ return void 0;
3492
+ } catch (e) {
3493
+ return new HttpError(501, `${name2}: ${e instanceof Error ? e.message : String(e)}`);
3494
+ }
3495
+ }
3496
+ async websocket(ctx, req) {
3497
+ const href = ctx.origin ? tunnelRequestUrl(req, ctx.origin) : requestUrl(webSocketTarget(req));
3498
+ const { transport } = this.options;
3499
+ if (!transport.traits.websocket || !transport.websocket) {
3500
+ if (!this.warnedWebSocket) {
3501
+ this.warnedWebSocket = true;
3502
+ console.warn(`slicc-kernel: a program asked for a WebSocket: ${NO_WEBSOCKET2}`);
3503
+ }
3504
+ throw new HttpError(501, NO_WEBSOCKET2);
3505
+ }
3506
+ const accept2 = await acceptKey(req.headers);
3507
+ const protocols = fieldValues(req.headers, "sec-websocket-protocol").flatMap((v) => v.split(",")).map((t) => t.trim()).filter((t) => t !== "");
3508
+ const hangup = new AbortController();
3509
+ const waiting = new AbortController();
3510
+ void watchHangup(ctx.socket, hangup, waiting.signal);
3511
+ let socket;
3512
+ try {
3513
+ socket = await transport.websocket({
3514
+ url: toHostLoopback(href).replace(/^http/, "ws"),
3515
+ protocols,
3516
+ headers: forwardRequestHeaders(req.headers),
3517
+ signal: AbortSignal.any([this.stop.signal, hangup.signal])
3518
+ });
3519
+ } catch (e) {
3520
+ waiting.abort();
3521
+ throw new HttpError(502, e instanceof Error ? e.message : String(e));
3522
+ }
3523
+ ctx.idle.ms = Number.POSITIVE_INFINITY;
3524
+ try {
3525
+ if (hangup.signal.aborted) throw hangup.signal.reason;
3526
+ if (socket.protocol && !(protocols.includes(socket.protocol) && TOKEN2.test(socket.protocol))) {
3527
+ throw new HttpError(502, "the far end chose a subprotocol the client did not offer");
3528
+ }
3529
+ await ctx.sink.write(switchingHead(accept2, socket.protocol), this.stop.signal);
3530
+ await this.bridge(ctx, socket, hangup.signal);
3531
+ } catch (e) {
3532
+ socket.close(1001, "the client went away");
3533
+ throw e;
3534
+ } finally {
3535
+ waiting.abort();
3536
+ }
3537
+ }
3538
+ async bridge(ctx, socket, hangup) {
3539
+ const stop = this.stop.signal;
3540
+ let chain = Promise.resolve();
3541
+ const toGuest = (bytes) => {
3542
+ chain = chain.then(() => ctx.sink.write(bytes, stop));
3543
+ return chain;
3544
+ };
3545
+ const done4 = new AbortController();
3546
+ const reading = AbortSignal.any([stop, done4.signal, hangup]);
3547
+ const stopping = () => socket.close(1001, "the proxy is stopping");
3548
+ stop.addEventListener("abort", stopping, { once: true });
3549
+ let guestDone = false;
3550
+ let closeSent = false;
3551
+ const outbound2 = (async () => {
3552
+ for await (const data of socket.messages) {
3553
+ if (guestDone) continue;
3554
+ const text = typeof data === "string";
3555
+ await toGuest(
3556
+ frame(text ? OP_TEXT : OP_BINARY, text ? new TextEncoder().encode(data) : data)
3557
+ );
3558
+ }
3559
+ const closed = await socket.closed;
3560
+ if (guestDone) return;
3561
+ closeSent = true;
3562
+ await toGuest(closeFrame(wireCode(closed.code), closed.reason));
3563
+ AbortSignal.timeout(this.limits.closeWaitMs).addEventListener("abort", () => done4.abort(), {
3564
+ once: true
3565
+ });
3566
+ })().catch(() => done4.abort());
3567
+ const reader = new WsReader(ctx.incoming, MESSAGE_LIMIT, true);
3568
+ try {
3569
+ for (; ; ) {
3570
+ const event = await reader.next(reading);
3571
+ if (!event) break;
3572
+ if (event.kind === "ping") {
3573
+ await toGuest(pongFrame(event.payload));
3574
+ continue;
3575
+ }
3576
+ if (event.kind === "close") {
3577
+ guestDone = true;
3578
+ if (closeSent) break;
3579
+ const code = wireCode(event.code);
3580
+ socket.close(code, event.reason);
3581
+ const reply = await Promise.race([
3582
+ socket.closed,
3583
+ quietly(this.limits.closeWaitMs).then(() => ({ code, reason: event.reason }))
3584
+ ]);
3585
+ await toGuest(closeFrame(wireCode(reply.code), reply.reason));
3586
+ break;
3587
+ }
3588
+ while (socket.buffered > SEND_BUFFER2 && !reading.aborted) await quietly(10);
3589
+ socket.send(event.kind === "text" ? event.text : event.bytes);
3590
+ }
3591
+ } catch (e) {
3592
+ if (e instanceof WsError) {
3593
+ guestDone = true;
3594
+ socket.close(e.code, e.message);
3595
+ await toGuest(closeFrame(e.code, e.message)).catch(() => void 0);
3596
+ }
3597
+ } finally {
3598
+ guestDone = true;
3599
+ socket.close(1e3, "the client went away");
3600
+ stop.removeEventListener("abort", stopping);
3601
+ await Promise.race([outbound2, quietly(this.limits.closeWaitMs)]);
3602
+ }
3603
+ }
3087
3604
  };
3088
3605
 
3089
3606
  // src/kernel/net/x509.ts
@@ -3813,12 +4330,15 @@ var RemoteTransport = class {
3813
4330
  port;
3814
4331
  waiting = /* @__PURE__ */ new Map();
3815
4332
  nextId = 0;
4333
+ sockets = /* @__PURE__ */ new Map();
3816
4334
  failure;
3817
4335
  constructor(port, traits) {
3818
4336
  this.port = port;
3819
4337
  this.traits = traits;
3820
4338
  }
3821
4339
  receive(reply) {
4340
+ const socket = this.sockets.get(reply.nid);
4341
+ if (socket && this.toSocket(reply, socket)) return;
3822
4342
  const waiter = this.waiting.get(reply.nid);
3823
4343
  if (!waiter) return;
3824
4344
  this.waiting.delete(reply.nid);
@@ -3826,10 +4346,22 @@ var RemoteTransport = class {
3826
4346
  waiter.reject(new TransportError(reply.message, reply.status, reply.code));
3827
4347
  } else waiter.resolve(reply);
3828
4348
  }
4349
+ toSocket(reply, socket) {
4350
+ if (reply.net === "ws-message") socket.queue.push(reply.data);
4351
+ else if (reply.net === "ws-acked") socket.acked(reply.n);
4352
+ else if (reply.net === "ws-closed") {
4353
+ this.sockets.delete(reply.nid);
4354
+ socket.queue.end({ code: reply.code, reason: reply.reason });
4355
+ } else return false;
4356
+ return true;
4357
+ }
3829
4358
  fail(error) {
3830
4359
  this.failure = error;
3831
4360
  for (const waiter of this.waiting.values()) waiter.reject(error);
3832
4361
  this.waiting.clear();
4362
+ for (const socket of this.sockets.values())
4363
+ socket.queue.end({ code: 1011, reason: error.message });
4364
+ this.sockets.clear();
3833
4365
  }
3834
4366
  ask(call) {
3835
4367
  if (this.failure) return Promise.reject(this.failure);
@@ -3908,6 +4440,46 @@ var RemoteTransport = class {
3908
4440
  }
3909
4441
  };
3910
4442
  }
4443
+ async websocket(request2) {
4444
+ const nid = ++this.nextId;
4445
+ const { signal } = request2;
4446
+ signal.throwIfAborted();
4447
+ const abort = () => this.cancel(nid);
4448
+ signal.addEventListener("abort", abort, { once: true });
4449
+ let opened2;
4450
+ try {
4451
+ opened2 = await this.ask({
4452
+ net: "ws-open",
4453
+ nid,
4454
+ url: request2.url,
4455
+ protocols: request2.protocols,
4456
+ headers: request2.headers
4457
+ });
4458
+ } finally {
4459
+ signal.removeEventListener("abort", abort);
4460
+ }
4461
+ const close = (code, reason) => this.port.postMessage({ net: "ws-close", nid, code, reason });
4462
+ const queue = new MessageQueue(
4463
+ () => close(1e3, "a message is too big"),
4464
+ Number.POSITIVE_INFINITY,
4465
+ (n) => this.port.postMessage({ net: "ws-consumed", nid, n })
4466
+ );
4467
+ let unacked = 0;
4468
+ this.sockets.set(nid, { queue, acked: (n) => void (unacked -= n) });
4469
+ return {
4470
+ protocol: opened2.protocol,
4471
+ get buffered() {
4472
+ return unacked;
4473
+ },
4474
+ send: (data) => {
4475
+ unacked += payloadSize(data);
4476
+ this.port.postMessage({ net: "ws-send", nid, data });
4477
+ },
4478
+ messages: queue,
4479
+ close,
4480
+ closed: queue.closed
4481
+ };
4482
+ }
3911
4483
  };
3912
4484
 
3913
4485
  // src/kernel/net/routes.ts
@@ -3992,277 +4564,104 @@ var Routes = class {
3992
4564
  if (!addr) return "unreachable";
3993
4565
  if (addr.v6 && addr.bits === 1n) return "loopback";
3994
4566
  if (!addr.v6 && addr.bits >> 24n === 127n) return "loopback";
3995
- if (RESERVED.some((p) => within2(p, addr))) return "reserved";
3996
- if (addr.v6 && !this.ipv6) return "unreachable";
3997
- if (this.exit || this.table.some((p) => within2(p, addr))) return "uplink";
3998
- return "unreachable";
3999
- }
4000
- };
4001
-
4002
- // src/kernel/net/uplink.ts
4003
- function answerOf(result) {
4004
- const answer = Array.isArray(result) ? { addresses: result } : result;
4005
- const addresses = Array.isArray(answer?.addresses) ? answer.addresses.filter((a) => typeof a === "string") : [];
4006
- return {
4007
- addresses,
4008
- ...typeof answer?.ttl === "number" && answer.ttl >= 0 ? { ttl: answer.ttl } : {}
4009
- };
4010
- }
4011
- var NEVER = new Routes(true);
4012
- var RemoteUplink = class {
4013
- traits;
4014
- routes;
4015
- port;
4016
- waiting = /* @__PURE__ */ new Map();
4017
- nextId = 0;
4018
- constructor(port, traits, routes) {
4019
- this.port = port;
4020
- this.traits = traits;
4021
- this.routes = routes;
4022
- }
4023
- receive(reply) {
4024
- const waiter = this.waiting.get(reply.uid);
4025
- if (!waiter) return;
4026
- this.waiting.delete(reply.uid);
4027
- if (reply.uplink === "error") {
4028
- waiter.reject(Object.assign(new Error(reply.message), { code: reply.code }));
4029
- } else waiter.resolve(reply);
4030
- }
4031
- ask(call, signal, transfer) {
4032
- const waiter = Promise.withResolvers();
4033
- this.waiting.set(call.uid, waiter);
4034
- const abort = () => {
4035
- this.waiting.delete(call.uid);
4036
- this.port.postMessage({ uplink: "cancel", uid: call.uid });
4037
- waiter.reject(signal?.reason);
4038
- };
4039
- signal?.addEventListener("abort", abort, { once: true });
4040
- const settled = () => signal?.removeEventListener("abort", abort);
4041
- waiter.promise.then(settled, settled);
4042
- this.port.postMessage(call, transfer);
4043
- return waiter.promise;
4044
- }
4045
- async resolve(name2, family, signal) {
4046
- const reply = await this.ask(
4047
- { uplink: "resolve", uid: ++this.nextId, name: name2, family },
4048
- signal
4049
- );
4050
- return answerOf(reply);
4051
- }
4052
- async dial(req) {
4053
- req.signal.throwIfAborted();
4054
- const { host, port } = req;
4055
- const opened2 = await this.ask(
4056
- { uplink: "dial", uid: ++this.nextId, host, port },
4057
- req.signal
4058
- );
4059
- const { cid, localAddr, remoteAddr } = opened2;
4060
- return {
4061
- localAddr,
4062
- remoteAddr,
4063
- read: async () => {
4064
- const got = await this.ask({
4065
- uplink: "read",
4066
- uid: ++this.nextId,
4067
- cid
4068
- });
4069
- return got.bytes;
4070
- },
4071
- write: async (bytes) => {
4072
- const copy = bytes.slice();
4073
- const call = { uplink: "write", uid: ++this.nextId, cid, bytes: copy };
4074
- const done4 = await this.ask(call, void 0, [
4075
- copy.buffer
4076
- ]);
4077
- return done4.n;
4078
- },
4079
- closeWrite: () => this.port.postMessage({ uplink: "close-write", cid }),
4080
- close: () => this.port.postMessage({ uplink: "close", cid })
4081
- };
4082
- }
4083
- };
4084
-
4085
- // src/cdp/types.ts
4086
- var CDP_PORT = 9222;
4087
- var NO_CDP_HOST = "slicc-kernel: no CDP host is attached (createKernel({ cdp }) or client.serveCdp)";
4088
-
4089
- // src/cdp/websocket.ts
4090
- var GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
4091
- var MAX_MESSAGE = 256 * 1024 * 1024;
4092
- var CLOSE = {
4093
- normal: 1e3,
4094
- protocol: 1002,
4095
- unsupported: 1003,
4096
- invalid: 1007,
4097
- tooBig: 1009,
4098
- error: 1011
4099
- };
4100
- var OP_TEXT = 1;
4101
- var OP = { continuation: 0, text: OP_TEXT, binary: 2, close: 8, ping: 9, pong: 10 };
4102
- var WsError = class extends Error {
4103
- code;
4104
- constructor(code, message2) {
4105
- super(message2);
4106
- this.code = code;
4107
- }
4108
- };
4109
- function field(headers, name2) {
4110
- return headers.find(([n]) => n.toLowerCase() === name2)?.[1];
4111
- }
4112
- async function acceptKey(headers) {
4113
- const tokens = (name2) => (field(headers, name2) ?? "").split(",").map((t) => t.trim().toLowerCase());
4114
- if (!tokens("upgrade").includes("websocket") || !tokens("connection").includes("upgrade")) {
4115
- throw new HttpError(400, "expected a WebSocket upgrade");
4116
- }
4117
- if (field(headers, "sec-websocket-version") !== "13") {
4118
- throw new HttpError(400, "Sec-WebSocket-Version must be 13");
4119
- }
4120
- const key = field(headers, "sec-websocket-key") ?? "";
4121
- if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) throw new HttpError(400, "bad Sec-WebSocket-Key");
4122
- const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(key + GUID));
4123
- return btoa(String.fromCharCode(...new Uint8Array(digest)));
4124
- }
4125
- function isUpgrade(headers) {
4126
- return (field(headers, "upgrade") ?? "").toLowerCase().includes("websocket");
4127
- }
4128
- function frame(opcode, payload) {
4129
- const n = payload.length;
4130
- const size = n < 126 ? 2 : n < 65536 ? 4 : 10;
4131
- const out = new Uint8Array(size + n);
4132
- const view = new DataView(out.buffer);
4133
- out[0] = 128 | opcode;
4134
- if (n < 126) out[1] = n;
4135
- else if (n < 65536) {
4136
- out[1] = 126;
4137
- view.setUint16(2, n);
4138
- } else {
4139
- out[1] = 127;
4140
- view.setBigUint64(2, BigInt(n));
4141
- }
4142
- out.set(payload, size);
4143
- return out;
4144
- }
4145
- function closeFrame(code, reason = "") {
4146
- let bytes = new TextEncoder().encode(reason);
4147
- if (bytes.length > 123) bytes = bytes.subarray(0, 123);
4148
- const payload = new Uint8Array(2 + bytes.length);
4149
- new DataView(payload.buffer).setUint16(0, code);
4150
- payload.set(bytes, 2);
4151
- return frame(OP.close, payload);
4152
- }
4153
- function pongFrame(payload) {
4154
- return frame(OP.pong, payload);
4155
- }
4156
- async function fill(incoming, out, signal) {
4157
- for (let at = 0; at < out.length; ) {
4158
- const piece = await incoming.some(out.length - at, signal);
4159
- if (piece.length === 0) throw new WsError(CLOSE.protocol, "the connection closed mid-frame");
4160
- out.set(piece, at);
4161
- at += piece.length;
4162
- }
4163
- }
4164
- var fatal = new TextDecoder("utf-8", { fatal: true });
4165
- function decode(bytes) {
4166
- try {
4167
- return fatal.decode(bytes);
4168
- } catch {
4169
- throw new WsError(CLOSE.invalid, "a text message is not UTF-8");
4567
+ if (RESERVED.some((p) => within2(p, addr))) return "reserved";
4568
+ if (addr.v6 && !this.ipv6) return "unreachable";
4569
+ if (this.exit || this.table.some((p) => within2(p, addr))) return "uplink";
4570
+ return "unreachable";
4170
4571
  }
4572
+ };
4573
+
4574
+ // src/kernel/net/uplink.ts
4575
+ function answerOf(result) {
4576
+ const answer = Array.isArray(result) ? { addresses: result } : result;
4577
+ const addresses = Array.isArray(answer?.addresses) ? answer.addresses.filter((a) => typeof a === "string") : [];
4578
+ return {
4579
+ addresses,
4580
+ ...typeof answer?.ttl === "number" && answer.ttl >= 0 ? { ttl: answer.ttl } : {}
4581
+ };
4171
4582
  }
4172
- var WsReader = class {
4173
- incoming;
4174
- max;
4175
- parts = [];
4176
- size = 0;
4177
- continuing = false;
4178
- constructor(incoming, max = MAX_MESSAGE) {
4179
- this.incoming = incoming;
4180
- this.max = max;
4583
+ var NEVER = new Routes(true);
4584
+ var RemoteUplink = class {
4585
+ traits;
4586
+ routes;
4587
+ port;
4588
+ waiting = /* @__PURE__ */ new Map();
4589
+ nextId = 0;
4590
+ constructor(port, traits, routes) {
4591
+ this.port = port;
4592
+ this.traits = traits;
4593
+ this.routes = routes;
4181
4594
  }
4182
- async next(signal) {
4183
- for (; ; ) {
4184
- const first = await this.incoming.some(1, signal);
4185
- if (first.length === 0) return null;
4186
- const head = new Uint8Array(2);
4187
- head[0] = first[0];
4188
- await fill(this.incoming, head.subarray(1), signal);
4189
- const fin = (head[0] & 128) !== 0;
4190
- const opcode = head[0] & 15;
4191
- if ((head[0] & 112) !== 0) throw new WsError(CLOSE.protocol, "reserved bits are set");
4192
- if ((head[1] & 128) === 0) throw new WsError(CLOSE.protocol, "client frames must be masked");
4193
- const length2 = await this.length(head[1] & 127, signal);
4194
- const control = opcode >= 8;
4195
- if (control && (length2 > 125 || !fin)) {
4196
- throw new WsError(CLOSE.protocol, "a control frame is too long or fragmented");
4197
- }
4198
- if (!control && this.size + length2 > this.max) {
4199
- throw new WsError(CLOSE.tooBig, `a message is over ${this.max} bytes`);
4200
- }
4201
- const mask = new Uint8Array(4);
4202
- await fill(this.incoming, mask, signal);
4203
- const payload = new Uint8Array(length2);
4204
- await fill(this.incoming, payload, signal);
4205
- for (let i = 0; i < length2; i++) payload[i] ^= mask[i & 3];
4206
- const event = this.take(opcode, fin, payload);
4207
- if (event) return event;
4208
- }
4595
+ receive(reply) {
4596
+ const waiter = this.waiting.get(reply.uid);
4597
+ if (!waiter) return;
4598
+ this.waiting.delete(reply.uid);
4599
+ if (reply.uplink === "error") {
4600
+ waiter.reject(Object.assign(new Error(reply.message), { code: reply.code }));
4601
+ } else waiter.resolve(reply);
4209
4602
  }
4210
- async length(short, signal) {
4211
- if (short < 126) return short;
4212
- const ext = new Uint8Array(short === 126 ? 2 : 8);
4213
- await fill(this.incoming, ext, signal);
4214
- const view = new DataView(ext.buffer);
4215
- if (short === 126) return view.getUint16(0);
4216
- const long = view.getBigUint64(0);
4217
- return long > BigInt(this.max) ? this.max + 1 : Number(long);
4603
+ ask(call, signal, transfer) {
4604
+ const waiter = Promise.withResolvers();
4605
+ this.waiting.set(call.uid, waiter);
4606
+ const abort = () => {
4607
+ this.waiting.delete(call.uid);
4608
+ this.port.postMessage({ uplink: "cancel", uid: call.uid });
4609
+ waiter.reject(signal?.reason);
4610
+ };
4611
+ signal?.addEventListener("abort", abort, { once: true });
4612
+ const settled = () => signal?.removeEventListener("abort", abort);
4613
+ waiter.promise.then(settled, settled);
4614
+ this.port.postMessage(call, transfer);
4615
+ return waiter.promise;
4218
4616
  }
4219
- take(opcode, fin, payload) {
4220
- switch (opcode) {
4221
- case OP.ping:
4222
- return { kind: "ping", payload };
4223
- case OP.pong:
4224
- return void 0;
4225
- case OP.close: {
4226
- if (payload.length === 1) throw new WsError(CLOSE.protocol, "a close frame is malformed");
4227
- const code = payload.length >= 2 ? new DataView(payload.buffer).getUint16(0) : 1005;
4228
- return { kind: "close", code, reason: decode(payload.subarray(2)) };
4229
- }
4230
- case OP.text:
4231
- case OP.continuation: {
4232
- if (opcode === OP.continuation !== this.continuing) {
4233
- throw new WsError(CLOSE.protocol, "unexpected continuation frame");
4234
- }
4235
- this.parts.push(payload);
4236
- this.size += payload.length;
4237
- this.continuing = !fin;
4238
- if (!fin) return void 0;
4239
- const whole = this.parts.length === 1 ? payload : concat(this.parts, this.size);
4240
- this.parts = [];
4241
- this.size = 0;
4242
- return { kind: "text", text: decode(whole) };
4243
- }
4244
- case OP.binary:
4245
- throw new WsError(CLOSE.unsupported, "CDP messages are text");
4246
- default:
4247
- throw new WsError(CLOSE.protocol, `unknown opcode ${opcode}`);
4248
- }
4617
+ async resolve(name2, family, signal) {
4618
+ const reply = await this.ask(
4619
+ { uplink: "resolve", uid: ++this.nextId, name: name2, family },
4620
+ signal
4621
+ );
4622
+ return answerOf(reply);
4623
+ }
4624
+ async dial(req) {
4625
+ req.signal.throwIfAborted();
4626
+ const { host, port } = req;
4627
+ const opened2 = await this.ask(
4628
+ { uplink: "dial", uid: ++this.nextId, host, port },
4629
+ req.signal
4630
+ );
4631
+ const { cid, localAddr, remoteAddr } = opened2;
4632
+ return {
4633
+ localAddr,
4634
+ remoteAddr,
4635
+ read: async () => {
4636
+ const got = await this.ask({
4637
+ uplink: "read",
4638
+ uid: ++this.nextId,
4639
+ cid
4640
+ });
4641
+ return got.bytes;
4642
+ },
4643
+ write: async (bytes) => {
4644
+ const copy = bytes.slice();
4645
+ const call = { uplink: "write", uid: ++this.nextId, cid, bytes: copy };
4646
+ const done4 = await this.ask(call, void 0, [
4647
+ copy.buffer
4648
+ ]);
4649
+ return done4.n;
4650
+ },
4651
+ closeWrite: () => this.port.postMessage({ uplink: "close-write", cid }),
4652
+ close: () => this.port.postMessage({ uplink: "close", cid })
4653
+ };
4249
4654
  }
4250
4655
  };
4251
- function switchingHead(accept2) {
4252
- return latin1Bytes(
4253
- `HTTP/1.1 101 Switching Protocols\r
4254
- Upgrade: websocket\r
4255
- Connection: Upgrade\r
4256
- Sec-WebSocket-Accept: ${accept2}\r
4257
- \r
4258
- `
4259
- );
4260
- }
4656
+
4657
+ // src/cdp/types.ts
4658
+ var CDP_PORT = 9222;
4659
+ var NO_CDP_HOST = "slicc-kernel: no CDP host is attached (createKernel({ cdp }) or client.serveCdp)";
4261
4660
 
4262
4661
  // src/cdp/facade.ts
4263
4662
  var MAX_HEAD = 64 * 1024;
4264
4663
  var LIST_TIMEOUT = 1e4;
4265
- var CLOSE_WAIT_MS = 1e3;
4664
+ var CLOSE_WAIT_MS2 = 1e3;
4266
4665
  function respond(status, body, type = "text/plain; charset=utf-8") {
4267
4666
  const bytes = new TextEncoder().encode(body);
4268
4667
  const head = latin1Bytes(
@@ -4410,7 +4809,7 @@ var CdpFacade = class {
4410
4809
  const host = await this.host(runtime);
4411
4810
  const limits = {
4412
4811
  max: this.options.maxMessage ?? MAX_MESSAGE,
4413
- closeWaitMs: this.options.closeWaitMs ?? CLOSE_WAIT_MS
4812
+ closeWaitMs: this.options.closeWaitMs ?? CLOSE_WAIT_MS2
4414
4813
  };
4415
4814
  await new Relay(conn, incoming, host, limits, this.stop.signal).start(switchingHead(accept2)).finally(() => host.close());
4416
4815
  }
@@ -4878,13 +5277,193 @@ function withCommandDirs(fs, names) {
4878
5277
  };
4879
5278
  }
4880
5279
 
5280
+ // src/fs/unlinked.ts
5281
+ var UNLINKED_DIR = ".slicc-unlinked";
5282
+ function webLocks() {
5283
+ return globalThis.navigator?.locks;
5284
+ }
5285
+ var within3 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
5286
+ var parentOf3 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5287
+ var lockName = (instance) => `slicc-unlinked:${instance}`;
5288
+ var dirOf = (root) => root === "/" ? `/${UNLINKED_DIR}` : `${root}/${UNLINKED_DIR}`;
5289
+ var PathGate = class {
5290
+ tails = /* @__PURE__ */ new Map();
5291
+ async acquire(path) {
5292
+ const prev = this.tails.get(path) ?? Promise.resolve();
5293
+ const { promise, resolve } = Promise.withResolvers();
5294
+ const tail = prev.then(() => promise);
5295
+ this.tails.set(path, tail);
5296
+ await prev;
5297
+ return () => {
5298
+ resolve();
5299
+ if (this.tails.get(path) === tail) this.tails.delete(path);
5300
+ };
5301
+ }
5302
+ async run(path, op) {
5303
+ const release = await this.acquire(path);
5304
+ try {
5305
+ return await op();
5306
+ } finally {
5307
+ release();
5308
+ }
5309
+ }
5310
+ };
5311
+ var UnlinkedKeeper = class {
5312
+ fs;
5313
+ holders;
5314
+ rootOf;
5315
+ locks;
5316
+ busy;
5317
+ waiting = /* @__PURE__ */ new Set();
5318
+ instance = crypto.randomUUID();
5319
+ gate = new PathGate();
5320
+ queue = Promise.resolve();
5321
+ count = 0;
5322
+ constructor(fs, holders, rootOf, locks, busy = () => false) {
5323
+ this.fs = fs;
5324
+ this.holders = holders;
5325
+ this.rootOf = rootOf;
5326
+ this.locks = locks;
5327
+ this.busy = busy;
5328
+ void locks?.request(lockName(this.instance), () => new Promise(() => void 0));
5329
+ }
5330
+ serial(op) {
5331
+ const next = this.queue.then(op);
5332
+ this.queue = next.catch(() => void 0);
5333
+ return next;
5334
+ }
5335
+ wrap() {
5336
+ const fs = this.fs;
5337
+ const keeper = this;
5338
+ const visible = (names) => names.filter((name2) => name2 !== UNLINKED_DIR);
5339
+ return {
5340
+ ...fs,
5341
+ async rm(path, options) {
5342
+ const target = fs.resolvePath("/", path);
5343
+ const pinned = await keeper.pin(target);
5344
+ if (pinned.moved.has(target)) pinned.commit();
5345
+ else await keeper.settle(pinned, () => fs.rm(path, options));
5346
+ },
5347
+ async rename(from, to) {
5348
+ const pinned = await keeper.pin(fs.resolvePath("/", to));
5349
+ await keeper.settle(pinned, () => fs.rename(from, to));
5350
+ },
5351
+ readdir: async (path) => visible(await fs.readdir(path)),
5352
+ readdirStat: async (path) => (await fs.readdirStat(path)).filter(([name2]) => name2 !== UNLINKED_DIR)
5353
+ };
5354
+ }
5355
+ async settle(pinned, op) {
5356
+ try {
5357
+ await op();
5358
+ } catch (err) {
5359
+ await pinned.rollback();
5360
+ throw err;
5361
+ }
5362
+ pinned.commit();
5363
+ }
5364
+ async pin(target) {
5365
+ const held = /* @__PURE__ */ new Map();
5366
+ for (const holder of this.holders) {
5367
+ if (holder.owns(target)) continue;
5368
+ for (const path of holder.opens.keys()) {
5369
+ if (!within3(path, target) || holder.unlinked.has(path)) continue;
5370
+ held.set(path, [...held.get(path) ?? [], holder]);
5371
+ }
5372
+ }
5373
+ const paths = [...held.keys()].sort();
5374
+ const releases = [];
5375
+ for (const path of paths) releases.push(await this.gate.acquire(path));
5376
+ const hidden = /* @__PURE__ */ new Map();
5377
+ for (const path of paths) {
5378
+ const at = await this.hide(path);
5379
+ if (at !== void 0) hidden.set(path, at);
5380
+ }
5381
+ const done4 = () => {
5382
+ for (const release of releases) release();
5383
+ };
5384
+ return {
5385
+ moved: new Set(hidden.keys()),
5386
+ commit: () => {
5387
+ for (const [path, at] of hidden) {
5388
+ const holders = held.get(path);
5389
+ const file = this.keptFile(at, { refs: holders.length });
5390
+ for (const holder of holders) holder.unlinked.set(path, file);
5391
+ }
5392
+ done4();
5393
+ },
5394
+ rollback: async () => {
5395
+ for (const [path, at] of hidden) {
5396
+ await this.serial(async () => {
5397
+ await Promise.allSettled([this.fs.rename(at, path)]);
5398
+ await this.tidy(parentOf3(at));
5399
+ });
5400
+ }
5401
+ done4();
5402
+ }
5403
+ };
5404
+ }
5405
+ keptFile(hidden, entry) {
5406
+ return {
5407
+ hidden,
5408
+ release: () => {
5409
+ if (--entry.refs === 0) void this.serial(() => this.drop(hidden));
5410
+ }
5411
+ };
5412
+ }
5413
+ hide(path) {
5414
+ return this.serial(async () => {
5415
+ const dir = dirOf(this.rootOf(path));
5416
+ const hidden = `${dir}/${this.instance}.${++this.count}`;
5417
+ try {
5418
+ await this.fs.mkdir(dir, { recursive: true });
5419
+ await this.fs.rename(path, hidden);
5420
+ return hidden;
5421
+ } catch {
5422
+ await this.tidy(dir);
5423
+ return void 0;
5424
+ }
5425
+ });
5426
+ }
5427
+ closed(path) {
5428
+ if (this.waiting.delete(path)) void this.serial(() => this.drop(path));
5429
+ }
5430
+ async drop(hidden) {
5431
+ if (this.busy(hidden)) {
5432
+ this.waiting.add(hidden);
5433
+ return;
5434
+ }
5435
+ await this.fs.rm(hidden).catch(() => void 0);
5436
+ await this.tidy(parentOf3(hidden));
5437
+ }
5438
+ async tidy(dir) {
5439
+ const left = await this.fs.readdir(dir).catch(() => [UNLINKED_DIR]);
5440
+ if (left.length === 0) await this.fs.rm(dir, { recursive: true }).catch(() => void 0);
5441
+ }
5442
+ sweep(root) {
5443
+ const query = this.locks?.query?.bind(this.locks);
5444
+ if (!query) return Promise.resolve();
5445
+ return this.serial(async () => {
5446
+ const dir = dirOf(root);
5447
+ const names = await this.fs.readdir(dir).catch(() => void 0);
5448
+ if (!names) return;
5449
+ const held = new Set(((await query()).held ?? []).map((lock) => lock.name));
5450
+ for (const name2 of names) {
5451
+ const owner = name2.slice(0, name2.lastIndexOf("."));
5452
+ if (owner === this.instance || held.has(lockName(owner))) continue;
5453
+ await this.fs.rm(`${dir}/${name2}`, { recursive: true }).catch(() => void 0);
5454
+ }
5455
+ await this.tidy(dir);
5456
+ });
5457
+ }
5458
+ };
5459
+
4881
5460
  // src/fs/watch.ts
4882
5461
  var WATCH_BATCH_LIMIT = 1e3;
4883
5462
  var under2 = (path, dir) => dir === "/" || path.startsWith(`${dir}/`);
4884
- var parentOf3 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5463
+ var parentOf4 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
4885
5464
  function touches(watcher, changed) {
4886
5465
  return watcher.paths.some(
4887
- (watched) => changed === watched || under2(watched, changed) || (watcher.recursive ? under2(changed, watched) : parentOf3(changed) === watched)
5466
+ (watched) => changed === watched || under2(watched, changed) || (watcher.recursive ? under2(changed, watched) : parentOf4(changed) === watched)
4888
5467
  );
4889
5468
  }
4890
5469
  var FsWatchers = class {
@@ -5266,7 +5845,7 @@ function parseSize(text) {
5266
5845
  if (!match) throw errnoError("EINVAL", `not a size: ${text}`);
5267
5846
  return Math.floor(Number(match[1]) * UNITS[match[2].toLowerCase()]);
5268
5847
  }
5269
- var within3 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
5848
+ var within4 = (path, target) => path === target || target === "/" || path.startsWith(`${target}/`);
5270
5849
  var HeldSet = class extends Set {
5271
5850
  idle;
5272
5851
  constructor(idle) {
@@ -5279,18 +5858,47 @@ var HeldSet = class extends Set {
5279
5858
  }
5280
5859
  delete(holds) {
5281
5860
  const removed = super.delete(holds);
5282
- for (const path of holds.keys()) this.idle(path);
5861
+ for (const path of /* @__PURE__ */ new Set([...holds.keys(), ...holds.opens.keys()])) this.idle(path);
5862
+ for (const kept of holds.unlinked.values()) kept.release();
5863
+ holds.unlinked.clear();
5283
5864
  return removed;
5284
5865
  }
5285
5866
  };
5286
5867
  function heldUnder(held, target) {
5287
- for (const paths of held) for (const path of paths.keys()) if (within3(path, target)) return true;
5868
+ for (const paths of held) for (const path of paths.keys()) if (within4(path, target)) return true;
5288
5869
  return false;
5289
5870
  }
5290
5871
  var HeldPaths = class extends Map {
5291
5872
  revoked = /* @__PURE__ */ new Set();
5873
+ opens = /* @__PURE__ */ new Map();
5874
+ unlinked = /* @__PURE__ */ new Map();
5875
+ aliases = /* @__PURE__ */ new Map();
5876
+ owning;
5292
5877
  onIdle;
5293
- hold(path, on) {
5878
+ keyOf(path) {
5879
+ return this.aliases.get(path) ?? path;
5880
+ }
5881
+ keptAt(path) {
5882
+ return this.unlinked.get(this.keyOf(path));
5883
+ }
5884
+ holding(path) {
5885
+ return this.opens.has(this.keyOf(path));
5886
+ }
5887
+ hold(path, on, open = false, real) {
5888
+ if (open) {
5889
+ const key = real ?? this.keyOf(path);
5890
+ if (key !== path) this.aliases.set(path, key);
5891
+ const count2 = (this.opens.get(key) ?? 0) + (on ? 1 : -1);
5892
+ if (count2 > 0) this.opens.set(key, count2);
5893
+ else {
5894
+ this.opens.delete(key);
5895
+ this.unlinked.get(key)?.release();
5896
+ this.unlinked.delete(key);
5897
+ for (const [alias, target] of this.aliases) if (target === key) this.aliases.delete(alias);
5898
+ this.onIdle?.(key);
5899
+ }
5900
+ return;
5901
+ }
5294
5902
  const count = (this.get(path) ?? 0) + (on ? 1 : -1);
5295
5903
  if (count > 0) this.set(path, count);
5296
5904
  else {
@@ -5299,23 +5907,40 @@ var HeldPaths = class extends Map {
5299
5907
  this.onIdle?.(path);
5300
5908
  }
5301
5909
  }
5910
+ async own(path, op) {
5911
+ this.owning = path;
5912
+ try {
5913
+ return await op();
5914
+ } finally {
5915
+ this.owning = void 0;
5916
+ }
5917
+ }
5918
+ owns(path) {
5919
+ return this.owning === path;
5920
+ }
5302
5921
  revoke(prefix) {
5303
- for (const path of this.keys()) if (within3(path, prefix)) this.revoked.add(path);
5922
+ for (const path of this.keys()) if (within4(path, prefix)) this.revoked.add(path);
5304
5923
  }
5305
5924
  renamed(from, to) {
5306
5925
  for (const [path, count] of [...this]) {
5307
- if (!within3(path, from)) continue;
5926
+ if (!within4(path, from)) continue;
5308
5927
  const moved2 = to + path.slice(from.length);
5309
5928
  this.delete(path);
5310
5929
  this.set(moved2, (this.get(moved2) ?? 0) + count);
5311
5930
  if (this.revoked.delete(path)) this.revoked.add(moved2);
5312
5931
  }
5932
+ for (const [path, count] of [...this.opens]) {
5933
+ if (!within4(path, from)) continue;
5934
+ const moved2 = to + path.slice(from.length);
5935
+ this.opens.delete(path);
5936
+ this.opens.set(moved2, (this.opens.get(moved2) ?? 0) + count);
5937
+ }
5313
5938
  }
5314
5939
  isRevoked(path) {
5315
5940
  return this.revoked.has(path);
5316
5941
  }
5317
5942
  };
5318
- var parentOf4 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5943
+ var parentOf5 = (path) => path.slice(0, path.lastIndexOf("/")) || "/";
5319
5944
  var readonly = (mount) => mount.caps.readonly === true || mount.options.ro !== void 0;
5320
5945
  function statOf(attr, path, mount) {
5321
5946
  const mtime = new Date(attr.mtime);
@@ -5404,7 +6029,7 @@ var MountTable = class {
5404
6029
  locate(path) {
5405
6030
  let best;
5406
6031
  for (const mount of this.mounts.values()) {
5407
- if (within3(path, mount.target) && (!best || mount.target.length > best.target.length))
6032
+ if (within4(path, mount.target) && (!best || mount.target.length > best.target.length))
5408
6033
  best = mount;
5409
6034
  }
5410
6035
  if (!best) return void 0;
@@ -5543,7 +6168,7 @@ var MountTable = class {
5543
6168
  async commitUnder(prefix) {
5544
6169
  const at = normalizePath(prefix);
5545
6170
  await Promise.all(
5546
- [...this.sessions.keys()].filter((p) => within3(p, at)).map((p) => this.commit(p))
6171
+ [...this.sessions.keys()].filter((p) => within4(p, at)).map((p) => this.commit(p))
5547
6172
  );
5548
6173
  }
5549
6174
  async commitOverlapping(real, offset, length2) {
@@ -5577,10 +6202,10 @@ var MountTable = class {
5577
6202
  this.deps.changed?.(...full);
5578
6203
  }
5579
6204
  forget(mount, path) {
5580
- for (const key of [...mount.attrs.keys()]) if (within3(key, path)) mount.attrs.delete(key);
6205
+ for (const key of [...mount.attrs.keys()]) if (within4(key, path)) mount.attrs.delete(key);
5581
6206
  mount.lists.delete(path);
5582
- mount.lists.delete(parentOf4(path));
5583
- for (const key of [...mount.lists.keys()]) if (within3(key, path)) mount.lists.delete(key);
6207
+ mount.lists.delete(parentOf5(path));
6208
+ for (const key of [...mount.lists.keys()]) if (within4(key, path)) mount.lists.delete(key);
5584
6209
  }
5585
6210
  async getattr(mount, path, rel2) {
5586
6211
  if (mount.conn.failure) throw mount.conn.failure;
@@ -5645,7 +6270,7 @@ var MountFs = class {
5645
6270
  if (attr.kind !== "symlink") return real;
5646
6271
  if (hops >= MAX_LINKS4) throw errnoError("ELOOP", path);
5647
6272
  const target = await found.mount.conn.call({ op: "readlink", path: found.rel });
5648
- return this.follow(this.base.resolvePath(parentOf4(real), target), hops + 1, create);
6273
+ return this.follow(this.base.resolvePath(parentOf5(real), target), hops + 1, create);
5649
6274
  }
5650
6275
  async open(mount, rel2, write) {
5651
6276
  return await mount.conn.call({
@@ -6159,6 +6784,8 @@ async function ranged(fs, path, req) {
6159
6784
  else await ops.truncate(path, req.length ?? 0);
6160
6785
  return done;
6161
6786
  }
6787
+ var UNLINKED_OPS = /* @__PURE__ */ new Set(["read", "pread", "pwrite", "truncate"]);
6788
+ var OWN_OPS = /* @__PURE__ */ new Set(["rm", "unlink", "rmdir", "rename"]);
6162
6789
  async function dispatchSyncFs(req) {
6163
6790
  const entry = resolveSyncFsToken(req.token);
6164
6791
  if (!entry) return { ok: false, errno: "EACCES", message: "sync-fs: unknown or revoked token" };
@@ -6167,11 +6794,22 @@ async function dispatchSyncFs(req) {
6167
6794
  const path = fs.resolvePath(cwd, req.path);
6168
6795
  if (req.op === "statfs") return json2(await entry.statfs?.(path) ?? await storageUsage());
6169
6796
  if (req.op === "hold") {
6170
- entry.hold?.(path, req.mode === 1);
6797
+ const [on, open] = [((req.mode ?? 0) & 1) === 1, ((req.mode ?? 0) & 2) === 2];
6798
+ const real = on && open ? await fs.realpath?.(path, true).catch(() => path) : void 0;
6799
+ entry.hold?.(path, on, open, real);
6171
6800
  return done;
6172
6801
  }
6173
6802
  if (entry.revoked?.(path)) return { ok: false, errno: "EIO", message: `${path} was unmounted` };
6174
- return await run(entry, path, req);
6803
+ if (req.op === "write" && req.mode === 1 || UNLINKED_OPS.has(req.op)) {
6804
+ const serve = async () => {
6805
+ const kept = entry.unlinked?.(path);
6806
+ return run(entry, kept && !await fs.exists(path) ? kept.hidden : path, req);
6807
+ };
6808
+ return await (entry.gate ? entry.gate(path, serve) : serve());
6809
+ }
6810
+ if (!entry.own || !OWN_OPS.has(req.op)) return await run(entry, path, req);
6811
+ const target = req.op === "rename" ? fs.resolvePath(cwd, req.arg2 ?? "") : path;
6812
+ return await entry.own(target, () => run(entry, path, req));
6175
6813
  } catch (err) {
6176
6814
  return toErrno(err);
6177
6815
  }
@@ -7559,7 +8197,7 @@ function isMissing(err) {
7559
8197
  if (err?.code === "ENOENT") return true;
7560
8198
  return err instanceof Error && err.message.startsWith("ENOENT");
7561
8199
  }
7562
- function within4(path, root) {
8200
+ function within5(path, root) {
7563
8201
  return path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`);
7564
8202
  }
7565
8203
  var VfsNode = class {
@@ -7764,7 +8402,7 @@ var VfsNodes = class {
7764
8402
  return run2;
7765
8403
  }
7766
8404
  async entryKey(path) {
7767
- const parent = parentOf5(path);
8405
+ const parent = parentOf6(path);
7768
8406
  const dir = await this.real(parent, true, () => realDir(this.fs, parent));
7769
8407
  return joinName(dir, path.slice(path.lastIndexOf("/") + 1));
7770
8408
  }
@@ -7812,12 +8450,12 @@ var VfsNodes = class {
7812
8450
  return node;
7813
8451
  }
7814
8452
  holds(prefix) {
7815
- for (const path of this.byPath.keys()) if (within4(path, prefix)) return true;
8453
+ for (const path of this.byPath.keys()) if (within5(path, prefix)) return true;
7816
8454
  return false;
7817
8455
  }
7818
8456
  writes(prefix) {
7819
8457
  for (const [path, node] of this.byPath)
7820
- if (node.writers > 0 && within4(path, prefix)) return true;
8458
+ if (node.writers > 0 && within5(path, prefix)) return true;
7821
8459
  return false;
7822
8460
  }
7823
8461
  closed(node, writable = false) {
@@ -7831,24 +8469,24 @@ var VfsNodes = class {
7831
8469
  this.onClosed?.(node.path);
7832
8470
  }
7833
8471
  async flush(path) {
7834
- for (const [p, node] of this.byPath) if (within4(p, path)) await node.serial(() => node.flush());
8472
+ for (const [p, node] of this.byPath) if (within5(p, path)) await node.serial(() => node.flush());
7835
8473
  }
7836
8474
  async unlinking(path) {
7837
8475
  for (const [held, node] of [...this.byPath]) {
7838
- if (!within4(held, path)) continue;
8476
+ if (!within5(held, path)) continue;
7839
8477
  await node.serial(async () => void await Promise.all([node.keep(), node.limit()]));
7840
8478
  }
7841
8479
  }
7842
8480
  revoke(prefix) {
7843
8481
  for (const [path, node] of this.byPath) {
7844
- if (!within4(path, prefix)) continue;
8482
+ if (!within5(path, prefix)) continue;
7845
8483
  node.revoke();
7846
8484
  this.byPath.delete(path);
7847
8485
  }
7848
8486
  }
7849
8487
  unlinked(path) {
7850
8488
  for (const [held, node] of [...this.byPath]) {
7851
- if (!within4(held, path)) continue;
8489
+ if (!within5(held, path)) continue;
7852
8490
  node.orphaned = true;
7853
8491
  this.byPath.delete(held);
7854
8492
  }
@@ -7857,8 +8495,8 @@ var VfsNodes = class {
7857
8495
  if (from === to) return;
7858
8496
  const moved2 = [];
7859
8497
  for (const [p, node] of this.byPath) {
7860
- if (within4(p, from)) moved2.push(node);
7861
- else if (within4(p, to)) {
8498
+ if (within5(p, from)) moved2.push(node);
8499
+ else if (within5(p, to)) {
7862
8500
  node.orphaned = true;
7863
8501
  this.byPath.delete(p);
7864
8502
  }
@@ -7881,7 +8519,7 @@ async function present(fs, path, follow = false) {
7881
8519
  }
7882
8520
  }
7883
8521
  var MAX_LINKS5 = 40;
7884
- function parentOf5(path) {
8522
+ function parentOf6(path) {
7885
8523
  return path.slice(0, path.lastIndexOf("/")) || "/";
7886
8524
  }
7887
8525
  async function linkOf(fs, path) {
@@ -7894,7 +8532,7 @@ async function realDir(fs, dir, budget = { hops: 0 }) {
7894
8532
  const part = queue.shift();
7895
8533
  if (part === ".") continue;
7896
8534
  if (part === "..") {
7897
- at = parentOf5(at);
8535
+ at = parentOf6(at);
7898
8536
  continue;
7899
8537
  }
7900
8538
  const next = at === "/" ? `/${part}` : `${at}/${part}`;
@@ -7918,7 +8556,7 @@ function joinName(dir, name2) {
7918
8556
  }
7919
8557
  async function realTarget(fs, path) {
7920
8558
  const budget = { hops: 0 };
7921
- let dir = await realDir(fs, parentOf5(path), budget);
8559
+ let dir = await realDir(fs, parentOf6(path), budget);
7922
8560
  let name2 = path.slice(path.lastIndexOf("/") + 1);
7923
8561
  for (; ; ) {
7924
8562
  const link2 = await linkOf(fs, joinName(dir, name2));
@@ -7926,13 +8564,13 @@ async function realTarget(fs, path) {
7926
8564
  if (budget.hops >= MAX_LINKS5) throw fsError("ELOOP", path);
7927
8565
  budget.hops++;
7928
8566
  const target = link2.startsWith("/") ? link2 : joinName(dir, link2);
7929
- dir = await realDir(fs, parentOf5(target), budget);
8567
+ dir = await realDir(fs, parentOf6(target), budget);
7930
8568
  name2 = target.slice(target.lastIndexOf("/") + 1);
7931
8569
  }
7932
8570
  return joinName(dir, name2);
7933
8571
  }
7934
8572
  async function targetDir(fs, path) {
7935
- return parentOf5(await realTarget(fs, path).catch(() => path));
8573
+ return parentOf6(await realTarget(fs, path).catch(() => path));
7936
8574
  }
7937
8575
  async function refuseReadonly(fs, path, flags, opts = {}) {
7938
8576
  const writes = (flags & O_ACCMODE) !== 0 || opts.truncate === true;
@@ -8321,7 +8959,7 @@ var WasmProcess = class {
8321
8959
  const fd = this.fds.install(heldFile(), 3);
8322
8960
  try {
8323
8961
  const nodes = this.nodes;
8324
- const path = req.orphan ? req.path : req.exclusive ? await nodes.entryKey(req.path) : await nodes.targetKey(req.path);
8962
+ const path = req.orphan ? req.path : req.exclusive ? await nodes.entryKey(req.path) : req.create ? await nodes.targetKey(req.path) : await this.keptOr(await nodes.targetKey(req.path));
8325
8963
  const open = { ...req, path };
8326
8964
  if (req.exclusive) {
8327
8965
  await nodes.createExclusive(path);
@@ -8477,6 +9115,11 @@ var WasmProcess = class {
8477
9115
  return { ok: true, kind: "json", json: await file.stat() };
8478
9116
  }
8479
9117
  }
9118
+ keptPath;
9119
+ async keptOr(path) {
9120
+ const kept = this.keptPath?.(path);
9121
+ return kept !== void 0 && !await present(this.options.fs, path, true) ? kept : path;
9122
+ }
8480
9123
  async promote(req) {
8481
9124
  if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
8482
9125
  if (req.share !== void 0) {
@@ -8484,7 +9127,7 @@ var WasmProcess = class {
8484
9127
  return;
8485
9128
  }
8486
9129
  if (req.path === void 0) throw new KernelError("EINVAL");
8487
- const path = req.orphan ? req.path : await this.nodes.targetKey(req.path);
9130
+ const path = req.orphan ? req.path : await this.keptOr(await this.nodes.targetKey(req.path));
8488
9131
  if (!this.fds.get(req.fd).file.held) throw new KernelError("EBADF");
8489
9132
  const file = vfsFile(
8490
9133
  this.options.fs,
@@ -8843,6 +9486,19 @@ function inherited(opts) {
8843
9486
  ...opts.umask !== void 0 ? { umask: opts.umask } : {}
8844
9487
  };
8845
9488
  }
9489
+ function holdingToken(opts, holds) {
9490
+ return mintSyncFsToken({
9491
+ fs: opts.fs,
9492
+ cwd: opts.cwd,
9493
+ ...opts.statfs ? { statfs: opts.statfs } : {},
9494
+ hold: (path, on, open, real) => holds.hold(path, on, open, real),
9495
+ unlinked: (path) => holds.keptAt(path),
9496
+ gate: (path, op) => opts.unlinkGate && holds.holding(path) ? opts.unlinkGate.run(holds.keyOf(path), op) : op(),
9497
+ own: (path, op) => holds.own(path, op),
9498
+ revoked: (path) => holds.isRevoked(path),
9499
+ renamed: (from, to) => holds.renamed(from, to)
9500
+ });
9501
+ }
8846
9502
  function spawnWasmProcess(opts) {
8847
9503
  const sab = new SharedArrayBuffer(SAB_HEADER_BYTES + SAB_DEFAULT_WINDOW_BYTES);
8848
9504
  const header2 = new Int32Array(sab, 0, SAB_HEADER_I32);
@@ -8887,14 +9543,8 @@ function spawnWasmProcess(opts) {
8887
9543
  });
8888
9544
  const holds = new HeldPaths();
8889
9545
  opts.held?.add(holds);
8890
- const token = mintSyncFsToken({
8891
- fs: opts.fs,
8892
- cwd: opts.cwd,
8893
- ...opts.statfs ? { statfs: opts.statfs } : {},
8894
- hold: (path, on) => holds.hold(path, on),
8895
- revoked: (path) => holds.isRevoked(path),
8896
- renamed: (from, to) => holds.renamed(from, to)
8897
- });
9546
+ const token = holdingToken(opts, holds);
9547
+ process.keptPath = (path) => holds.keptAt(path)?.hidden;
8898
9548
  const worker = opts.createWorker();
8899
9549
  const dying = new Dying();
8900
9550
  const dispatch2 = (req) => dying.serving(() => isWasmSyscall(req) ? process.syscall(req) : dispatchSyncFs(req));
@@ -10534,6 +11184,7 @@ var Launcher = class {
10534
11184
  deciding = /* @__PURE__ */ new Map();
10535
11185
  identities = { asked: 0, timedOut: 0 };
10536
11186
  nodes;
11187
+ unlinked;
10537
11188
  held = new HeldSet((path) => this.released(path));
10538
11189
  processMounts;
10539
11190
  booting = new AbortController();
@@ -10578,7 +11229,15 @@ var Launcher = class {
10578
11229
  const fs = withCommandDirs(this.base, async () => new Set((await this.commands()).keys()));
10579
11230
  this.nodes = new VfsNodes(fs, (path) => this.released(path));
10580
11231
  this.openFiles.add(this.nodes);
10581
- this.fs = keepingOpen(fs, this.nodes);
11232
+ this.unlinked = new UnlinkedKeeper(
11233
+ fs,
11234
+ this.held,
11235
+ (path) => this.mountRoot(path),
11236
+ webLocks(),
11237
+ (path) => [...this.openFiles].some((nodes) => nodes.holds(path))
11238
+ );
11239
+ this.fs = keepingOpen(this.unlinked.wrap(), this.nodes);
11240
+ void this.unlinked.sweep("/");
10582
11241
  this.watchers.watch([this.modulesDir, this.pnpmHome], { recursive: true }, () => {
10583
11242
  this.catalog = void 0;
10584
11243
  this.binfmts = void 0;
@@ -10788,6 +11447,7 @@ var Launcher = class {
10788
11447
  openFiles: this.openFiles,
10789
11448
  nodes: this.nodes,
10790
11449
  held: this.held,
11450
+ unlinkGate: this.unlinked.gate,
10791
11451
  statfs: (path) => this.mounts.statfs(path),
10792
11452
  mounts: () => this.mounts.list(),
10793
11453
  mount: (call, signal) => this.processMount(pid, call, signal),
@@ -10945,6 +11605,7 @@ var Launcher = class {
10945
11605
  }
10946
11606
  released(path) {
10947
11607
  const at = normalizePath(path);
11608
+ this.unlinked.closed(at);
10948
11609
  const open = [...this.openFiles].some((nodes) => nodes.writes(at)) || heldUnder(this.held, at);
10949
11610
  if (!open) void this.mounts.commit(at).catch(() => void 0);
10950
11611
  }
@@ -11040,10 +11701,23 @@ var Launcher = class {
11040
11701
  }
11041
11702
  };
11042
11703
  }
11043
- mount(spec) {
11044
- if (spec.type !== "fsa" || spec.source.startsWith("fsa:"))
11045
- return this.mounts.mount(spec, this.fs);
11046
- return this.mounts.mount({ ...spec, source: `fsa:${crypto.randomUUID()}` }, this.fs);
11704
+ async mount(spec) {
11705
+ const fsa = spec.type === "fsa" && !spec.source.startsWith("fsa:");
11706
+ const entry = await this.mounts.mount(
11707
+ fsa ? { ...spec, source: `fsa:${crypto.randomUUID()}` } : spec,
11708
+ this.fs
11709
+ );
11710
+ void this.unlinked.sweep(entry.target);
11711
+ return entry;
11712
+ }
11713
+ mountRoot(path) {
11714
+ let root = "/";
11715
+ for (const { target } of this.mounts.list()) {
11716
+ if (target.length > root.length && (path === target || path.startsWith(`${target}/`))) {
11717
+ root = target;
11718
+ }
11719
+ }
11720
+ return root;
11047
11721
  }
11048
11722
  async insert(target, handle, source) {
11049
11723
  const at = normalizePath(target);
@@ -11057,6 +11731,7 @@ var Launcher = class {
11057
11731
  await this.media.put(slot.id, handle);
11058
11732
  slot.medium.insert(handle);
11059
11733
  slot.served.invalidate(true);
11734
+ void this.unlinked.sweep(at);
11060
11735
  }
11061
11736
  umount(target, detach = false) {
11062
11737
  const at = normalizePath(target);