beam.com 0.1.0-rc.3 → 0.1.0-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -219,7 +219,7 @@ jobs:
219
219
 
220
220
  Then turn on Pages one time: Settings, Pages, Source "GitHub Actions". The
221
221
  `GITHUB_TOKEN` cannot do this step. Until the release `v0.1.0`, use the
222
- release candidate `@v0.1.0-rc.3` in place of `@v0.1.0`. With `@main`, the
222
+ release candidate `@v0.1.0-rc.5` in place of `@v0.1.0`. With `@main`, the
223
223
  workflow uses the `edge` build of `beam.com`.
224
224
 
225
225
  The workflow ([`pages-app.yml`](.github/workflows/pages-app.yml)) builds
@@ -286,7 +286,7 @@ Each GitHub release from 0.1.0-rc.2 also holds the package, the same
286
286
  file as on npm. npm installs it with no registry:
287
287
 
288
288
  ```sh
289
- npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.3/beam.com-0.1.0-rc.3.tgz
289
+ npm install https://github.com/sntran/BEAM.com/releases/download/v0.1.0-rc.5/beam.com-0.1.0-rc.5.tgz
290
290
  ```
291
291
 
292
292
  - `BEAM_COM`: the path of a `beam.com` to use. Then nothing is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beam.com",
3
- "version": "0.1.0-rc.3",
3
+ "version": "0.1.0-rc.5",
4
4
  "description": "Erlang/OTP and Elixir in one executable file: npx beam.com, and the WebAssembly runtime for a native app.com",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -36,6 +36,21 @@ import { DurableObject } from 'cloudflare:workers';
36
36
  import { Vm } from './worker.js';
37
37
 
38
38
  const valid = (name) => /^[a-z0-9-]{1,32}$/.test(name ?? '');
39
+
40
+ // The request to the object stub. workerd pipes the body from the client
41
+ // to the object, and that pipe reads on after the response has gone when
42
+ // the object stops its read early. The read then fails with no handler:
43
+ // "Uncaught TypeError: Can't read from request stream after response has
44
+ // been sent". Here the pipe is one of this Worker, with a handler. A
45
+ // FixedLengthStream (workerd) keeps the length of the body.
46
+ export function forward(stub, request) {
47
+ if (!request.body) return stub.fetch(request);
48
+ const length = Number(request.headers.get('content-length') ?? NaN);
49
+ const pass = Number.isSafeInteger(length) && typeof FixedLengthStream === 'function'
50
+ ? new FixedLengthStream(length) : new TransformStream();
51
+ request.body.pipeTo(pass.writable).catch(() => {});
52
+ return stub.fetch(new Request(request, { body: pass.readable, duplex: 'half' }));
53
+ }
39
54
  const REGISTRY = '.registry';
40
55
  const EXPIRES = 'beam:expires';
41
56
 
@@ -363,7 +378,7 @@ export default {
363
378
  // The app can trust the header: a client cannot give it.
364
379
  request = new Request(inner, request);
365
380
  request.headers.set('x-beam-tenant', m[1]);
366
- const response = await env.BEAM.get(env.BEAM.idFromName(m[1])).fetch(request);
381
+ const response = await forward(env.BEAM.get(env.BEAM.idFromName(m[1])), request);
367
382
  if (!response.headers.has('service-worker-allowed')) return response;
368
383
  const out = new Response(response.body, response);
369
384
  out.headers.delete('service-worker-allowed');
@@ -386,6 +401,6 @@ export default {
386
401
  request = new Request(request);
387
402
  if (env.BEAM_TENANTS) request.headers.set('x-beam-tenant', name);
388
403
  else request.headers.delete('x-beam-tenant');
389
- return env.BEAM.get(env.BEAM.idFromName(name)).fetch(request);
404
+ return forward(env.BEAM.get(env.BEAM.idFromName(name)), request);
390
405
  },
391
406
  };
@@ -28,7 +28,7 @@ import { env as globalEnv } from 'cloudflare:workers';
28
28
  import { release, use } from './release.js';
29
29
  import { useSnapshot } from './snapshot.js';
30
30
  import plain, { parseSnapshot, staticResponse, Vm } from './worker.js';
31
- import front from './durable.js';
31
+ import front, { forward } from './durable.js';
32
32
 
33
33
  export { Beam } from './durable.js';
34
34
 
@@ -113,7 +113,7 @@ export function serve(app, { binding = 'BEAM', name, nifs = null, snapshot = nul
113
113
  const objects = env[binding];
114
114
  if (!objects && vm) return vm.fetch(request, ctx);
115
115
  if (!objects) return plain.fetch(request, env, ctx);
116
- if (typeof name === 'function') return objects.getByName(name(request)).fetch(request);
116
+ if (typeof name === 'function') return forward(objects.getByName(name(request)), request);
117
117
  return front.fetch(request, frontEnv(env), ctx);
118
118
  },
119
119
  scheduled(controller, env, ctx) {
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,20 +1986,28 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
1973
1993
  // host tells wasm_tcp what the client took (tcp_sent).
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
- this.bridgeSend(c, bytes);
1976
- if (body) this.bridgeUpload(c, body, declared === null, maxBody);
1995
+ // With a body, the head goes in the event of the first part of the
1996
+ // body (bridgeUpload): a small body is one event, as before the parts.
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1998
+ else this.bridgeSend(c, bytes);
1977
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1978
2005
  }
1979
2006
 
1980
2007
  // Bytes to the app on the connection c.
1981
2008
  bridgeSend(c, bytes) {
1982
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1983
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1984
2012
  }
1985
2013
 
@@ -1987,8 +2015,13 @@ export class Vm {
1987
2015
  // (or the rest of the body): one event for each part, not for each small
1988
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1989
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1990
- // the memory of the VM, and a slow app slows the client.
1991
- async bridgeUpload(c, body, chunked, max) {
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
2024
+ async bridgeUpload(c, body, chunked, max, head) {
1992
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1993
2026
  // not safe: when the stream closes with fewer bytes than min, the read
1994
2027
  // errors the stream (Node.js 26, Chromium). So the other hosts use the
@@ -1997,7 +2030,7 @@ export class Vm {
1997
2030
  try { byob = body.getReader({ mode: 'byob' }); } catch {}
1998
2031
  if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
1999
2032
  const reader = byob ?? body.getReader();
2000
- const read = byob ? () => byob.readAtLeast(UPLOAD_PART, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2033
+ const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2001
2034
  let total = 0;
2002
2035
  let part = [];
2003
2036
  let size = 0;
@@ -2011,13 +2044,22 @@ export class Vm {
2011
2044
  const bytes = part.length === 1 ? part[0] : join(part, size);
2012
2045
  part = [];
2013
2046
  size = 0;
2014
- this.bridgeSend(c, chunked ? chunk(bytes) : bytes);
2047
+ out(chunked ? chunk(bytes) : bytes);
2015
2048
  return true;
2016
2049
  };
2050
+ // Bytes to the app, after the head when it did not go yet.
2051
+ const out = (bytes) => {
2052
+ clearTimeout(alone);
2053
+ this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
2054
+ head = null;
2055
+ };
2056
+ // A client that sends no body yet: the head goes alone after HEAD_WAIT
2057
+ // ms, so the app sees the request (and its own time limits run).
2058
+ const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
2017
2059
  try {
2018
2060
  for (;;) {
2019
2061
  // A read can end the stream with its last bytes (done and a value).
2020
- const { value, done } = await read();
2062
+ const { value, done } = await read(head ? 1 : UPLOAD_PART);
2021
2063
  if (c.done || this.dead) break;
2022
2064
  if (value?.length) {
2023
2065
  total += value.length;
@@ -2033,21 +2075,26 @@ export class Vm {
2033
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2034
2076
  part.push(piece);
2035
2077
  size += piece.length;
2036
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2037
2079
  }
2038
- if (ok && done && size) ok = await flush();
2080
+ if (ok && (done || head) && size) ok = await flush();
2039
2081
  if (!ok) break;
2040
2082
  if (done) {
2041
- if (chunked) this.bridgeSend(c, CHUNKS_END);
2083
+ if (chunked) out(CHUNKS_END);
2084
+ else if (head) out(new Uint8Array(0));
2042
2085
  this.bridgeClock(c, c.timeout * 1000);
2043
2086
  return;
2044
2087
  }
2045
2088
  }
2046
- reader.cancel().catch(() => {});
2047
2089
  } catch {
2048
2090
  // The client stopped the upload.
2049
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2050
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2051
2098
  }
2052
2099
 
2053
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2055,9 +2102,12 @@ export class Vm {
2055
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2056
2103
  }
2057
2104
 
2058
- // The app read n bytes of the connection c (tcp_read).
2059
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2060
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2061
2111
  this.bridgeRoom(c);
2062
2112
  c.inbound?.flush();
2063
2113
  }
@@ -2667,7 +2717,7 @@ export class Vm {
2667
2717
  }
2668
2718
  // wasm_tcp: the app read n bytes of a socket.
2669
2719
  case 'tcp_read': {
2670
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2671
2721
  break;
2672
2722
  }
2673
2723
  case 'tcp_close': {
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
2759
2809
  // read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
2760
2810
  // costs a turn of the VM.
2761
2811
  const UPLOAD_WINDOW = 256 * 1024;
2762
- const UPLOAD_PART = 16 * 1024;
2812
+ const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2814
+ // The time that the head of a request waits for the first bytes of its
2815
+ // body (bridgeUpload), in ms.
2816
+ const HEAD_WAIT = 20;
2763
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2764
2856
 
2765
2857
  // The size bytes of the list parts, as one array.
2766
2858
  function join(parts, size) {
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,20 +1986,28 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
1973
1993
  // host tells wasm_tcp what the client took (tcp_sent).
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
- this.bridgeSend(c, bytes);
1976
- if (body) this.bridgeUpload(c, body, declared === null, maxBody);
1995
+ // With a body, the head goes in the event of the first part of the
1996
+ // body (bridgeUpload): a small body is one event, as before the parts.
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1998
+ else this.bridgeSend(c, bytes);
1977
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1978
2005
  }
1979
2006
 
1980
2007
  // Bytes to the app on the connection c.
1981
2008
  bridgeSend(c, bytes) {
1982
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1983
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1984
2012
  }
1985
2013
 
@@ -1987,8 +2015,13 @@ export class Vm {
1987
2015
  // (or the rest of the body): one event for each part, not for each small
1988
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1989
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1990
- // the memory of the VM, and a slow app slows the client.
1991
- async bridgeUpload(c, body, chunked, max) {
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
2024
+ async bridgeUpload(c, body, chunked, max, head) {
1992
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1993
2026
  // not safe: when the stream closes with fewer bytes than min, the read
1994
2027
  // errors the stream (Node.js 26, Chromium). So the other hosts use the
@@ -1997,7 +2030,7 @@ export class Vm {
1997
2030
  try { byob = body.getReader({ mode: 'byob' }); } catch {}
1998
2031
  if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
1999
2032
  const reader = byob ?? body.getReader();
2000
- const read = byob ? () => byob.readAtLeast(UPLOAD_PART, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2033
+ const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2001
2034
  let total = 0;
2002
2035
  let part = [];
2003
2036
  let size = 0;
@@ -2011,13 +2044,22 @@ export class Vm {
2011
2044
  const bytes = part.length === 1 ? part[0] : join(part, size);
2012
2045
  part = [];
2013
2046
  size = 0;
2014
- this.bridgeSend(c, chunked ? chunk(bytes) : bytes);
2047
+ out(chunked ? chunk(bytes) : bytes);
2015
2048
  return true;
2016
2049
  };
2050
+ // Bytes to the app, after the head when it did not go yet.
2051
+ const out = (bytes) => {
2052
+ clearTimeout(alone);
2053
+ this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
2054
+ head = null;
2055
+ };
2056
+ // A client that sends no body yet: the head goes alone after HEAD_WAIT
2057
+ // ms, so the app sees the request (and its own time limits run).
2058
+ const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
2017
2059
  try {
2018
2060
  for (;;) {
2019
2061
  // A read can end the stream with its last bytes (done and a value).
2020
- const { value, done } = await read();
2062
+ const { value, done } = await read(head ? 1 : UPLOAD_PART);
2021
2063
  if (c.done || this.dead) break;
2022
2064
  if (value?.length) {
2023
2065
  total += value.length;
@@ -2033,21 +2075,26 @@ export class Vm {
2033
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2034
2076
  part.push(piece);
2035
2077
  size += piece.length;
2036
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2037
2079
  }
2038
- if (ok && done && size) ok = await flush();
2080
+ if (ok && (done || head) && size) ok = await flush();
2039
2081
  if (!ok) break;
2040
2082
  if (done) {
2041
- if (chunked) this.bridgeSend(c, CHUNKS_END);
2083
+ if (chunked) out(CHUNKS_END);
2084
+ else if (head) out(new Uint8Array(0));
2042
2085
  this.bridgeClock(c, c.timeout * 1000);
2043
2086
  return;
2044
2087
  }
2045
2088
  }
2046
- reader.cancel().catch(() => {});
2047
2089
  } catch {
2048
2090
  // The client stopped the upload.
2049
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2050
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2051
2098
  }
2052
2099
 
2053
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2055,9 +2102,12 @@ export class Vm {
2055
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2056
2103
  }
2057
2104
 
2058
- // The app read n bytes of the connection c (tcp_read).
2059
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2060
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2061
2111
  this.bridgeRoom(c);
2062
2112
  c.inbound?.flush();
2063
2113
  }
@@ -2667,7 +2717,7 @@ export class Vm {
2667
2717
  }
2668
2718
  // wasm_tcp: the app read n bytes of a socket.
2669
2719
  case 'tcp_read': {
2670
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2671
2721
  break;
2672
2722
  }
2673
2723
  case 'tcp_close': {
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
2759
2809
  // read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
2760
2810
  // costs a turn of the VM.
2761
2811
  const UPLOAD_WINDOW = 256 * 1024;
2762
- const UPLOAD_PART = 16 * 1024;
2812
+ const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2814
+ // The time that the head of a request waits for the first bytes of its
2815
+ // body (bridgeUpload), in ms.
2816
+ const HEAD_WAIT = 20;
2763
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2764
2856
 
2765
2857
  // The size bytes of the list parts, as one array.
2766
2858
  function join(parts, size) {
@@ -36,6 +36,21 @@ import { DurableObject } from 'cloudflare:workers';
36
36
  import { Vm } from './worker.js';
37
37
 
38
38
  const valid = (name) => /^[a-z0-9-]{1,32}$/.test(name ?? '');
39
+
40
+ // The request to the object stub. workerd pipes the body from the client
41
+ // to the object, and that pipe reads on after the response has gone when
42
+ // the object stops its read early. The read then fails with no handler:
43
+ // "Uncaught TypeError: Can't read from request stream after response has
44
+ // been sent". Here the pipe is one of this Worker, with a handler. A
45
+ // FixedLengthStream (workerd) keeps the length of the body.
46
+ export function forward(stub, request) {
47
+ if (!request.body) return stub.fetch(request);
48
+ const length = Number(request.headers.get('content-length') ?? NaN);
49
+ const pass = Number.isSafeInteger(length) && typeof FixedLengthStream === 'function'
50
+ ? new FixedLengthStream(length) : new TransformStream();
51
+ request.body.pipeTo(pass.writable).catch(() => {});
52
+ return stub.fetch(new Request(request, { body: pass.readable, duplex: 'half' }));
53
+ }
39
54
  const REGISTRY = '.registry';
40
55
  const EXPIRES = 'beam:expires';
41
56
 
@@ -363,7 +378,7 @@ export default {
363
378
  // The app can trust the header: a client cannot give it.
364
379
  request = new Request(inner, request);
365
380
  request.headers.set('x-beam-tenant', m[1]);
366
- const response = await env.BEAM.get(env.BEAM.idFromName(m[1])).fetch(request);
381
+ const response = await forward(env.BEAM.get(env.BEAM.idFromName(m[1])), request);
367
382
  if (!response.headers.has('service-worker-allowed')) return response;
368
383
  const out = new Response(response.body, response);
369
384
  out.headers.delete('service-worker-allowed');
@@ -386,6 +401,6 @@ export default {
386
401
  request = new Request(request);
387
402
  if (env.BEAM_TENANTS) request.headers.set('x-beam-tenant', name);
388
403
  else request.headers.delete('x-beam-tenant');
389
- return env.BEAM.get(env.BEAM.idFromName(name)).fetch(request);
404
+ return forward(env.BEAM.get(env.BEAM.idFromName(name)), request);
390
405
  },
391
406
  };
@@ -1,3 +1,3 @@
1
1
  // The identity of this runtime (beam_com_wasm:runtime_id/1): the SHA-256 of
2
2
  // the output of "sha256sum app-com.js beam.mjs beam.wasm worker.js".
3
- export default '6f6148b7c684c1118b9033a0e895df5091e4e0d063aef93417fa882c5fe3ceee';
3
+ export default 'b5c6d4198742c02f6c53a20a23c9a8858899eab45b42b9fb67c0e261ec6b21d0';
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,20 +1986,28 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
1973
1993
  // host tells wasm_tcp what the client took (tcp_sent).
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
- this.bridgeSend(c, bytes);
1976
- if (body) this.bridgeUpload(c, body, declared === null, maxBody);
1995
+ // With a body, the head goes in the event of the first part of the
1996
+ // body (bridgeUpload): a small body is one event, as before the parts.
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1998
+ else this.bridgeSend(c, bytes);
1977
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1978
2005
  }
1979
2006
 
1980
2007
  // Bytes to the app on the connection c.
1981
2008
  bridgeSend(c, bytes) {
1982
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1983
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1984
2012
  }
1985
2013
 
@@ -1987,8 +2015,13 @@ export class Vm {
1987
2015
  // (or the rest of the body): one event for each part, not for each small
1988
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1989
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1990
- // the memory of the VM, and a slow app slows the client.
1991
- async bridgeUpload(c, body, chunked, max) {
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
2024
+ async bridgeUpload(c, body, chunked, max, head) {
1992
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1993
2026
  // not safe: when the stream closes with fewer bytes than min, the read
1994
2027
  // errors the stream (Node.js 26, Chromium). So the other hosts use the
@@ -1997,7 +2030,7 @@ export class Vm {
1997
2030
  try { byob = body.getReader({ mode: 'byob' }); } catch {}
1998
2031
  if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
1999
2032
  const reader = byob ?? body.getReader();
2000
- const read = byob ? () => byob.readAtLeast(UPLOAD_PART, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2033
+ const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2001
2034
  let total = 0;
2002
2035
  let part = [];
2003
2036
  let size = 0;
@@ -2011,13 +2044,22 @@ export class Vm {
2011
2044
  const bytes = part.length === 1 ? part[0] : join(part, size);
2012
2045
  part = [];
2013
2046
  size = 0;
2014
- this.bridgeSend(c, chunked ? chunk(bytes) : bytes);
2047
+ out(chunked ? chunk(bytes) : bytes);
2015
2048
  return true;
2016
2049
  };
2050
+ // Bytes to the app, after the head when it did not go yet.
2051
+ const out = (bytes) => {
2052
+ clearTimeout(alone);
2053
+ this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
2054
+ head = null;
2055
+ };
2056
+ // A client that sends no body yet: the head goes alone after HEAD_WAIT
2057
+ // ms, so the app sees the request (and its own time limits run).
2058
+ const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
2017
2059
  try {
2018
2060
  for (;;) {
2019
2061
  // A read can end the stream with its last bytes (done and a value).
2020
- const { value, done } = await read();
2062
+ const { value, done } = await read(head ? 1 : UPLOAD_PART);
2021
2063
  if (c.done || this.dead) break;
2022
2064
  if (value?.length) {
2023
2065
  total += value.length;
@@ -2033,21 +2075,26 @@ export class Vm {
2033
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2034
2076
  part.push(piece);
2035
2077
  size += piece.length;
2036
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2037
2079
  }
2038
- if (ok && done && size) ok = await flush();
2080
+ if (ok && (done || head) && size) ok = await flush();
2039
2081
  if (!ok) break;
2040
2082
  if (done) {
2041
- if (chunked) this.bridgeSend(c, CHUNKS_END);
2083
+ if (chunked) out(CHUNKS_END);
2084
+ else if (head) out(new Uint8Array(0));
2042
2085
  this.bridgeClock(c, c.timeout * 1000);
2043
2086
  return;
2044
2087
  }
2045
2088
  }
2046
- reader.cancel().catch(() => {});
2047
2089
  } catch {
2048
2090
  // The client stopped the upload.
2049
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2050
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2051
2098
  }
2052
2099
 
2053
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2055,9 +2102,12 @@ export class Vm {
2055
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2056
2103
  }
2057
2104
 
2058
- // The app read n bytes of the connection c (tcp_read).
2059
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2060
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2061
2111
  this.bridgeRoom(c);
2062
2112
  c.inbound?.flush();
2063
2113
  }
@@ -2667,7 +2717,7 @@ export class Vm {
2667
2717
  }
2668
2718
  // wasm_tcp: the app read n bytes of a socket.
2669
2719
  case 'tcp_read': {
2670
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2671
2721
  break;
2672
2722
  }
2673
2723
  case 'tcp_close': {
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
2759
2809
  // read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
2760
2810
  // costs a turn of the VM.
2761
2811
  const UPLOAD_WINDOW = 256 * 1024;
2762
- const UPLOAD_PART = 16 * 1024;
2812
+ const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2814
+ // The time that the head of a request waits for the first bytes of its
2815
+ // body (bridgeUpload), in ms.
2816
+ const HEAD_WAIT = 20;
2763
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2764
2856
 
2765
2857
  // The size bytes of the list parts, as one array.
2766
2858
  function join(parts, size) {
@@ -1 +1 @@
1
- {"version": "0.1.0-rc.3", "sha256": "c9877e7ba31c2304aab52def9c0eee54ecf1f558d685a7acb4243c7fe2d0764e"}
1
+ {"version": "0.1.0-rc.5", "sha256": "ac36bab20b0cf8a6bcd9b9e02bae1e9d356855a75f4664ac65c58ff6fed8c024"}
@@ -1,3 +1,3 @@
1
1
  // The identity of this runtime (beam_com_wasm:runtime_id/1): the SHA-256 of
2
2
  // the output of "sha256sum app-com.js beam.mjs beam.wasm worker.js".
3
- export default '6f6148b7c684c1118b9033a0e895df5091e4e0d063aef93417fa882c5fe3ceee';
3
+ export default 'b5c6d4198742c02f6c53a20a23c9a8858899eab45b42b9fb67c0e261ec6b21d0';
package/runtime/worker.js CHANGED
@@ -285,6 +285,17 @@ export async function inflateSnapshot(snap) {
285
285
  // BEAM_ERL_FLAGS as one text, with one space between the flags.
286
286
  export const erlFlags = (env) => (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean).join(' ');
287
287
 
288
+ // The flags of the allocators of the VM, before BEAM_ERL_FLAGS (a later
289
+ // flag wins). A binary or a heap of 512 KB or more got its own carrier,
290
+ // and in WebAssembly these carriers made the memory of the VM grow far
291
+ // above the memory that Erlang used (14 MB): 16 clients that sent bodies
292
+ // of 2.7 MiB grew it to 1 GB and more. With a threshold of 8 MB, the peak
293
+ // was about 200 MB. With -Mea min, the allocators of ERTS are off, and
294
+ // these flags do not apply.
295
+ export function allocFlags(flags) {
296
+ return flags.includes('-Mea') ? [] : ['-MBsbct', '8192', '-MHsbct', '8192'];
297
+ }
298
+
288
299
  // BEAM_CONNECT: the hosts that the VM can connect to, separated by commas:
289
300
  // "host", "host:port", "*.domain" (the subdomains of domain), or "*" (all
290
301
  // hosts, as "*:443"). The host resolves a name, so the VM cannot reach
@@ -1340,7 +1351,7 @@ export class Vm {
1340
1351
  // Livebook starts with 40 MB, not 70 MB, but :erlang.memory/0 is
1341
1352
  // not supported).
1342
1353
  const flags = (env.BEAM_ERL_FLAGS ?? '').split(/\s+/).filter(Boolean);
1343
- m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1354
+ m.arguments.push('-S', '1', '-SDcpu', '1', '-A', '0', ...allocFlags(flags), ...flags, ...(this.makeKey || this.bootKey || this.capture ? ['-c', 'false'] : []), '--',
1344
1355
  '-root', '/app', '-bindir', '/app/bin', '-progname', 'erl', '--',
1345
1356
  '-home', '/', ...args, '-noshell');
1346
1357
  // Distributed Erlang over wasm_tcp, with no epmd (all nodes on
@@ -1664,12 +1675,20 @@ export class Vm {
1664
1675
  if (ctx) this.waitUntil = (p) => ctx.waitUntil(p);
1665
1676
  let finished = () => {};
1666
1677
  const h = this.plain ? this.serve(ctx, new Promise((r) => { finished = r; })) : undefined;
1678
+ let response;
1667
1679
  try {
1668
- return await this.request(request, h, finished);
1680
+ response = await this.request(request, h, finished);
1669
1681
  } catch (e) {
1670
1682
  finished();
1671
1683
  throw e;
1672
1684
  }
1685
+ // A response before the VM read the body (a limit of the host, a VM
1686
+ // that stopped): the body goes to no reader (see drain).
1687
+ if (request.body && !request.body.locked) {
1688
+ const reader = request.body.getReader();
1689
+ await drain(() => reader.read(), () => reader.cancel());
1690
+ }
1691
+ return response;
1673
1692
  }
1674
1693
 
1675
1694
  async request(request, h, finished) {
@@ -1953,8 +1972,9 @@ export class Vm {
1953
1972
  finished();
1954
1973
  return this.stopped();
1955
1974
  }
1956
- return new Promise((resolve) => {
1957
- const c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1975
+ let c;
1976
+ const response = await new Promise((resolve) => {
1977
+ c = { id, in: new Pieces(), resolve, finished, upgrade, head: request.method === 'HEAD', h, origin: request.headers.get('origin'), path: url.pathname, sent: 0, read: 0, room: null };
1958
1978
  if (h) h.sockets++;
1959
1979
  this.conns.add(c);
1960
1980
  c.timeout = timeout;
@@ -1966,20 +1986,28 @@ export class Vm {
1966
1986
  this.tcps.set(id, {
1967
1987
  send: (b) => { this.bridgeGuard(c, () => this.bridgeData(c, b)); return c.wrote; },
1968
1988
  close: () => this.bridgeGuard(c, () => this.bridgeEnd(c)),
1969
- ack: (n) => this.bridgeRead(c, n), h,
1989
+ ack: (n, msg) => this.bridgeRead(c, n, msg), h,
1970
1990
  });
1971
1991
  // ack: wasm_tcp tells the host what the app read (tcp_read): the
1972
1992
  // body of the request, and the messages of a WebSocket. sent: the
1973
1993
  // host tells wasm_tcp what the client took (tcp_sent).
1974
1994
  this.event({ t: 'tcp_accept', id: this.listeners.get(port), conn: id, host: request.headers.get('cf-connecting-ip') ?? '0.0.0.0', port: 0, ack: !!body || upgrade, sent: true });
1975
- this.bridgeSend(c, bytes);
1976
- if (body) this.bridgeUpload(c, body, declared === null, maxBody);
1995
+ // With a body, the head goes in the event of the first part of the
1996
+ // body (bridgeUpload): a small body is one event, as before the parts.
1997
+ if (body) c.upload = this.bridgeUpload(c, body, declared === null, maxBody, bytes);
1998
+ else this.bridgeSend(c, bytes);
1977
1999
  });
2000
+ // A response before the end of the body: the rest of the body goes to
2001
+ // the app while it reads, else bridgeUpload reads and drops it (see
2002
+ // drain). The response waits for that, DRAIN_TIME ms at most.
2003
+ if (c.upload) await within(c.upload, DRAIN_TIME);
2004
+ return response;
1978
2005
  }
1979
2006
 
1980
2007
  // Bytes to the app on the connection c.
1981
2008
  bridgeSend(c, bytes) {
1982
2009
  c.sent += bytes.length;
2010
+ if (c.need) c.need = Math.max(0, c.need - bytes.length);
1983
2011
  this.event({ t: 'tcp_data', id: c.id }, bytes);
1984
2012
  }
1985
2013
 
@@ -1987,8 +2015,13 @@ export class Vm {
1987
2015
  // (or the rest of the body): one event for each part, not for each small
1988
2016
  // chunk of the client. A part goes when less than UPLOAD_WINDOW bytes are
1989
2017
  // unread in the VM (tcp_read of wasm_tcp): so a large body does not fill
1990
- // the memory of the VM, and a slow app slows the client.
1991
- async bridgeUpload(c, body, chunked, max) {
2018
+ // the memory of the VM, and a slow app slows the client. While a recv of
2019
+ // the app waits for more bytes (c.need, as Bandit reads a body), a part
2020
+ // has those bytes, UPLOAD_LARGE at most: the app holds them anyway, and
2021
+ // each event costs a turn of the VM. The head of the request goes with
2022
+ // the bytes of the first read of the client, with no wait for
2023
+ // UPLOAD_PART bytes.
2024
+ async bridgeUpload(c, body, chunked, max, head) {
1992
2025
  // workerd has readAtLeast. The option min of the standard BYOB read is
1993
2026
  // not safe: when the stream closes with fewer bytes than min, the read
1994
2027
  // errors the stream (Node.js 26, Chromium). So the other hosts use the
@@ -1997,7 +2030,7 @@ export class Vm {
1997
2030
  try { byob = body.getReader({ mode: 'byob' }); } catch {}
1998
2031
  if (byob && !byob.readAtLeast) { byob.releaseLock(); byob = null; }
1999
2032
  const reader = byob ?? body.getReader();
2000
- const read = byob ? () => byob.readAtLeast(UPLOAD_PART, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2033
+ const read = byob ? (min) => byob.readAtLeast(min, new Uint8Array(UPLOAD_READ)) : () => reader.read();
2001
2034
  let total = 0;
2002
2035
  let part = [];
2003
2036
  let size = 0;
@@ -2011,13 +2044,22 @@ export class Vm {
2011
2044
  const bytes = part.length === 1 ? part[0] : join(part, size);
2012
2045
  part = [];
2013
2046
  size = 0;
2014
- this.bridgeSend(c, chunked ? chunk(bytes) : bytes);
2047
+ out(chunked ? chunk(bytes) : bytes);
2015
2048
  return true;
2016
2049
  };
2050
+ // Bytes to the app, after the head when it did not go yet.
2051
+ const out = (bytes) => {
2052
+ clearTimeout(alone);
2053
+ this.bridgeSend(c, head ? join([head, bytes], head.length + bytes.length) : bytes);
2054
+ head = null;
2055
+ };
2056
+ // A client that sends no body yet: the head goes alone after HEAD_WAIT
2057
+ // ms, so the app sees the request (and its own time limits run).
2058
+ const alone = setTimeout(() => { if (head && !c.done && !this.dead) out(new Uint8Array(0)); }, HEAD_WAIT);
2017
2059
  try {
2018
2060
  for (;;) {
2019
2061
  // A read can end the stream with its last bytes (done and a value).
2020
- const { value, done } = await read();
2062
+ const { value, done } = await read(head ? 1 : UPLOAD_PART);
2021
2063
  if (c.done || this.dead) break;
2022
2064
  if (value?.length) {
2023
2065
  total += value.length;
@@ -2033,21 +2075,26 @@ export class Vm {
2033
2075
  const piece = value.subarray(at, at + UPLOAD_READ);
2034
2076
  part.push(piece);
2035
2077
  size += piece.length;
2036
- if (size >= UPLOAD_PART) ok = await flush();
2078
+ if (size >= Math.min(Math.max(c.need ?? 0, UPLOAD_PART), UPLOAD_LARGE)) ok = await flush();
2037
2079
  }
2038
- if (ok && done && size) ok = await flush();
2080
+ if (ok && (done || head) && size) ok = await flush();
2039
2081
  if (!ok) break;
2040
2082
  if (done) {
2041
- if (chunked) this.bridgeSend(c, CHUNKS_END);
2083
+ if (chunked) out(CHUNKS_END);
2084
+ else if (head) out(new Uint8Array(0));
2042
2085
  this.bridgeClock(c, c.timeout * 1000);
2043
2086
  return;
2044
2087
  }
2045
2088
  }
2046
- reader.cancel().catch(() => {});
2047
2089
  } catch {
2048
2090
  // The client stopped the upload.
2049
2091
  this.bridgeRefuse(c, new Response('bad request\n', { status: 400 }));
2092
+ return;
2093
+ } finally {
2094
+ clearTimeout(alone);
2050
2095
  }
2096
+ // The app ended the connection, or the body is above BEAM_MAX_BODY.
2097
+ await drain(() => read(UPLOAD_PART), () => reader.cancel());
2051
2098
  }
2052
2099
 
2053
2100
  // The time limit of the response of c: BEAM_REQUEST_TIMEOUT, in ms ms.
@@ -2055,9 +2102,12 @@ export class Vm {
2055
2102
  if (c.timeout && !c.status && !c.done) c.timer = setTimeout(() => this.bridgeTimeout(c, c.timeout), Math.max(ms, 1));
2056
2103
  }
2057
2104
 
2058
- // The app read n bytes of the connection c (tcp_read).
2059
- bridgeRead(c, n) {
2105
+ // The app read n bytes of the connection c (tcp_read). msg.want and
2106
+ // msg.got give the bytes that a recv of the app still waits for, beyond
2107
+ // the bytes on their way to it (c.need).
2108
+ bridgeRead(c, n, msg) {
2060
2109
  c.read += n;
2110
+ if (msg?.want !== undefined) c.need = Math.max(0, msg.got + msg.want - c.sent);
2061
2111
  this.bridgeRoom(c);
2062
2112
  c.inbound?.flush();
2063
2113
  }
@@ -2667,7 +2717,7 @@ export class Vm {
2667
2717
  }
2668
2718
  // wasm_tcp: the app read n bytes of a socket.
2669
2719
  case 'tcp_read': {
2670
- this.tcps.get(msg.id)?.ack?.(msg.n);
2720
+ this.tcps.get(msg.id)?.ack?.(msg.n, msg);
2671
2721
  break;
2672
2722
  }
2673
2723
  case 'tcp_close': {
@@ -2759,8 +2809,50 @@ const LATE = Symbol('late');
2759
2809
  // read in workerd and of the largest piece of a client chunk. A client chunk can be 2 KB (workerd), and each event
2760
2810
  // costs a turn of the VM.
2761
2811
  const UPLOAD_WINDOW = 256 * 1024;
2762
- const UPLOAD_PART = 16 * 1024;
2812
+ const UPLOAD_PART = 32 * 1024;
2813
+ const UPLOAD_LARGE = 1024 * 1024;
2814
+ // The time that the head of a request waits for the first bytes of its
2815
+ // body (bridgeUpload), in ms.
2816
+ const HEAD_WAIT = 20;
2763
2817
  const UPLOAD_READ = 64 * 1024;
2818
+ // The rest of a request body that the app did not read (drain): the
2819
+ // bytes that the host reads at most before the response goes, the ms with
2820
+ // no new bytes, and the ms in all.
2821
+ const DRAIN_BYTES = 64 * 1024 * 1024;
2822
+ const DRAIN_IDLE = 5000;
2823
+ const DRAIN_TIME = 60000;
2824
+
2825
+ // The rest of a request body, read and dropped before the response goes,
2826
+ // when the app answered before it read the whole body (a 413, for
2827
+ // example). workerd cannot read a request body after the response has
2828
+ // gone, and it then closes the connection with the bytes that were not
2829
+ // read. wrangler dev uses that connection again, and its next request
2830
+ // fails with 500. The read stops after DRAIN_BYTES, after DRAIN_IDLE ms
2831
+ // with no bytes, or after DRAIN_TIME ms, and then cancel() ends the body.
2832
+ // read() gives { value, done }. limits replaces the bounds (a test).
2833
+ export async function drain(read, cancel, { bytes = DRAIN_BYTES, idle = DRAIN_IDLE, time = DRAIN_TIME } = {}) {
2834
+ const end = Date.now() + time;
2835
+ let n = 0;
2836
+ try {
2837
+ for (;;) {
2838
+ const r = await within(read(), Math.min(idle, end - Date.now()));
2839
+ if (r === LATE) break;
2840
+ if (r.done) return;
2841
+ n += r.value?.byteLength ?? 0;
2842
+ if (n > bytes || Date.now() >= end) break;
2843
+ }
2844
+ } catch {
2845
+ return; // the client stopped the body
2846
+ }
2847
+ try { await cancel(); } catch {}
2848
+ }
2849
+
2850
+ // The promise p, or LATE after ms ms.
2851
+ function within(p, ms) {
2852
+ let timer;
2853
+ const late = new Promise((r) => { timer = setTimeout(r, Math.max(ms, 0), LATE); });
2854
+ return Promise.race([p, late]).finally(() => clearTimeout(timer));
2855
+ }
2764
2856
 
2765
2857
  // The size bytes of the list parts, as one array.
2766
2858
  function join(parts, size) {